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, ' ') %}
... {% 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. Ifx
is being indexed with an integer (x[0]
)x
is a list, dictionary or tuple (that behaviour can be adjusted usingjinja2schema.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 bytemplate
.Parameters: - template (string) – a template
- config (
config.Config
) – a config
Return type: 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
- var (
Standard JSON schema encoders are:
-
class
jinja2schema.
JSONSchemaDraft4Encoder
[source]¶ Extensible JSON schema encoder for
model.Variable
.
-
class
jinja2schema.
StringJSONSchemaDraft4Encoder
[source]¶ Encodes
model.Unknown
andmodel.Scalar
(but not it’s subclasses –model.String
,model.Number
ormodel.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.