Skip to content

Commit

Permalink
Better implementation of custom formats
Browse files Browse the repository at this point in the history
  • Loading branch information
horejsek committed Sep 25, 2019
1 parent aa4ea89 commit b21744f
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 47 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
=== 2.14 (unreleased)

* Possibility to pass custom formats
* Fix of date-time regexp (time zone is required by RFC 3339)


Expand Down
15 changes: 13 additions & 2 deletions fastjsonschema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,17 @@ def compile(definition, handlers={}, formats={}):
You can pass mapping from URI to function that should be used to retrieve
remote schemes used in your ``definition`` in parameter ``handlers``.
Also, you can pass mapping for custom formats. Key is the name of your
formatter and value can be regular expression which will be compiled or
callback returning `bool` (or you can raise your own exception).
.. code-block:: python
validate = fastjsonschema.compile(definition, formats={
'foo': r'foo|bar',
'bar': lambda value: value in ('foo', 'bar'),
})
Exception :any:`JsonSchemaDefinitionException` is raised when generating the
code fails (bad definition).
Expand All @@ -158,7 +169,7 @@ def compile(definition, handlers={}, formats={}):


# pylint: disable=dangerous-default-value
def compile_to_code(definition, handlers={}):
def compile_to_code(definition, handlers={}, formats={}):
"""
Generates validation code for validating JSON schema passed in ``definition``.
Example:
Expand All @@ -181,7 +192,7 @@ def compile_to_code(definition, handlers={}):
Exception :any:`JsonSchemaDefinitionException` is raised when generating the
code fails (bad definition).
"""
_, code_generator = _factory(definition, handlers)
_, code_generator = _factory(definition, handlers, formats)
return (
'VERSION = "' + VERSION + '"\n' +
code_generator.global_state_code + '\n' +
Expand Down
31 changes: 20 additions & 11 deletions fastjsonschema/draft04.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class CodeGeneratorDraft04(CodeGenerator):
}

def __init__(self, definition, resolver=None, formats={}):
super().__init__(definition, resolver, formats)
super().__init__(definition, resolver)
self._custom_formats = formats
self._json_keywords_to_function.update((
('type', self.generate_type),
('enum', self.generate_enum),
Expand Down Expand Up @@ -62,6 +63,12 @@ def __init__(self, definition, resolver=None, formats={}):
('dependencies', self.generate_dependencies),
))

@property
def global_state(self):
res = super().global_state
res['custom_formats'] = self._custom_formats
return res

def generate_type(self):
"""
Validation of type. Can be one type or list of types.
Expand Down Expand Up @@ -239,24 +246,26 @@ def generate_format(self):
"""
with self.l('if isinstance({variable}, str):'):
format_ = self._definition['format']
if format_ in self.FORMAT_REGEXS:
# Checking custom formats - user is allowed to override default formats.
if format_ in self._custom_formats:
custom_format = self._custom_formats[format_]
if isinstance(custom_format, str):
self._generate_format(format_, format_ + '_re_pattern', custom_format)
else:
with self.l('if not custom_formats["{}"]({variable}):', format_):
self.l('raise JsonSchemaException("{name} must be {}")', format_)
elif format_ in self.FORMAT_REGEXS:
format_regex = self.FORMAT_REGEXS[format_]
self._generate_format(format_, format_ + '_re_pattern', format_regex)
# format regex is used only in meta schemas
# Format regex is used only in meta schemas.
elif format_ == 'regex':
with self.l('try:'):
self.l('re.compile({variable})')
with self.l('except Exception:'):
self.l('raise JsonSchemaException("{name} must be a valid regex")')

# format checking from format callable
if format_ in self._formats:
with self.l('try:'):
self.l('formats[{}]({variable})', repr(format_))
with self.l('except Exception as e:'):
self.l('raise JsonSchemaException("{name} is not a valid {variable}") from e')
else:
self.l('pass')
raise JsonSchemaDefinitionException('Undefined format %s'.format(format_))


def _generate_format(self, format_name, regexp_name, regexp):
if self._definition['format'] == format_name:
Expand Down
11 changes: 1 addition & 10 deletions fastjsonschema/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class CodeGenerator:

INDENT = 4 # spaces

def __init__(self, definition, resolver=None, formats={}):
def __init__(self, definition, resolver=None):
self._code = []
self._compile_regexps = {}

Expand All @@ -49,14 +49,6 @@ def __init__(self, definition, resolver=None, formats={}):
resolver = RefResolver.from_schema(definition)
self._resolver = resolver

# Initialize formats values as callable
for key, value in formats.items():
if isinstance(value, str):
formats[key] = lambda data: re.match(value, data)
elif isinstance(value, re.Pattern):
formats[key] = lambda data: value.match(data)
self._formats = formats

# add main function to `self._needed_validation_functions`
self._needed_validation_functions[self._resolver.get_uri()] = self._resolver.get_scope_name()

Expand Down Expand Up @@ -84,7 +76,6 @@ def global_state(self):
REGEX_PATTERNS=self._compile_regexps,
re=re,
JsonSchemaException=JsonSchemaException,
formats=self._formats
)

@property
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
def asserter():
def f(definition, value, expected, formats={}):
# When test fails, it will show up code.
code_generator = CodeGeneratorDraft07(definition)
code_generator = CodeGeneratorDraft07(definition, formats=formats)
print(code_generator.func_code)
pprint(code_generator.global_state)

Expand Down
40 changes: 17 additions & 23 deletions tests/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,22 @@ def test_hostname(asserter, value, expected):
asserter({'type': 'string', 'format': 'hostname'}, value, expected)


def __special_timestamp_format_checker(date_string: str) -> bool:
dt = datetime.datetime.fromisoformat(date_string).replace(tzinfo=datetime.timezone.utc)
dt_now = datetime.datetime.now(datetime.timezone.utc)
if dt > dt_now:
raise ValueError(f"{date_string} is in the future")
return True


pattern = "^(19|20)[0-9][0-9]-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([" \
"0-5][0-9])\\.[0-9]{6}$ "
exc = JsonSchemaException('data must be custom-format')
@pytest.mark.parametrize('value,expected,custom_format', [
('', exc, r'^[ab]$'),
('', exc, lambda value: value in ('a', 'b')),
('a', 'a', r'^[ab]$'),
('a', 'a', lambda value: value in ('a', 'b')),
('c', exc, r'^[ab]$'),
('c', exc, lambda value: value in ('a', 'b')),
])
def test_custom_format(asserter, value, expected, custom_format):
asserter({'format': 'custom-format'}, value, expected, formats={
'custom-format': custom_format,
})


exc = JsonSchemaException('data is not a valid data')
@pytest.mark.parametrize('value, expected, formats', [
('', exc, {"special-timestamp": __special_timestamp_format_checker}),
('bla', exc, {"special-timestamp": __special_timestamp_format_checker}),
('2018-02-05T14:17:10.00', exc, {"special-timestamp": __special_timestamp_format_checker}),
('2019-03-12 13:08:03.001000\n', exc, {"special-timestamp": __special_timestamp_format_checker}),
('2999-03-12 13:08:03.001000', '2999-03-12 13:08:03.001000', exc, {"special-timestamp": __special_timestamp_format_checker}),
('2019-03-12 13:08:03.001000', '2019-03-12 13:08:03.001000', {"special-timestamp": __special_timestamp_format_checker}),
('2019-03-12 13:08:03.001000', '2019-03-12 13:08:03.001000', {"special-timestamp": pattern}),
('2019-03-12 13:08:03.001000', '2019-03-12 13:08:03.001000', {"special-timestamp": re.compile(pattern)}),
])
def test_special_datetime(asserter, value, expected, formats):
asserter({'type': 'string', 'format': 'special-timestamp'}, value, expected, formats=formats)
def test_custom_format_override(asserter):
asserter({'format': 'date-time'}, 'a', 'a', formats={
'date-time': r'^[ab]$',
})

3 comments on commit b21744f

@ystreibel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @horejsek,

You change the __init__ function in generator.py but you don't change the code of draft06.py and draft07.py.
When I launch a make test I have 30 failed tests.

@ystreibel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, inheritance! But I still have 30 failed test. I will try to understand this!!!

@horejsek
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, it was work in progress. See the latest version. :-)

Please sign in to comment.