mirror of
https://github.com/aykhans/AzSuicideDataVisualization.git
synced 2025-04-22 02:23:48 +00:00
313 lines
11 KiB
Python
313 lines
11 KiB
Python
from collections import OrderedDict
|
|
import re
|
|
|
|
from .exceptions import JsonSchemaValueException, JsonSchemaDefinitionException
|
|
from .indent import indent
|
|
from .ref_resolver import RefResolver
|
|
|
|
|
|
def enforce_list(variable):
|
|
if isinstance(variable, list):
|
|
return variable
|
|
return [variable]
|
|
|
|
|
|
# pylint: disable=too-many-instance-attributes,too-many-public-methods
|
|
class CodeGenerator:
|
|
"""
|
|
This class is not supposed to be used directly. Anything
|
|
inside of this class can be changed without noticing.
|
|
|
|
This class generates code of validation function from JSON
|
|
schema object as string. Example:
|
|
|
|
.. code-block:: python
|
|
|
|
CodeGenerator(json_schema_definition).func_code
|
|
"""
|
|
|
|
INDENT = 4 # spaces
|
|
|
|
def __init__(self, definition, resolver=None):
|
|
self._code = []
|
|
self._compile_regexps = {}
|
|
self._custom_formats = {}
|
|
|
|
# Any extra library should be here to be imported only once.
|
|
# Lines are imports to be printed in the file and objects
|
|
# key-value pair to pass to compile function directly.
|
|
self._extra_imports_lines = []
|
|
self._extra_imports_objects = {}
|
|
|
|
self._variables = set()
|
|
self._indent = 0
|
|
self._indent_last_line = None
|
|
self._variable = None
|
|
self._variable_name = None
|
|
self._root_definition = definition
|
|
self._definition = None
|
|
|
|
# map schema URIs to validation function names for functions
|
|
# that are not yet generated, but need to be generated
|
|
self._needed_validation_functions = {}
|
|
# validation function names that are already done
|
|
self._validation_functions_done = set()
|
|
|
|
if resolver is None:
|
|
resolver = RefResolver.from_schema(definition, store={})
|
|
self._resolver = resolver
|
|
|
|
# add main function to `self._needed_validation_functions`
|
|
self._needed_validation_functions[self._resolver.get_uri()] = self._resolver.get_scope_name()
|
|
|
|
self._json_keywords_to_function = OrderedDict()
|
|
|
|
@property
|
|
def func_code(self):
|
|
"""
|
|
Returns generated code of whole validation function as string.
|
|
"""
|
|
self._generate_func_code()
|
|
|
|
return '\n'.join(self._code)
|
|
|
|
@property
|
|
def global_state(self):
|
|
"""
|
|
Returns global variables for generating function from ``func_code``. Includes
|
|
compiled regular expressions and imports, so it does not have to do it every
|
|
time when validation function is called.
|
|
"""
|
|
self._generate_func_code()
|
|
|
|
return dict(
|
|
**self._extra_imports_objects,
|
|
REGEX_PATTERNS=self._compile_regexps,
|
|
re=re,
|
|
JsonSchemaValueException=JsonSchemaValueException,
|
|
)
|
|
|
|
@property
|
|
def global_state_code(self):
|
|
"""
|
|
Returns global variables for generating function from ``func_code`` as code.
|
|
Includes compiled regular expressions and imports.
|
|
"""
|
|
self._generate_func_code()
|
|
|
|
if not self._compile_regexps:
|
|
return '\n'.join(self._extra_imports_lines + [
|
|
'from fastjsonschema import JsonSchemaValueException',
|
|
'',
|
|
'',
|
|
])
|
|
return '\n'.join(self._extra_imports_lines + [
|
|
'import re',
|
|
'from fastjsonschema import JsonSchemaValueException',
|
|
'',
|
|
'',
|
|
'REGEX_PATTERNS = ' + serialize_regexes(self._compile_regexps),
|
|
'',
|
|
])
|
|
|
|
|
|
def _generate_func_code(self):
|
|
if not self._code:
|
|
self.generate_func_code()
|
|
|
|
def generate_func_code(self):
|
|
"""
|
|
Creates base code of validation function and calls helper
|
|
for creating code by definition.
|
|
"""
|
|
self.l('NoneType = type(None)')
|
|
# Generate parts that are referenced and not yet generated
|
|
while self._needed_validation_functions:
|
|
# During generation of validation function, could be needed to generate
|
|
# new one that is added again to `_needed_validation_functions`.
|
|
# Therefore usage of while instead of for loop.
|
|
uri, name = self._needed_validation_functions.popitem()
|
|
self.generate_validation_function(uri, name)
|
|
|
|
def generate_validation_function(self, uri, name):
|
|
"""
|
|
Generate validation function for given uri with given name
|
|
"""
|
|
self._validation_functions_done.add(uri)
|
|
self.l('')
|
|
with self._resolver.resolving(uri) as definition:
|
|
with self.l('def {}(data, custom_formats={{}}):', name):
|
|
self.generate_func_code_block(definition, 'data', 'data', clear_variables=True)
|
|
self.l('return data')
|
|
|
|
def generate_func_code_block(self, definition, variable, variable_name, clear_variables=False):
|
|
"""
|
|
Creates validation rules for current definition.
|
|
"""
|
|
backup = self._definition, self._variable, self._variable_name
|
|
self._definition, self._variable, self._variable_name = definition, variable, variable_name
|
|
if clear_variables:
|
|
backup_variables = self._variables
|
|
self._variables = set()
|
|
|
|
self._generate_func_code_block(definition)
|
|
|
|
self._definition, self._variable, self._variable_name = backup
|
|
if clear_variables:
|
|
self._variables = backup_variables
|
|
|
|
def _generate_func_code_block(self, definition):
|
|
if not isinstance(definition, dict):
|
|
raise JsonSchemaDefinitionException("definition must be an object")
|
|
if '$ref' in definition:
|
|
# needed because ref overrides any sibling keywords
|
|
self.generate_ref()
|
|
else:
|
|
self.run_generate_functions(definition)
|
|
|
|
def run_generate_functions(self, definition):
|
|
for key, func in self._json_keywords_to_function.items():
|
|
if key in definition:
|
|
func()
|
|
|
|
def generate_ref(self):
|
|
"""
|
|
Ref can be link to remote or local definition.
|
|
|
|
.. code-block:: python
|
|
|
|
{'$ref': 'http://json-schema.org/draft-04/schema#'}
|
|
{
|
|
'properties': {
|
|
'foo': {'type': 'integer'},
|
|
'bar': {'$ref': '#/properties/foo'}
|
|
}
|
|
}
|
|
"""
|
|
with self._resolver.in_scope(self._definition['$ref']):
|
|
name = self._resolver.get_scope_name()
|
|
uri = self._resolver.get_uri()
|
|
if uri not in self._validation_functions_done:
|
|
self._needed_validation_functions[uri] = name
|
|
# call validation function
|
|
self.l('{}({variable}, custom_formats)', name)
|
|
|
|
|
|
# pylint: disable=invalid-name
|
|
@indent
|
|
def l(self, line, *args, **kwds):
|
|
"""
|
|
Short-cut of line. Used for inserting line. It's formated with parameters
|
|
``variable``, ``variable_name`` (as ``name`` for short-cut), all keys from
|
|
current JSON schema ``definition`` and also passed arguments in ``args``
|
|
and named ``kwds``.
|
|
|
|
.. code-block:: python
|
|
|
|
self.l('if {variable} not in {enum}: raise JsonSchemaValueException("Wrong!")')
|
|
|
|
When you want to indent block, use it as context manager. For example:
|
|
|
|
.. code-block:: python
|
|
|
|
with self.l('if {variable} not in {enum}:'):
|
|
self.l('raise JsonSchemaValueException("Wrong!")')
|
|
"""
|
|
spaces = ' ' * self.INDENT * self._indent
|
|
|
|
name = self._variable_name
|
|
if name and '{' in name:
|
|
name = '"+"{}".format(**locals())+"'.format(self._variable_name)
|
|
|
|
context = dict(
|
|
self._definition or {},
|
|
variable=self._variable,
|
|
name=name,
|
|
**kwds
|
|
)
|
|
line = line.format(*args, **context)
|
|
line = line.replace('\n', '\\n').replace('\r', '\\r')
|
|
self._code.append(spaces + line)
|
|
return line
|
|
|
|
def e(self, string):
|
|
"""
|
|
Short-cut of escape. Used for inserting user values into a string message.
|
|
|
|
.. code-block:: python
|
|
|
|
self.l('raise JsonSchemaValueException("Variable: {}")', self.e(variable))
|
|
"""
|
|
return str(string).replace('"', '\\"')
|
|
|
|
def exc(self, msg, *args, rule=None):
|
|
"""
|
|
Short-cut for creating raising exception in the code.
|
|
"""
|
|
msg = 'raise JsonSchemaValueException("'+msg+'", value={variable}, name="{name}", definition={definition}, rule={rule})'
|
|
definition_rule = self.e(self._definition.get(rule) if isinstance(self._definition, dict) else None)
|
|
self.l(msg, *args, definition=repr(self._definition), rule=repr(rule), definition_rule=definition_rule)
|
|
|
|
def create_variable_with_length(self):
|
|
"""
|
|
Append code for creating variable with length of that variable
|
|
(for example length of list or dictionary) with name ``{variable}_len``.
|
|
It can be called several times and always it's done only when that variable
|
|
still does not exists.
|
|
"""
|
|
variable_name = '{}_len'.format(self._variable)
|
|
if variable_name in self._variables:
|
|
return
|
|
self._variables.add(variable_name)
|
|
self.l('{variable}_len = len({variable})')
|
|
|
|
def create_variable_keys(self):
|
|
"""
|
|
Append code for creating variable with keys of that variable (dictionary)
|
|
with a name ``{variable}_keys``. Similar to `create_variable_with_length`.
|
|
"""
|
|
variable_name = '{}_keys'.format(self._variable)
|
|
if variable_name in self._variables:
|
|
return
|
|
self._variables.add(variable_name)
|
|
self.l('{variable}_keys = set({variable}.keys())')
|
|
|
|
def create_variable_is_list(self):
|
|
"""
|
|
Append code for creating variable with bool if it's instance of list
|
|
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:
|
|
return
|
|
self._variables.add(variable_name)
|
|
self.l('{variable}_is_list = isinstance({variable}, (list, tuple))')
|
|
|
|
def create_variable_is_dict(self):
|
|
"""
|
|
Append code for creating variable with bool if it's instance of list
|
|
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:
|
|
return
|
|
self._variables.add(variable_name)
|
|
self.l('{variable}_is_dict = isinstance({variable}, dict)')
|
|
|
|
|
|
def serialize_regexes(patterns_dict):
|
|
# Unfortunately using `pprint.pformat` is causing errors
|
|
# specially with big regexes
|
|
regex_patterns = (
|
|
repr(k) + ": " + repr_regex(v)
|
|
for k, v in patterns_dict.items()
|
|
)
|
|
return '{\n ' + ",\n ".join(regex_patterns) + "\n}"
|
|
|
|
|
|
def repr_regex(regex):
|
|
all_flags = ("A", "I", "DEBUG", "L", "M", "S", "X")
|
|
flags = " | ".join(f"re.{f}" for f in all_flags if regex.flags & getattr(re, f))
|
|
flags = ", " + flags if flags else ""
|
|
return "re.compile({!r}{})".format(regex.pattern, flags)
|