Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions fastjsonschema/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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)')


Expand Down
18 changes: 15 additions & 3 deletions fastjsonschema/indent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions tests/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])