diff --git a/fastjsonschema/generator.py b/fastjsonschema/generator.py index 7858774..f6ac2b9 100644 --- a/fastjsonschema/generator.py +++ b/fastjsonschema/generator.py @@ -46,7 +46,10 @@ def __init__(self, definition, resolver=None, detailed_exceptions=True, fast_fai "Decimal": Decimal, } - self._variables = set() + self._variables = {} + self._scope_stack = [] + self._scope_counter = 0 + self._last_closed_scope = None self._indent = 0 self._indent_last_line = None self._variable = None @@ -162,7 +165,7 @@ def generate_func_code_block(self, definition, variable, variable_name, clear_va self._definition, self._variable, self._variable_name = definition, variable, variable_name if clear_variables: backup_variables = self._variables - self._variables = set() + self._variables = {} count = self._generate_func_code_block(definition) @@ -304,6 +307,17 @@ def _expand_refs(self, definition): return schema return {k: self._expand_refs(v) for k, v in definition.items()} + def _is_variable_in_scope(self, variable_name): + """ + Whether ``variable_name`` was already defined in a block enclosing the + current one, and is therefore still bound here. A variable defined in a + sibling block is not, because that block may not have been entered. + """ + scope = self._variables.get(variable_name) + if scope is None: + return False + return tuple(self._scope_stack[:len(scope)]) == scope + def create_variable_with_length(self): """ Append code for creating variable with length of that variable @@ -312,9 +326,9 @@ def create_variable_with_length(self): still does not exists. """ variable_name = '{}_len'.format(self._variable) - if variable_name in self._variables: + if self._is_variable_in_scope(variable_name): return - self._variables.add(variable_name) + self._variables[variable_name] = tuple(self._scope_stack) self.l('{variable}_len = len({variable})') def create_variable_keys(self): @@ -323,9 +337,9 @@ def create_variable_keys(self): with a name ``{variable}_keys``. Similar to `create_variable_with_length`. """ variable_name = '{}_keys'.format(self._variable) - if variable_name in self._variables: + if self._is_variable_in_scope(variable_name): return - self._variables.add(variable_name) + self._variables[variable_name] = tuple(self._scope_stack) self.l('{variable}_keys = set({variable}.keys())') def create_variable_is_list(self): @@ -334,9 +348,9 @@ def create_variable_is_list(self): with a name ``{variable}_is_list``. Similar to `create_variable_with_length`. """ variable_name = '{}_is_list'.format(self._variable) - if variable_name in self._variables: + if self._is_variable_in_scope(variable_name): return - self._variables.add(variable_name) + self._variables[variable_name] = tuple(self._scope_stack) self.l('{variable}_is_list = isinstance({variable}, (list, tuple))') def create_variable_is_dict(self): @@ -345,9 +359,9 @@ def create_variable_is_dict(self): with a name ``{variable}_is_dict``. Similar to `create_variable_with_length`. """ variable_name = '{}_is_dict'.format(self._variable) - if variable_name in self._variables: + if self._is_variable_in_scope(variable_name): return - self._variables.add(variable_name) + self._variables[variable_name] = tuple(self._scope_stack) self.l('{variable}_is_dict = isinstance({variable}, dict)') diff --git a/fastjsonschema/indent.py b/fastjsonschema/indent.py index 2526c6b..9cc011a 100644 --- a/fastjsonschema/indent.py +++ b/fastjsonschema/indent.py @@ -10,21 +10,33 @@ def wrapper(self, line, *args, optimize=True, **kwds): line = func(self, line, *args, **kwds) # When two blocks have the same condition (such as value has to be dict), # do the check only once and keep it under one block. - if optimize and last_line == line: + merged = optimize and last_line == line + if merged: self._code.pop() self._indent_last_line = line - return Indent(self, line) + return Indent(self, line, merged=merged) return wrapper class Indent: - def __init__(self, instance, line): + def __init__(self, instance, line, merged=False): self.instance = instance self.line = line + self.merged = merged def __enter__(self): self.instance._indent += 1 + # A merged block is a continuation of the block just closed, so it keeps + # its scope; otherwise this is a new scope and variables defined in it + # are not visible to sibling blocks. + if self.merged and self.instance._last_closed_scope is not None: + scope = self.instance._last_closed_scope + else: + self.instance._scope_counter += 1 + scope = self.instance._scope_counter + self.instance._scope_stack.append(scope) def __exit__(self, type_, value, traceback): self.instance._indent -= 1 + self.instance._last_closed_scope = self.instance._scope_stack.pop() self.instance._indent_last_line = self.line diff --git a/tests/test_array.py b/tests/test_array.py index 20f7efd..2de8e6e 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -218,3 +218,14 @@ def test_additional_items_subschema_without_validation_code(asserter): 'items': [{'type': 'string'}], 'additionalItems': {'format': 'unknown-format'}, }, ['a', 1], ['a', 1]) + + +def test_min_items_with_min_length(asserter): + # regression test for issue #157: minLength defines data_len inside an + # `isinstance(data, str)` block, so minItems must not reuse it in its own + # block, where it is not bound. + asserter({'minItems': 1, 'minLength': 1}, ['str'], ['str']) + + +def test_max_items_with_max_length(asserter): + asserter({'maxItems': 2, 'maxLength': 2}, ['a'], ['a'])