diff --git a/.gitignore b/.gitignore index 1003006..07c59ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .coverage docs/build +**/__pycache__/* +*.pyc +*.egg-info* diff --git a/README.rst b/README.rst index 4c81061..e174f7a 100644 --- a/README.rst +++ b/README.rst @@ -73,7 +73,7 @@ License .. _Jinja2: http://jinja.pocoo.org/docs/ .. _Demo: http://jinja2schema.aromanovich.ru/ .. _demo page: http://jinja2schema.aromanovich.ru/ -.. _Documentation: http://jinja2schema.rtfd.org/ +.. _Documentation: https://jinja2schema.readthedocs.io/ .. _GitHub: https://github.com/aromanovich/jinja2schema .. _PyPI: https://pypi.python.org/pypi/jinja2schema .. _BSD license: https://github.com/aromanovich/jinja2schema/blob/master/LICENSE diff --git a/jinja2schema/__init__.py b/jinja2schema/__init__.py index 2ced7c6..0aaa35d 100644 --- a/jinja2schema/__init__.py +++ b/jinja2schema/__init__.py @@ -6,9 +6,9 @@ Type inference for Jinja2 templates. -See http://jinja2schema.rtfd.org/ for documentation. +See https://jinja2schema.readthedocs.io/ for documentation. -:copyright: (c) 2014 Anton Romanovich +:copyright: (c) 2017 Anton Romanovich :license: BSD """ @@ -16,8 +16,8 @@ __title__ = 'jinja2schema' __author__ = 'Anton Romanovich' __license__ = 'BSD' -__copyright__ = 'Copyright 2014 Anton Romanovich' -__version__ = '0.1.1' +__copyright__ = 'Copyright 2017 Anton Romanovich' +__version__ = '0.1.4' __version_info__ = tuple(int(i) for i in __version__.split('.')) diff --git a/jinja2schema/config.py b/jinja2schema/config.py index 7f48cad..a72b53d 100644 --- a/jinja2schema/config.py +++ b/jinja2schema/config.py @@ -3,6 +3,7 @@ jinja2schema.config ~~~~~~~~~~~~~~~~~~~ """ +from .order_number import OrderNumber class Config(object): @@ -47,12 +48,26 @@ class Config(object): this configuration is not needed. """ + ORDER_NUMBER = False + """Add a order number to each node + + Add a order number to make schema sortable. + """ + + ORDER_NUMBER_SUB_COUNTER = True + """Independent subsection order numbers + + Use a separate counter in subsections as order number creator. + """ + def __init__(self, TYPE_OF_VARIABLE_INDEXED_WITH_VARIABLE_TYPE='dictionary', TYPE_OF_VARIABLE_INDEXED_WITH_INTEGER_TYPE='list', BOOLEAN_CONDITIONS=False, PACKAGE_NAME='', - TEMPLATE_DIR='templates'): + TEMPLATE_DIR='templates', + ORDER_NUMBER=False, + ORDER_NUMBER_SUB_COUNTER=True): if TYPE_OF_VARIABLE_INDEXED_WITH_VARIABLE_TYPE not in ('dictionary', 'list'): raise ValueError('TYPE_OF_VARIABLE_INDEXED_WITH_VARIABLE_TYPE must be' 'either "dictionary" or "list"') @@ -64,6 +79,9 @@ def __init__(self, self.BOOLEAN_CONDITIONS = BOOLEAN_CONDITIONS self.PACKAGE_NAME = PACKAGE_NAME self.TEMPLATE_DIR = TEMPLATE_DIR + self.ORDER_NUMBER = ORDER_NUMBER + self.ORDER_OBJECT = OrderNumber(number=1, enabled=self.ORDER_NUMBER, + sub_counter_enabled=ORDER_NUMBER_SUB_COUNTER) default_config = Config() diff --git a/jinja2schema/core.py b/jinja2schema/core.py index 844c0f2..240b431 100644 --- a/jinja2schema/core.py +++ b/jinja2schema/core.py @@ -76,6 +76,10 @@ def encode_common_attrs(self, var): rv = {} if var.label: rv['title'] = var.label + if var.value and var.used_with_default: + rv['default'] = var.value + if var.order_nr: + rv['order_number'] = var.order_nr return rv def encode(self, var): diff --git a/jinja2schema/mergers.py b/jinja2schema/mergers.py index 6f6ee07..9ab0f6e 100644 --- a/jinja2schema/mergers.py +++ b/jinja2schema/mergers.py @@ -71,6 +71,9 @@ def merge(fst, snd, custom_merger=None): result.used_with_default = fst.used_with_default and snd.used_with_default result.checked_as_defined = fst.checked_as_defined and snd.checked_as_defined result.checked_as_undefined = fst.checked_as_undefined and snd.checked_as_undefined + if fst.value == snd.value: + result.value = fst.value + result.order_nr = fst.order_nr if callable(custom_merger): result = custom_merger(fst, snd, result) return result diff --git a/jinja2schema/model.py b/jinja2schema/model.py index 4da979c..3abd5df 100644 --- a/jinja2schema/model.py +++ b/jinja2schema/model.py @@ -47,10 +47,15 @@ class Variable(object): Is true if the variable occurs within ``{% if %}`` block which condition checks if the variable is defined. + + .. attribute:: value + + Value of the variable in template. Set by default filter or assignment. """ def __init__(self, label=None, linenos=None, constant=False, may_be_defined=False, used_with_default=False, - checked_as_undefined=False, checked_as_defined=False): + checked_as_undefined=False, checked_as_defined=False, + value=None, order_nr=None): self.label = label self.linenos = linenos if linenos is not None else [] self.constant = constant @@ -58,6 +63,8 @@ def __init__(self, label=None, linenos=None, constant=False, self.used_with_default = used_with_default self.checked_as_undefined = checked_as_undefined self.checked_as_defined = checked_as_defined + self.value = value + self.order_nr = order_nr def clone(self): cls = type(self) @@ -68,6 +75,7 @@ def _get_kwargs_from_ast(cls, ast): return { 'linenos': [ast.lineno], 'label': ast.name if isinstance(ast, nodes.Name) else None, + 'value': ast.value if hasattr(ast, 'value') else None, } @classmethod @@ -77,7 +85,7 @@ def from_ast(cls, ast, **kwargs): :param ast: AST node :type ast: :class:`jinja2.nodes.Node` """ - for k, v in kwargs.items(): + for k, v in list(kwargs.items()): if v is None: del kwargs[k] kwargs = dict(cls._get_kwargs_from_ast(ast), **kwargs) @@ -97,7 +105,8 @@ def __eq__(self, other): self.used_with_default == other.used_with_default and self.checked_as_undefined == other.checked_as_undefined and self.checked_as_defined == other.checked_as_defined and - self.required == other.required + self.required == other.required and + self.value == other.value ) def __ne__(self, other): diff --git a/jinja2schema/order_number.py b/jinja2schema/order_number.py new file mode 100644 index 0000000..1a07fdf --- /dev/null +++ b/jinja2schema/order_number.py @@ -0,0 +1,42 @@ +# coding: utf-8 +""" +jinja2schema.order_number +~~~~~~~~~~~~~~~~~~ +""" +from contextlib import contextmanager + + +class OrderNumber(object): + """A base Order Number class. + + .. attribute:: number + + Initial counter value. + + .. attribute:: enabled + + Counter enabled or return None. + + """ + + def __init__(self, number=0, enabled=False, sub_counter_enabled=True): + self.start = number + self.number = self.start + self.order_enabled = enabled + self.sub_counter_enabled = sub_counter_enabled + + def get_next(self): + if self.order_enabled: + self.number += 1 + return self.number + return None + + @contextmanager + def sub_counter(self): + if self.sub_counter_enabled: + counter = self.number + self.number = self.start + yield + self.number = counter + return + yield diff --git a/jinja2schema/visitors/expr.py b/jinja2schema/visitors/expr.py index 17122f2..4acb804 100644 --- a/jinja2schema/visitors/expr.py +++ b/jinja2schema/visitors/expr.py @@ -137,7 +137,6 @@ def wrapped_func(ast, ctx, macroses=None, config=default_config): return wrapped_func return decorator - def visit_expr(ast, ctx, macroses=None, config=default_config): """Returns a structure of ``ast``. @@ -163,14 +162,16 @@ def _visit_dict(ast, ctx, macroses, items, config=default_config): :param items: a list of (key, value); key may be either AST node or string """ ctx.meet(Dictionary(), ast) - rtype = Dictionary.from_ast(ast, constant=True) + rtype = Dictionary.from_ast(ast, constant=True, order_nr=config.ORDER_OBJECT.get_next()) struct = Dictionary() for key, value in items: value_rtype, value_struct = visit_expr(value, Context( - predicted_struct=Unknown.from_ast(value)), macroses, config=config) + predicted_struct=Unknown.from_ast(value, order_nr=config.ORDER_OBJECT.get_next())), macroses, config=config) struct = merge(struct, value_struct) if isinstance(key, nodes.Node): - key_rtype, key_struct = visit_expr(key, Context(predicted_struct=Scalar.from_ast(key)), macroses, config=config) + key_rtype, key_struct = visit_expr(key, Context( + predicted_struct=Scalar.from_ast(key, order_nr=config.ORDER_OBJECT.get_next())), macroses, + config=config) struct = merge(struct, key_struct) if isinstance(key, nodes.Const): rtype[key.value] = value_rtype @@ -195,13 +196,14 @@ def visit_unary_expr(ast, ctx, macroses=None, config=default_config): @visits_expr(nodes.Compare) def visit_compare(ast, ctx, macroses=None, config=default_config): ctx.meet(Boolean(), ast) - rtype, struct = visit_expr(ast.expr, Context(predicted_struct=Unknown.from_ast(ast.expr)), - macroses, config=config) + rtype, struct = visit_expr(ast.expr, Context( + predicted_struct=Unknown.from_ast(ast.expr, order_nr=config.ORDER_OBJECT.get_next())), macroses, config=config) for op in ast.ops: - op_rtype, op_struct = visit_expr(op.expr, Context(predicted_struct=Unknown.from_ast(ast.expr)), - macroses, config=config) + op_rtype, op_struct = visit_expr(op.expr, Context( + predicted_struct=Unknown.from_ast(ast.expr, order_nr=config.ORDER_OBJECT.get_next())), macroses, + config=config) struct = merge(struct, op_struct) - return Boolean.from_ast(ast), struct + return Boolean.from_ast(ast, order_nr=config.ORDER_OBJECT.get_next()), struct @visits_expr(nodes.Slice) @@ -215,7 +217,9 @@ def visit_slice(ast, ctx, macroses=None, config=default_config): @visits_expr(nodes.Name) def visit_name(ast, ctx, macroses=None, config=default_config): - kwargs = {} + kwargs = { + 'order_nr': config.ORDER_OBJECT.get_next() + } return ctx.return_struct_cls.from_ast(ast, **kwargs), Dictionary({ ast.name: ctx.get_predicted_struct(label=ast.name) }) @@ -227,7 +231,7 @@ def visit_getattr(ast, ctx, macroses=None, config=default_config): ctx=ctx, predicted_struct=Dictionary.from_ast(ast, { ast.attr: ctx.get_predicted_struct(label=ast.attr), - })) + }, order_nr=config.ORDER_OBJECT.get_next())) return visit_expr(ast.node, context, macroses, config=config) @@ -237,31 +241,35 @@ def visit_getitem(ast, ctx, macroses=None, config=default_config): if isinstance(arg, nodes.Const): if isinstance(arg.value, int): if config.TYPE_OF_VARIABLE_INDEXED_WITH_INTEGER_TYPE == 'list': - predicted_struct = List.from_ast(ast, ctx.get_predicted_struct()) + predicted_struct = List.from_ast(ast, ctx.get_predicted_struct(), + order_nr=config.ORDER_OBJECT.get_next()) elif config.TYPE_OF_VARIABLE_INDEXED_WITH_INTEGER_TYPE == 'dictionary': predicted_struct = Dictionary.from_ast(ast, { arg.value: ctx.get_predicted_struct(), - }) + }, order_nr=config.ORDER_OBJECT.get_next()) elif config.TYPE_OF_VARIABLE_INDEXED_WITH_INTEGER_TYPE == 'tuple': items = [Unknown() for i in range(arg.value + 1)] items[arg.value] = ctx.get_predicted_struct() - predicted_struct = Tuple.from_ast(ast, tuple(items), may_be_extended=True) + predicted_struct = Tuple.from_ast(ast, tuple(items), may_be_extended=True, + order_nr=config.ORDER_OBJECT.get_next()) elif isinstance(arg.value, _compat.string_types): predicted_struct = Dictionary.from_ast(ast, { arg.value: ctx.get_predicted_struct(label=arg.value), - }) + }, order_nr=config.ORDER_OBJECT.get_next()) else: raise InvalidExpression(arg, '{0} is not supported as an index for a list or' ' a key for a dictionary'.format(arg.value)) elif isinstance(arg, nodes.Slice): - predicted_struct = List.from_ast(ast, ctx.get_predicted_struct()) + predicted_struct = List.from_ast(ast, ctx.get_predicted_struct(), order_nr=config.ORDER_OBJECT.get_next()) else: if config.TYPE_OF_VARIABLE_INDEXED_WITH_VARIABLE_TYPE == 'list': - predicted_struct = List.from_ast(ast, ctx.get_predicted_struct()) + predicted_struct = List.from_ast(ast, ctx.get_predicted_struct(), order_nr=config.ORDER_OBJECT.get_next()) elif config.TYPE_OF_VARIABLE_INDEXED_WITH_VARIABLE_TYPE == 'dictionary': - predicted_struct = Dictionary.from_ast(ast) + predicted_struct = Dictionary.from_ast(ast, order_nr=config.ORDER_OBJECT.get_next()) - _, arg_struct = visit_expr(arg, Context(predicted_struct=Scalar.from_ast(arg)), macroses, config=config) + _, arg_struct = visit_expr(arg, + Context(predicted_struct=Scalar.from_ast(arg, order_nr=config.ORDER_OBJECT.get_next())), + macroses, config=config) rtype, struct = visit_expr(ast.node, Context( ctx=ctx, predicted_struct=predicted_struct), macroses, config=config) @@ -273,10 +281,10 @@ def visit_test(ast, ctx, macroses=None, config=default_config): ctx.meet(Boolean(), ast) if ast.name in ('divisibleby', 'escaped', 'even', 'lower', 'odd', 'upper'): # TODO - predicted_struct = Scalar.from_ast(ast.node) + predicted_struct = Scalar.from_ast(ast.node, order_nr=config.ORDER_OBJECT.get_next()) elif ast.name in ('defined', 'undefined', 'equalto', 'iterable', 'mapping', 'none', 'number', 'sameas', 'sequence', 'string'): - predicted_struct = Unknown.from_ast(ast.node) + predicted_struct = Unknown.from_ast(ast.node, order_nr=config.ORDER_OBJECT.get_next()) if ast.name == 'defined': predicted_struct.checked_as_defined = True elif ast.name == 'undefined': @@ -289,7 +297,9 @@ def visit_test(ast, ctx, macroses=None, config=default_config): if not ast.args: raise InvalidExpression(ast, 'divisibleby must have an argument') _, arg_struct = visit_expr(ast.args[0], - Context(predicted_struct=Number.from_ast(ast.args[0])), macroses, config=config) + Context(predicted_struct=Number.from_ast(ast.args[0], + order_nr=config.ORDER_OBJECT.get_next())), + macroses, config=config) struct = merge(arg_struct, struct) return rtype, struct @@ -297,16 +307,18 @@ def visit_test(ast, ctx, macroses=None, config=default_config): @visits_expr(nodes.Concat) def visit_concat(ast, ctx, macroses=None, config=default_config): ctx.meet(Scalar(), ast) - return String.from_ast(ast), visit_many(ast.nodes, macroses, config, predicted_struct_cls=String) + return String.from_ast(ast, order_nr=config.ORDER_OBJECT.get_next()), visit_many(ast.nodes, macroses, config, + predicted_struct_cls=String) @visits_expr(nodes.CondExpr) def visit_cond_expr(ast, ctx, macroses=None, config=default_config): if config.BOOLEAN_CONDITIONS: - test_predicted_struct = Boolean.from_ast(ast.test) + test_predicted_struct = Boolean.from_ast(ast.test, order_nr=config.ORDER_OBJECT.get_next()) else: - test_predicted_struct = Unknown.from_ast(ast.test) - test_rtype, test_struct = visit_expr(ast.test, Context(predicted_struct=test_predicted_struct), macroses, config=config) + test_predicted_struct = Unknown.from_ast(ast.test, order_nr=config.ORDER_OBJECT.get_next()) + test_rtype, test_struct = visit_expr(ast.test, Context(predicted_struct=test_predicted_struct), macroses, + config=config) if_rtype, if_struct = visit_expr(ast.expr1, ctx, macroses, config=config) else_rtype, else_struct = visit_expr(ast.expr2, ctx, macroses, config=config) struct = merge_many(if_struct, test_struct, else_struct) @@ -358,7 +370,8 @@ def visit_call(ast, ctx, macroses=None, config=default_config): struct = Dictionary() for arg in ast.args: arg_rtype, arg_struct = visit_expr(arg, Context( - predicted_struct=Number.from_ast(arg)), macroses, config=config) + predicted_struct=Number.from_ast(arg, order_nr=config.ORDER_OBJECT.get_next())), macroses, + config=config) struct = merge(struct, arg_struct) return List(Number()), struct elif ast.node.name == 'lipsum': @@ -366,10 +379,14 @@ def visit_call(ast, ctx, macroses=None, config=default_config): struct = Dictionary() # probable TODO: set possible types for args and kwargs for arg in ast.args: - arg_rtype, arg_struct = visit_expr(arg, Context(predicted_struct=Scalar.from_ast(arg)), macroses, config=config) + arg_rtype, arg_struct = visit_expr(arg, Context( + predicted_struct=Scalar.from_ast(arg, order_nr=config.ORDER_OBJECT.get_next())), macroses, + config=config) struct = merge(struct, arg_struct) for kwarg in ast.kwargs: - arg_rtype, arg_struct = visit_expr(kwarg.value, Context(predicted_struct=Scalar.from_ast(kwarg)), macroses, config=config) + arg_rtype, arg_struct = visit_expr(kwarg.value, Context( + predicted_struct=Scalar.from_ast(kwarg, order_nr=config.ORDER_OBJECT.get_next())), macroses, + config=config) struct = merge(struct, arg_struct) return String(), struct elif ast.node.name == 'dict': @@ -380,23 +397,33 @@ def visit_call(ast, ctx, macroses=None, config=default_config): else: raise InvalidExpression(ast, '"{0}" call is not supported'.format(ast.node.name)) elif isinstance(ast.node, nodes.Getattr): - if ast.node.attr in ('keys', 'iterkeys', 'values', 'itervalues'): + if ast.node.node.name in ('loop'): + return Unknown(), Unknown() + elif ast.node.attr in ('keys', 'iterkeys', 'values', 'itervalues'): ctx.meet(List(Unknown()), ast) rtype, struct = visit_expr( - ast.node.node, Context(predicted_struct=Dictionary.from_ast(ast.node.node)), macroses, config=config) + ast.node.node, Context( + predicted_struct=Dictionary.from_ast(ast.node.node, order_nr=config.ORDER_OBJECT.get_next())), + macroses, config=config) return List(Unknown()), struct if ast.node.attr in ('startswith', 'endswith'): ctx.meet(Boolean(), ast) rtype, struct = visit_expr( - ast.node.node, Context(predicted_struct=String.from_ast(ast.node.node)), macroses, config=config) + ast.node.node, + Context(predicted_struct=String.from_ast(ast.node.node, order_nr=config.ORDER_OBJECT.get_next())), + macroses, config=config) return Boolean(), struct if ast.node.attr == 'split': ctx.meet(List(String()), ast) rtype, struct = visit_expr( - ast.node.node, Context(predicted_struct=String.from_ast(ast.node.node)), macroses, config=config) + ast.node.node, + Context(predicted_struct=String.from_ast(ast.node.node, order_nr=config.ORDER_OBJECT.get_next())), + macroses, config=config) if ast.args: arg = ast.args[0] - _, arg_struct = visit_expr(arg, Context(predicted_struct=String.from_ast(arg)), macroses, config=config) + _, arg_struct = visit_expr(arg, Context( + predicted_struct=String.from_ast(arg, order_nr=config.ORDER_OBJECT.get_next())), macroses, + config=config) struct = merge(struct, arg_struct) return List(String()), struct raise InvalidExpression(ast, '"{0}" call is not supported'.format(ast.node.attr)) @@ -411,27 +438,27 @@ def visit_filter(ast, ctx, macroses=None, config=default_config): 'urlencode', 'urlize', 'wordcount', 'wordwrap', 'e'): ctx.meet(Scalar(), ast) if ast.name in ('abs', 'round'): - node_struct = Number.from_ast(ast.node) + node_struct = Number.from_ast(ast.node, order_nr=config.ORDER_OBJECT.get_next()) return_struct_cls = Number elif ast.name in ('float', 'int'): - node_struct = Scalar.from_ast(ast.node) + node_struct = Scalar.from_ast(ast.node, order_nr=config.ORDER_OBJECT.get_next()) return_struct_cls = Number elif ast.name in ('striptags', 'capitalize', 'center', 'escape', 'forceescape', 'format', 'indent', 'replace', 'safe', 'title', 'trim', 'truncate', 'upper', 'urlencode', 'urlize', 'wordwrap', 'e'): - node_struct = String.from_ast(ast.node) + node_struct = String.from_ast(ast.node, order_nr=config.ORDER_OBJECT.get_next()) return_struct_cls = String elif ast.name == 'filesizeformat': - node_struct = Number.from_ast(ast.node) + node_struct = Number.from_ast(ast.node, order_nr=config.ORDER_OBJECT.get_next()) return_struct_cls = String elif ast.name == 'string': - node_struct = Scalar.from_ast(ast.node) + node_struct = Scalar.from_ast(ast.node, order_nr=config.ORDER_OBJECT.get_next()) return_struct_cls = String elif ast.name == 'wordcount': - node_struct = String.from_ast(ast.node) + node_struct = String.from_ast(ast.node, order_nr=config.ORDER_OBJECT.get_next()) return_struct_cls = Number else: - node_struct = Scalar.from_ast(ast.node) + node_struct = Scalar.from_ast(ast.node, order_nr=config.ORDER_OBJECT.get_next()) elif ast.name in ('batch', 'slice'): ctx.meet(List(List(Unknown())), ast) rtype = List(List(Unknown(), linenos=[ast.node.lineno]), linenos=[ast.node.lineno]) @@ -444,24 +471,29 @@ def visit_filter(ast, ctx, macroses=None, config=default_config): return rtype, struct elif ast.name == 'default': default_value_rtype, default_value_struct = visit_expr( - ast.args[0], Context(predicted_struct=Unknown.from_ast(ast.args[0])), macroses, config=config) + ast.args[0], + Context(predicted_struct=Unknown.from_ast(ast.args[0], order_nr=config.ORDER_OBJECT.get_next())), + macroses, config=config) node_struct = merge( - ctx.get_predicted_struct(), + ctx.get_predicted_struct(), default_value_rtype, ) node_struct.used_with_default = True + node_struct.value = default_value_rtype.value elif ast.name == 'dictsort': ctx.meet(List(Tuple([Scalar(), Unknown()])), ast) - node_struct = Dictionary.from_ast(ast.node) + node_struct = Dictionary.from_ast(ast.node, order_nr=config.ORDER_OBJECT.get_next()) elif ast.name == 'join': ctx.meet(Scalar(), ast) - node_struct = List.from_ast(ast.node, String()) + node_struct = List.from_ast(ast.node, String(), order_nr=config.ORDER_OBJECT.get_next()) rtype, struct = visit_expr(ast.node, Context( return_struct_cls=String, predicted_struct=node_struct ), macroses, config=config) arg_rtype, arg_struct = visit_expr(ast.args[0], - Context(predicted_struct=String.from_ast(ast.args[0])), macroses, config=config) + Context(predicted_struct=String.from_ast(ast.args[0], + order_nr=config.ORDER_OBJECT.get_next())), + macroses, config=config) return rtype, merge(struct, arg_struct) elif ast.name in ('first', 'last', 'random', 'length', 'sum'): if ast.name in ('first', 'last', 'random'): @@ -473,7 +505,7 @@ def visit_filter(ast, ctx, macroses=None, config=default_config): else: ctx.meet(Scalar(), ast) el_struct = Scalar() - node_struct = List.from_ast(ast.node, el_struct) + node_struct = List.from_ast(ast.node, el_struct, order_nr=config.ORDER_OBJECT.get_next()) elif ast.name in ('groupby', 'map', 'reject', 'rejectattr', 'select', 'selectattr', 'sort'): ctx.meet(List(Unknown()), ast) node_struct = merge( @@ -483,7 +515,7 @@ def visit_filter(ast, ctx, macroses=None, config=default_config): elif ast.name == 'list': ctx.meet(List(Scalar()), ast) node_struct = merge( - List(Scalar.from_ast(ast.node)), + List(Scalar.from_ast(ast.node, order_nr=config.ORDER_OBJECT.get_next())), ctx.get_predicted_struct() ).item elif ast.name == 'pprint': @@ -491,7 +523,7 @@ def visit_filter(ast, ctx, macroses=None, config=default_config): node_struct = ctx.get_predicted_struct() elif ast.name == 'xmlattr': ctx.meet(Scalar(), ast) - node_struct = Dictionary.from_ast(ast.node) + node_struct = Dictionary.from_ast(ast.node, order_nr=config.ORDER_OBJECT.get_next()) elif ast.name == 'attr': raise InvalidExpression(ast, 'attr filter is not supported') else: @@ -515,13 +547,13 @@ def visit_template_data(ast, ctx, macroses=None, config=default_config): def visit_const(ast, ctx, macroses=None, config=default_config): ctx.meet(Scalar(), ast) if isinstance(ast.value, _compat.string_types): - rtype = String.from_ast(ast, constant=True) + rtype = String.from_ast(ast, constant=True, order_nr=config.ORDER_OBJECT.get_next()) elif isinstance(ast.value, bool): - rtype = Boolean.from_ast(ast, constant=True) + rtype = Boolean.from_ast(ast, constant=True, order_nr=config.ORDER_OBJECT.get_next()) elif isinstance(ast.value, (int, float, complex)): - rtype = Number.from_ast(ast, constant=True) + rtype = Number.from_ast(ast, constant=True, order_nr=config.ORDER_OBJECT.get_next()) else: - rtype = Scalar.from_ast(ast, constant=True) + rtype = Scalar.from_ast(ast, constant=True, order_nr=config.ORDER_OBJECT.get_next()) return rtype, Dictionary() @@ -535,7 +567,7 @@ def visit_tuple(ast, ctx, macroses=None, config=default_config): item_rtype, item_struct = visit_expr(item, ctx, macroses, config=config) item_structs.append(item_rtype) struct = merge(struct, item_struct) - rtype = Tuple.from_ast(ast, item_structs, constant=True) + rtype = Tuple.from_ast(ast, item_structs, constant=True, order_nr=config.ORDER_OBJECT.get_next()) return rtype, struct @@ -553,7 +585,7 @@ def visit_list(ast, ctx, macroses=None, config=default_config): el_rtype = item_rtype else: el_rtype = merge_rtypes(el_rtype, item_rtype) - rtype = List.from_ast(ast, el_rtype or Unknown(), constant=True) + rtype = List.from_ast(ast, el_rtype or Unknown(), constant=True, order_nr=config.ORDER_OBJECT.get_next()) return rtype, struct diff --git a/jinja2schema/visitors/stmt.py b/jinja2schema/visitors/stmt.py index 34ca34e..e4dcaa3 100644 --- a/jinja2schema/visitors/stmt.py +++ b/jinja2schema/visitors/stmt.py @@ -31,14 +31,14 @@ def visits_stmt(node_cls): def decorator(func): stmt_visitors[node_cls] = func @functools.wraps(func) - def wrapped_func(ast, macroses=None, config=default_config): + def wrapped_func(ast, macroses=None, config=default_config, child_blocks=None): assert isinstance(ast, node_cls) - return func(ast, macroses, config) + return func(ast, macroses, config, child_blocks) return wrapped_func return decorator -def visit_stmt(ast, macroses=None, config=default_config): +def visit_stmt(ast, macroses=None, config=default_config, child_blocks=None): """Returns a structure of ``ast``. :param ast: instance of :class:`jinja2.nodes.Stmt` @@ -55,9 +55,11 @@ def visit_stmt(ast, macroses=None, config=default_config): @visits_stmt(nodes.For) -def visit_for(ast, macroses=None, config=default_config): - body_struct = visit_many(ast.body, macroses, config, predicted_struct_cls=Scalar) - else_struct = visit_many(ast.else_, macroses, config, predicted_struct_cls=Scalar) +def visit_for(ast, macroses=None, config=default_config, child_blocks=None): + with config.ORDER_OBJECT.sub_counter(): + body_struct = visit_many(ast.body, macroses, config, predicted_struct_cls=Scalar) + with config.ORDER_OBJECT.sub_counter(): + else_struct = visit_many(ast.else_, macroses, config, predicted_struct_cls=Scalar) if 'loop' in body_struct: # exclude a special `loop` variable from the body structure @@ -66,16 +68,16 @@ def visit_for(ast, macroses=None, config=default_config): if isinstance(ast.target, nodes.Tuple): target_struct = Tuple.from_ast( ast.target, - [body_struct.pop(item.name, Unknown.from_ast(ast.target)) - for item in ast.target.items]) + [body_struct.pop(item.name, Unknown.from_ast(ast.target, order_nr=config.ORDER_OBJECT.get_next())) + for item in ast.target.items], order_nr=config.ORDER_OBJECT.get_next()) else: - target_struct = body_struct.pop(ast.target.name, Unknown.from_ast(ast)) + target_struct = body_struct.pop(ast.target.name, Unknown.from_ast(ast, order_nr=config.ORDER_OBJECT.get_next())) iter_rtype, iter_struct = visit_expr( ast.iter, Context( return_struct_cls=Unknown, - predicted_struct=List.from_ast(ast, target_struct)), + predicted_struct=List.from_ast(ast, target_struct, order_nr=config.ORDER_OBJECT.get_next())), macroses, config) merge(iter_rtype, List(target_struct)) @@ -84,13 +86,13 @@ def visit_for(ast, macroses=None, config=default_config): @visits_stmt(nodes.If) -def visit_if(ast, macroses=None, config=default_config): +def visit_if(ast, macroses=None, config=default_config, child_blocks=None): if config.BOOLEAN_CONDITIONS: - test_predicted_struct = Boolean.from_ast(ast.test) + test_predicted_struct = Boolean.from_ast(ast.test, order_nr=config.ORDER_OBJECT.get_next()) else: - test_predicted_struct = Unknown.from_ast(ast.test) + test_predicted_struct = Unknown.from_ast(ast.test, order_nr=config.ORDER_OBJECT.get_next()) test_rtype, test_struct = visit_expr( - ast.test, Context(predicted_struct=test_predicted_struct), macroses, config) + ast.test, Context(predicted_struct=test_predicted_struct), macroses, config) if_struct = visit_many(ast.body, macroses, config, predicted_struct_cls=Scalar) else_struct = visit_many(ast.else_, macroses, config, predicted_struct_cls=Scalar) if ast.else_ else Dictionary() struct = merge_many(test_struct, if_struct, else_struct) @@ -114,7 +116,7 @@ def visit_if(ast, macroses=None, config=default_config): @visits_stmt(nodes.Assign) -def visit_assign(ast, macroses=None, config=default_config): +def visit_assign(ast, macroses=None, config=default_config, child_blocks=None): struct = Dictionary() if (isinstance(ast.target, nodes.Name) or (isinstance(ast.target, nodes.Tuple) and isinstance(ast.node, nodes.Tuple))): @@ -128,7 +130,8 @@ def visit_assign(ast, macroses=None, config=default_config): for name_ast, var_ast in izip(ast.target.items, ast.node.items): variables.append((name_ast.name, var_ast)) for var_name, var_ast in variables: - var_rtype, var_struct = visit_expr(var_ast, Context(predicted_struct=Unknown.from_ast(var_ast)), macroses, config) + var_rtype, var_struct = visit_expr(var_ast, Context( + predicted_struct=Unknown.from_ast(var_ast, order_nr=config.ORDER_OBJECT.get_next())), macroses, config) var_rtype.constant = True var_rtype.label = var_name struct = merge_many(struct, var_struct, Dictionary({ @@ -138,7 +141,7 @@ def visit_assign(ast, macroses=None, config=default_config): elif isinstance(ast.target, nodes.Tuple): tuple_items = [] for name_ast in ast.target.items: - var_struct = Unknown.from_ast(name_ast, constant=True) + var_struct = Unknown.from_ast(name_ast, constant=True, order_nr=config.ORDER_OBJECT.get_next()) tuple_items.append(var_struct) struct = merge(struct, Dictionary({name_ast.name: var_struct})) var_rtype, var_struct = visit_expr( @@ -149,12 +152,12 @@ def visit_assign(ast, macroses=None, config=default_config): @visits_stmt(nodes.Output) -def visit_output(ast, macroses=None, config=default_config): +def visit_output(ast, macroses=None, config=default_config, child_blocks=None): return visit_many(ast.nodes, macroses, config, predicted_struct_cls=Scalar) @visits_stmt(nodes.Macro) -def visit_macro(ast, macroses=None, config=default_config): +def visit_macro(ast, macroses=None, config=default_config, child_blocks=None): # XXX the code needs to be refactored args = [] kwargs = [] @@ -190,20 +193,44 @@ def visit_macro(ast, macroses=None, config=default_config): return body_struct +@visits_stmt(nodes.Block) +def visit_block(ast, macroses=None, config=default_config): + return visit_many(ast.body, macroses, config) + + @visits_stmt(nodes.Include) -def visit_include(ast, macroses=None, config=default_config): - env = Environment(loader=PackageLoader(config.PACKAGE_NAME, config.TEMPLATE_DIR)) - template = env.parse(env.loader.get_source(env, ast.template.value)[0]) +def visit_include(ast, macroses=None, config=default_config, child_blocks=None): + template = get_inherited_template(config, ast) return visit_many(template.body, macroses, config) @visits_stmt(nodes.Extends) -def visit_extends(ast, macroses=None, config=default_config): +def visit_extends(ast, macroses=None, config=default_config, child_blocks=None): + template = get_inherited_template(config, ast) + if not child_blocks: + return visit_many(template.body, macroses, config) + return visit_many(get_correct_nodes(child_blocks, template.body), None, config) + + +def get_inherited_template(config, ast): env = Environment(loader=PackageLoader(config.PACKAGE_NAME, config.TEMPLATE_DIR)) - template = env.parse(env.loader.get_source(env, ast.template.value)[0]) - return visit_many(template.body, macroses, config) + return env.parse(env.loader.get_source(env, ast.template.value)[0]) -@visits_stmt(nodes.Block) -def visit_block(ast, macroses=None, config=default_config): - return visit_many(ast.body, macroses, config) +def separate_template_blocks(template, blocks, template_nodes): + for node in template: + if isinstance(node, nodes.Block): + blocks.append(node) + else: + template_nodes.append(node) + return blocks, template_nodes + + +def get_correct_nodes(child_blocks, template): + parent_blocks, nodes = separate_template_blocks(template, [], []) + child_block_names = [c.name for c in child_blocks] + blocks = child_blocks + parent_blocks + for parent_block in parent_blocks: + if parent_block.name in child_block_names: + blocks.remove(parent_block) + return blocks + nodes diff --git a/jinja2schema/visitors/util.py b/jinja2schema/visitors/util.py index f44ed55..2b999db 100644 --- a/jinja2schema/visitors/util.py +++ b/jinja2schema/visitors/util.py @@ -13,7 +13,8 @@ def visit(node, macroses, config, predicted_struct_cls=Scalar, return_struct_cls if isinstance(node, jinja2.nodes.Stmt): structure = visit_stmt(node, macroses, config) elif isinstance(node, jinja2.nodes.Expr): - ctx = Context(predicted_struct=predicted_struct_cls.from_ast(node), return_struct_cls=return_struct_cls) + ctx = Context(predicted_struct=predicted_struct_cls.from_ast(node, order_nr=config.ORDER_OBJECT.get_next()), + return_struct_cls=return_struct_cls) _, structure = visit_expr(node, ctx, macroses, config) elif isinstance(node, jinja2.nodes.Template): structure = visit_many(node.body, macroses, config) @@ -30,11 +31,14 @@ def visit_many(nodes, macroses, config, predicted_struct_cls=Scalar, return_stru """ rv = Dictionary() for node in nodes: - structure = visit(node, macroses, config, predicted_struct_cls=predicted_struct_cls, return_struct_cls=return_struct_cls) + if isinstance(node, jinja2.nodes.Extends): + structure = visit_extends(node, macroses, config, [x for x in nodes if isinstance(x, jinja2.nodes.Block)]) + else: + structure = visit(node, macroses, config, predicted_struct_cls, return_struct_cls) rv = merge(rv, structure) return rv # keep these at the end of file to avoid circular imports from .expr import Context, visit_expr -from .stmt import visit_stmt +from .stmt import visit_stmt, visit_extends diff --git a/setup.py b/setup.py index 9973460..c6a3c08 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ license=open('LICENSE').read(), author='Anton Romanovich', author_email='anthony.romanovich@gmail.com', - url='https://jinja2schema.readthedocs.org', + url='https://jinja2schema.readthedocs.io', packages=find_packages(exclude=['tests']), install_requires=['Jinja2>=2.2'], classifiers=[ diff --git a/tests/func_tests/test_basics.py b/tests/func_tests/test_basics.py index afad078..d35316d 100644 --- a/tests/func_tests/test_basics.py +++ b/tests/func_tests/test_basics.py @@ -232,7 +232,7 @@ def test_basics_11(): 'a': Dictionary({ 'attr1': List(String(), label='attr1', linenos=[3]), 'attr2': List(Scalar(linenos=[4]), label='attr2', linenos=[4], used_with_default=True), - 'attr3': String(label='attr3', linenos=[5], used_with_default=True) + 'attr3': String(label='attr3', linenos=[5], used_with_default=True, value='gsom') }, label='a', linenos=[2, 3, 4, 5]), 'xs': List( Scalar(label='x', linenos=[7]), # TODO it should be Dictionary({'is_active': Unknown()}) @@ -413,3 +413,101 @@ def test_boolean_conditions_setting_2(): }) assert struct == expected_struct +def test_block_1(): + config = Config() + + template = ''' + {% block test %} + {{ x }} + {{ y }} + {% endblock %} + ''' + struct = infer(template, config) + expected_struct = Dictionary({ + 'x': Scalar(label='x', linenos=[3]), + 'y': Scalar(label='y', linenos=[4]), + }) + assert struct == expected_struct + + +def test_order_number_setting_1(): + config = Config(ORDER_NUMBER=True) + + template = ''' + {{ x }} + {{ y }} + {{ z }} + {{ x }} + {{ x }} + ''' + struct = infer(template, config) + assert struct['x'].order_nr < struct['y'].order_nr + assert struct['y'].order_nr < struct['z'].order_nr + + +def test_order_number_setting_1_5(): + config = Config(ORDER_NUMBER=True) + + template = ''' + {% if yy %} + {{ intooo }} + {{ zz }} + {% endif %} + ''' + struct = infer(template, config) + assert struct['yy'].order_nr < struct['zz'].order_nr + + +def test_order_number_setting_2(): + config = Config(ORDER_NUMBER=True) + + template = ''' + {% for n in nx %} + {{ y }} + {{ z }} + {% endfor %} + {% if yy %} + {{ zz }} + {{ xx }} + {% endif %} + ''' + struct = infer(template, config) + assert struct['y'].order_nr < struct['z'].order_nr + assert struct['nx'].order_nr < struct['yy'].order_nr + assert struct['zz'].order_nr < struct['xx'].order_nr + + +def test_order_number_setting_3(): + config = Config(ORDER_NUMBER=True) + + template = ''' + {% for a in aa %} + {{ ax }} + {% for b in bb %} + {{ bx }} + {% for c in cc %} + {{ cx }} + {% endfor %} + {% endfor %} + {% endfor %} + ''' + struct = infer(template, config) + assert struct['ax'].order_nr == struct['bx'].order_nr == struct['cx'].order_nr + + +def test_order_number_setting_4(): + config = Config(ORDER_NUMBER=True, ORDER_NUMBER_SUB_COUNTER=False) + + template = ''' + {% for a in aa %} + {{ ax }} + {% for b in bb %} + {{ bx }} + {% for c in cc %} + {{ cx }} + {% endfor %} + {% endfor %} + {% endfor %} + ''' + struct = infer(template, config) + assert struct['ax'].order_nr != struct['bx'].order_nr != struct['cx'].order_nr diff --git a/tests/func_tests/test_inheritance.py b/tests/func_tests/test_inheritance.py new file mode 100644 index 0000000..cb5c51c --- /dev/null +++ b/tests/func_tests/test_inheritance.py @@ -0,0 +1,115 @@ +# coding: utf-8 +import pytest +from jinja2 import PackageLoader, Environment + +from jinja2schema.config import Config +from jinja2schema.core import infer +from jinja2schema.model import Dictionary, Scalar + + +@pytest.fixture +def env(): + loader = PackageLoader('tests', 'templates') + return Environment(loader=loader) + + +@pytest.fixture +def config(): + return Config(PACKAGE_NAME='tests') + + +def test_include_1(env, config): + struct = infer(env.loader.get_source(env, 'inner_include_1.html')[0], config) + expected_struct = Dictionary({ + 'var': Dictionary( + { + 'x': Scalar(label='x', linenos=[1]), + 'y': Scalar(label='y', linenos=[1]), + }, + label='var', + linenos=[1] + ), + 'more': Scalar(label='more', linenos=[2]), + }) + assert struct == expected_struct + + +def test_include_override_1(env, config): + struct = infer(env.loader.get_source(env, 'inner_include_override_1.html')[0], config) + expected_struct = Dictionary({ + 'default_name': Scalar(label='default_name', linenos=[2]), + 'name': Scalar(label='name', linenos=[3]), + }) + assert struct == expected_struct + + +def test_extend_1(env, config): + struct = infer(env.loader.get_source(env, 'inner_extend.html')[0], config) + expected_struct = Dictionary({ + 'var': Dictionary( + {'a': Scalar(label='a', linenos=[1])}, + label='var', + linenos=[1] + ), + 'some': Scalar(label='some', linenos=[2]), + 'extended': Scalar(label='extended', linenos=[2]), + }) + assert struct == expected_struct + + +def test_include_extend_1(env, config): + struct = infer(env.loader.get_source(env, 'include_extend.html')[0], config) + expected_struct = Dictionary({ + 'var': Dictionary( + { + 'x': Scalar(label='x', linenos=[1]), + 'y': Scalar(label='y', linenos=[1]), + 'a': Scalar(label='a', linenos=[1]), + }, + label='var', + linenos=[1] + ), + 'also': Scalar(label='also', linenos=[3]), + 'extended': Scalar(label='extended', linenos=[2]), + }) + assert struct == expected_struct + + +def test_extend_with_block_override_1(env, config): + struct = infer(env.loader.get_source(env, 'inner_extend_override_1.html')[0], config) + expected_struct = Dictionary({ + 'name': Scalar(label='name', linenos=[3]), + }) + assert struct == expected_struct + + +def test_extend_with_block_override_2(env, config): + struct = infer(env.loader.get_source(env, 'inner_extend_override_2.html')[0], config) + expected_struct = Dictionary({ + 'name': Scalar(label='name', linenos=[3]), + 'location': Scalar(label='location', linenos=[6]), + 'default_mood': Scalar(label='default_mood', linenos=[8]), + }) + assert struct == expected_struct + + +def test_extend_with_block_override_3(env, config): + struct = infer(env.loader.get_source(env, 'inner_extend_override_3.html')[0], config) + expected_struct = Dictionary({ + 'location': Scalar(label='location', linenos=[6]), + 'mood': Scalar(label='mood', linenos=[9]), + 'name': Scalar(label='name', linenos=[3]), + }) + assert struct == expected_struct + + +def test_extend_with_block_override_4(env, config): + struct = infer(env.loader.get_source(env, 'inner_extend_override_4.html')[0], config) + expected_struct = Dictionary({ + 'noblock': Scalar(label='noblock', linenos=[1]), + 'brake': Scalar(label='brake', linenos=[3]), + 'location': Scalar(label='location', linenos=[6]), + 'mood': Scalar(label='mood', linenos=[9]), + 'name': Scalar(label='name', linenos=[3]), + }) + assert struct == expected_struct diff --git a/tests/templates/extend.html b/tests/templates/extend.html new file mode 100644 index 0000000..4d46470 --- /dev/null +++ b/tests/templates/extend.html @@ -0,0 +1,2 @@ +{{ var.a }} +{{ extended }} diff --git a/tests/templates/extend_override_1.html b/tests/templates/extend_override_1.html new file mode 100644 index 0000000..3bc4614 --- /dev/null +++ b/tests/templates/extend_override_1.html @@ -0,0 +1,3 @@ +{% block head %} +
I am the default, my name is {{ default_name }}
+{% endblock %} diff --git a/tests/templates/extend_override_2.html b/tests/templates/extend_override_2.html new file mode 100644 index 0000000..75723aa --- /dev/null +++ b/tests/templates/extend_override_2.html @@ -0,0 +1,9 @@ +{% block head %} +Kylo Ren is {{ default_name }}
+{% endblock %} +{% block body %} +He lives at {{ default_location }}
+{% endblock %} +{% block footer %} +He is {{ default_mood }}
+{% endblock %} diff --git a/tests/templates/extend_override_3.html b/tests/templates/extend_override_3.html new file mode 100644 index 0000000..c190c53 --- /dev/null +++ b/tests/templates/extend_override_3.html @@ -0,0 +1,6 @@ +{% block head %} +Kylo Ren is {{ default_name }}
+{% endblock %} +{% block body %} +He lives at {{ default_location }}
+{% endblock %} diff --git a/tests/templates/extend_override_4.html b/tests/templates/extend_override_4.html new file mode 100644 index 0000000..65b79bb --- /dev/null +++ b/tests/templates/extend_override_4.html @@ -0,0 +1,10 @@ +{{ noblock }} +{% block brake %} +Inners name is {{ brake }}
+{% endblock %} +{% block head %} +Kylo Ren is {{ default_name }}
+{% endblock %} +{% block body %} +He lives at {{ default_location }}
+{% endblock %} diff --git a/tests/templates/include_1.html b/tests/templates/include_1.html new file mode 100644 index 0000000..6abc693 --- /dev/null +++ b/tests/templates/include_1.html @@ -0,0 +1 @@ +{{ var.y }} {{ var.x }} diff --git a/tests/templates/include_2.html b/tests/templates/include_2.html new file mode 100644 index 0000000..0fe0d44 --- /dev/null +++ b/tests/templates/include_2.html @@ -0,0 +1,4 @@ +{{ parent }} +{% block head %} + {{ also_parent }} +{% endblock %} diff --git a/tests/templates/include_extend.html b/tests/templates/include_extend.html new file mode 100644 index 0000000..30cf2b9 --- /dev/null +++ b/tests/templates/include_extend.html @@ -0,0 +1,3 @@ +{% extends "extend.html" %} +{% include "include_1.html" %} +{{ also }} diff --git a/tests/templates/include_override_1.html b/tests/templates/include_override_1.html new file mode 100644 index 0000000..3bc4614 --- /dev/null +++ b/tests/templates/include_override_1.html @@ -0,0 +1,3 @@ +{% block head %} +I am the default, my name is {{ default_name }}
+{% endblock %} diff --git a/tests/templates/inner_extend.html b/tests/templates/inner_extend.html new file mode 100644 index 0000000..b1b9b2e --- /dev/null +++ b/tests/templates/inner_extend.html @@ -0,0 +1,2 @@ +{% extends "extend.html" %} +{{ some }} diff --git a/tests/templates/inner_extend_override_1.html b/tests/templates/inner_extend_override_1.html new file mode 100644 index 0000000..27874bb --- /dev/null +++ b/tests/templates/inner_extend_override_1.html @@ -0,0 +1,4 @@ +{% extends "extend_override_1.html" %} +{% block head %} +Hello, {{ name }}
+{% endblock %} diff --git a/tests/templates/inner_extend_override_2.html b/tests/templates/inner_extend_override_2.html new file mode 100644 index 0000000..ea8b49c --- /dev/null +++ b/tests/templates/inner_extend_override_2.html @@ -0,0 +1,7 @@ +{% extends "extend_override_2.html" %} +{% block head %} +Inners name is {{ name }}
+{% endblock %} +{% block body %} +Inner lives at {{ location }}
+{% endblock %} diff --git a/tests/templates/inner_extend_override_3.html b/tests/templates/inner_extend_override_3.html new file mode 100644 index 0000000..1f9357d --- /dev/null +++ b/tests/templates/inner_extend_override_3.html @@ -0,0 +1,10 @@ +{% extends "extend_override_3.html" %} +{% block head %} +Inners name is {{ name }}
+{% endblock %} +{% block body %} +Inner lives at {{ location }}
+{% endblock %} +{% block footer %} +Inner is {{ mood }}
+{% endblock %} diff --git a/tests/templates/inner_extend_override_4.html b/tests/templates/inner_extend_override_4.html new file mode 100644 index 0000000..af29299 --- /dev/null +++ b/tests/templates/inner_extend_override_4.html @@ -0,0 +1,10 @@ +{% extends "extend_override_4.html" %} +{% block head %} +Inners name is {{ name }}
+{% endblock %} +{% block body %} +Inner lives at {{ location }}
+{% endblock %} +{% block footer %} +Inner is {{ mood }}
+{% endblock %} diff --git a/tests/templates/inner_include_1.html b/tests/templates/inner_include_1.html new file mode 100644 index 0000000..d6d7a5b --- /dev/null +++ b/tests/templates/inner_include_1.html @@ -0,0 +1,2 @@ +{% include "include_1.html" %} +{{ more }} diff --git a/tests/templates/inner_include_2.html b/tests/templates/inner_include_2.html new file mode 100644 index 0000000..6718d38 --- /dev/null +++ b/tests/templates/inner_include_2.html @@ -0,0 +1,4 @@ +{% include "include_2.html" %} +{% block head %} + {{ child }} +{% endblock %} diff --git a/tests/templates/inner_include_override_1.html b/tests/templates/inner_include_override_1.html new file mode 100644 index 0000000..9a60de2 --- /dev/null +++ b/tests/templates/inner_include_override_1.html @@ -0,0 +1,4 @@ +{% include "include_override_1.html" %} +{% block head %} +Hello, {{ name }}
+{% endblock %} diff --git a/tests/unit_tests/test_call_visitor.py b/tests/unit_tests/test_call_visitor.py index 0322018..6803f7f 100644 --- a/tests/unit_tests/test_call_visitor.py +++ b/tests/unit_tests/test_call_visitor.py @@ -50,8 +50,8 @@ def test_dict_call(): expected_rtype = Dictionary({ 'x': Dictionary({ - 'a': Number(linenos=[3], constant=True), - 'b': Number(linenos=[3], constant=True) + 'a': Number(linenos=[3], constant=True, value=1), + 'b': Number(linenos=[3], constant=True, value=2) }, linenos=[2], constant=True), 'y': Unknown(label='a', linenos=[3]), }, linenos=[1], constant=True) diff --git a/tests/unit_tests/test_expr_visitors.py b/tests/unit_tests/test_expr_visitors.py index 22a8ff3..204fa68 100644 --- a/tests/unit_tests/test_expr_visitors.py +++ b/tests/unit_tests/test_expr_visitors.py @@ -186,7 +186,7 @@ def test_slice(): def test_test_1(): - template = '''{{ x is divisibleby data.field }}''' + template = '''{{ x is divisibleby (data.field) }}''' ast = parse(template).find(nodes.Test) rtype, struct = visit_test(ast, get_scalar_context(ast)) @@ -238,4 +238,4 @@ def test_const(): template = '''{{ false }}''' const_ast = parse(template).find(nodes.Const) rtype, struct = visit_const(const_ast, get_scalar_context(const_ast)) - assert rtype == Boolean(constant=True, linenos=[1]) + assert rtype == Boolean(constant=True, linenos=[1], value=False) diff --git a/tests/unit_tests/test_filter_visitor.py b/tests/unit_tests/test_filter_visitor.py index cd88871..1758ef4 100644 --- a/tests/unit_tests/test_filter_visitor.py +++ b/tests/unit_tests/test_filter_visitor.py @@ -56,7 +56,7 @@ def test_default_filter(): rtype, struct = visit_filter(ast, get_scalar_context(ast)) expected_struct = Dictionary({ - 'x': String(label='x', linenos=[1], used_with_default=True), + 'x': String(label='x', linenos=[1], used_with_default=True, value='g'), }) assert struct == expected_struct @@ -135,7 +135,7 @@ def test_join_filter(): assert rtype == String(label='xs', linenos=[1]) assert struct == Dictionary({ 'xs': List(String(), label='xs', linenos=[1]), - 'separator': String(label='separator', linenos=[1], used_with_default=True), + 'separator': String(label='separator', linenos=[1], used_with_default=True, value='|'), }) diff --git a/tests/unit_tests/test_order_number.py b/tests/unit_tests/test_order_number.py new file mode 100644 index 0000000..d32243e --- /dev/null +++ b/tests/unit_tests/test_order_number.py @@ -0,0 +1,29 @@ +# coding: utf-8 +from jinja2schema.order_number import OrderNumber + + +def test_get_next_order_number(): + on = OrderNumber(1, enabled=True) + assert on.get_next() == 2 + on = OrderNumber(1) + assert on.get_next() is None + on = OrderNumber(5, enabled=True) + assert on.get_next() == 6 + + +def test_sub_counter_1(): + on = OrderNumber(1, enabled=True) + on.get_next() + assert on.get_next() == 3 + with on.sub_counter(): + assert on.get_next() == 2 + assert on.get_next() == 4 + + +def test_sub_counter_2(): + on = OrderNumber(12, enabled=True) + on.get_next() + assert on.get_next() == 14 + with on.sub_counter(): + assert on.get_next() == 13 + assert on.get_next() == 15 diff --git a/tests/unit_tests/test_stmt_visitors.py b/tests/unit_tests/test_stmt_visitors.py index b352b5f..92e57db 100644 --- a/tests/unit_tests/test_stmt_visitors.py +++ b/tests/unit_tests/test_stmt_visitors.py @@ -101,9 +101,9 @@ def test_assign_4(): struct = visit_assign(ast) expected_struct = Dictionary({ - 'a': Number(label='a', linenos=[1], constant=True), + 'a': Number(label='a', linenos=[1], constant=True, value=1), 'b': Dictionary(data={ - 'gsom': String(linenos=[1], constant=True), + 'gsom': String(linenos=[1], constant=True, value='gsom'), }, label='b', linenos=[1], constant=True), 'z': Scalar(label='z', linenos=[1]), })