jinja2schema

Release v0.1.4.

Introduction

jinja2schema is a library for inferring types from Jinja2 templates.

One of the possible usages of jinja2schema is to create a JSON schema of a context expected by the template and then use it to render an HTML form (using such JS libraries as angular-schema-form, Alpaca or JSON Editor) or to validate a user input.

The library is in an early stage of development. Although the code is extensively tested, please be prepared for bugs or inconsistencies and if you find some, let the author know by opening a ticket.

Examples

Let’s start with inferring types from some expressions:

>>> from jinja2schema import infer
>>> s = infer('{{ x }}')
>>> s
{'x': <scalar>}
>>> type(s)
<class 'jinja2schema.model.Dictionary'>
>>> type(s['x'])
<class 'jinja2schema.model.Scalar'>
>>> infer('{{ x.a.b }}')
{'x': {'a': {'b': <scalar>}}}
>>> s = infer('{{ xs|first }}')
>>> s
{'xs': [<scalar>]}
>>> type(s['xs'])
<class 'jinja2schema.model.List'>
>>> infer('{{ (xs|first).name }}')
{'xs': [{'name': <scalar>}]}

jinja2schema supports all Jinja2 control structures:

>>> infer('''
... {% for row in items|batch(3, '&nbsp;') %}
...     {% for column in row %}
...         {% if column.has_title %}
...             {{ column.title }}
...         {% else %}
...             {{ column.desc|truncate(10) }}
...         {% endif %}
...     {% endfor %}
... {% endfor %}
... ''')
{
    'items': [{
        'desc': <scalar>,
        'has_title': <unknown>,
        'title': <scalar>
    }]
}

It works correctly with nested scopes:

>>> s = infer('''
... {% for x in xs %}
...     {% for x in ys %}
...         {{ x.c }}
...     {% endfor %}
...     {{ x.a }}
... {% endfor %}
... {% for a in xs %}
...     {{ a.b }}
... {% endfor %}
... ''')
>>> s
{
    'xs': [{'a': <scalar>, 'b': <scalar>}],
    'ys': [{'c': <scalar>}]
}

jinja2schema supports macroses:

>>> s = infer('''
... {% macro user(login, name) %}
...   {{ login }} {{ name.first }} {{ name.last }}
... {% endmacro %}
... {% for user in users %}
...   {{ user(user.login, user.name) }}
... {% endfor %}
... ''')
>>> s
{
    'users': [{
        'login': <scalar>
        'name': {'first': <scalar>, 'last': <scalar>}
    }]
}

A result of jinja2schema.infer() can be converted to JSON schema using jinja2schema.to_json_schema().

>>> schema = to_json_schema(infer('{% for x in xs %}{{ x }}{% endfor %}'))
>>> print json.dumps(schema, indent=2)
{
  "type": "object",
  "properties": {
    "xs": {
      "type": "array",
      "title": "xs",
      "items": {
        "title": "x",
        "anyOf": [
          {"type": "string"},
          {"type": "number"},
          {"type": "boolean"},
          {"type": "null"}
        ]
      }
    }
  },
  "required": ["xs"]
}

A more detailed representation of the structure can be obtained using jinja2schema.util.debug_repr().

>>> from jinja2schema.util import debug_repr
>>> print debug_repr(s)
Dictionary(label=None, required=True, constant=False, linenos=[], {
    xs: List(label=xs, required=True, constant=False, linenos=[2],
            Dictionary(label=x, required=True, constant=False, linenos=[6], {
                b: Scalar(label=b, required=True, constant=False, linenos=[6])
            })
        )
    ys: List(label=ys, required=True, constant=False, linenos=[3],
            Dictionary(label=x, required=True, constant=False, linenos=[4], {
                a: Scalar(label=a, required=True, constant=False, linenos=[4])
            })
        )
})

How It Works

jinja2schema algorithm based on the following common sense assumptions.

Note

This list is not exhausting and is a subject to change. Some of these “axioms” probably will be customizable at some point in the future.

  • If x is printed ({{ x }}), x is a scalar: a string, a number or a boolean;

  • If x is used as an iterable in for loop ({% for item in x %}) or used with a list filter (i.e., x|first), x is a list. If x is being indexed with an integer (x[0]) x is a list, dictionary or tuple (that behaviour can be adjusted using jinja2schema.config.Config.TYPE_OF_VARIABLE_INDEXED_WITH_INTEGER_TYPE);

  • If x is used with a dot (x.field) or being indexed with a string (x['field']), x is a dictionary.

  • A variable can only be used in the one role. So that a list or dictionary can not be printed, a string can not be indexed:

    >>> infer('''
    ... {{ x }}
    ... {{ x.name }}
    ... ''')
    jinja2schema.exceptions.MergeException: variable "x" (lines: 2, used as scalar)
    conflicts with variable "x" (lines: 3, used as dictionary)
    
  • Lists are assumed to be homogeneous, meaning all elements of the same list are assumed to have the same structure:

    >>> infer('''
    ... {% set xs = [
    ...    1,
    ...    {}
    ... ] %}
    ... ''')
    jinja2schema.exceptions.MergeException: unnamed variable (lines: 3, used as scalar)
    conflicts with unnamed variable (lines: 4, used as dictionary)
    

Installation

$ pip install jinja2schema

API

To infer types from a template, simply call jinja2schema.infer().

jinja2schema.infer(template, config=<jinja2schema.config.Config object>)[source]

Returns a model.Dictionary which reflects a structure of the context required by template.

Parameters:
  • template (string) – a template
  • config (config.Config) – a config
Return type:

model.Dictionary

Raises:

exceptions.MergeException, exceptions.InvalidExpression, exceptions.UnexpectedExpression

It’s logic can be tuned by specifying a custom jinja2schema.config.Config.


A models.Dictionary returned by infer can be converted to JSON schema using jinja2schema.to_json_schema() method.

jinja2schema.to_json_schema(var, jsonschema_encoder=<class 'jinja2schema.core.JSONSchemaDraft4Encoder'>)[source]

Returns JSON schema that describes var.

Parameters:
  • var (model.Variable) – a variable
  • jsonschema_encoder (subclass of JSONSchemaEncoder) – JSON schema encoder
Returns:

dict

Standard JSON schema encoders are:

class jinja2schema.JSONSchemaDraft4Encoder[source]

Extensible JSON schema encoder for model.Variable.

class jinja2schema.StringJSONSchemaDraft4Encoder[source]

Encodes model.Unknown and model.Scalar (but not it’s subclasses – model.String, model.Number or model.Boolean) variables as strings.

Useful for rendering forms using resulting JSON schema, as most of form-rendering tools do not support “anyOf” validator.


If you need more than that, please take a look at Internals.

Contributing

The project is hosted on GitHub. Please feel free to send a pull request or open an issue.

Running the Tests

$ pip install -r ./requirements-dev.txt
$ ./test.sh