Migration guide
When compared to Python Liquid and Shopify/Liquid, Liquid2 adds features, subtly changes the syntax of Liquid templates and changes the template engine's Python API. This is not "Python Liquid version 2", but a Python implementation of "Liquid2", which is mostly backwards compatible with other implementations.
Approach to compatibility and stability
With Python Liquid, our primary objectives were render behavior stability and Shopify/Liquid compatibility, in that order. Later we introduced liquid.future.Environment
, which sacrificed some stability for greater Shopify/Liquid compatibility as Shopify/Liquid and our understanding of it changed.
Now, with Python Liquid2, render behavior stability is still the top priority, but the default environment deliberately deviates from Shopify/Liquid in several ways, "fixing" and adding often requested features that Shopify can't due to their large user base and the technical debt that comes with it.
In most cases these fixes and features are backwards compatible with Shopify/Liquid, requiring little or no modification to legacy Liquid templates. To ease transition from legacy templates to Liquid2 templates, we include a liquid2.shopify.Environment
, which is configured to include some legacy tags that didn't make it in to the default environment.
Why is render stability so important?
When developing a conventional website, for example, templates are developed along side application code. Template authors and application developers might be different people or different teams, but templates are known at deployment time, and all templates can probably be parsed upfront and held in memory. In this scenario it's a pain if your template render engine introduces behavioral changes, but it's manageable.
Python Liquid2 caters for situations where templates change and grow with an application's user base. Not only can templates change after the application is deployed, but the number of templates could be huge, far more than can be expected to fit in memory all at once.
Behavioral stability is essential when application users are responsible for maintaining templates. It is impractical or unreasonable to expect authors to update their templates on demand.
Whether shopify/Liquid compatibility is important to you or not, if you're developing a multi-tenant application where users are responsible for maintaining templates, you should seriously consider building in an opt-in upgrade path for template authors to transition to updated syntax and features.
New features
More whitespace control
Along with a default_trim
configuration option, tags and the output statement now support +
, -
and ~
for controlling whitespace in templates. By default, ~
will remove newlines but retain space and tab characters.
Here we use ~
to remove the newline after the opening for
tag, but preserve indentation before <li>
.
Array construction syntax
If the left-hand side of a filtered expression (those found in output statements, the assign
tag and the echo
tag) is a comma separated list of primitive expressions, an array-like object will be created with those items.
or, using a {% liquid %}
tag:
With a
set to "Hello"
and b
set to "World"
, both of the examples above produce the following output.
String interpolation
String literals support interpolation using JavaScript-style ${
and }
delimiters. Liquid template strings don't use backticks like JavaScript. Any single or double quoted string can use ${variable_name}
placeholders for automatic variable substitution.
${
can be escaped with \${
to prevent variable substitution.
Liquid template strings are effectively a shorthand alternative to capture
tags or chains of append
filters. These two tags equivalent.
{% capture greeting %}
Hello, {{ you | capitalize }}!
{% endcapture %}
{% assign greeting = 'Hello, ${you | capitalize}!' %}
Logical not
Logical expressions now support negation with the not
operator and grouping terms with parentheses by default. Previously this was an opt-in feature.
In this example, {% if not user %}
is equivalent to {% unless user %}
, however, not
can also be used after and
and or
, like {% if user.active and not user.title %}
, potentially saving nested if
and unless
tags.
Ternary expressions
Inline conditional expression are now supported by default. Previously this was an opt-in feature. If omitted, the else
branch defaults to an instance of Undefined
.
Dedicated comment syntax
Comments surrounded by {#
and #}
are enabled by default. Additional #
's can be added to comment out blocks of markup that already contain comments, as long as hashes are balanced.
{## comment this out for now
{% for x in y %}
{# x could be empty #}
{{ x | default: TODO}}
{% endfor %}
##}
Better string literal parsing
String literals are now allowed to contain markup delimiters ({{
, }}
, {%
, %}
, {#
and #}
) and support c-like escape sequence to allow for including quote characters, literal newline characters and \uXXXX
Unicode code points.
Unicode identifiers
Identifiers and paths resolving to variables can contain Unicode characters (templates are assumed to be UTF-8 encoded). For example:
Scientific notation
Integer and float literals can use scientific notation, like 1.2e3
or 1e-2
.
Common argument delimiters
Filter and tag named arguments can be separated by a :
or =
. Previously only :
was allowed.
Template inheritance
Template inheritance is now built-in. Previously {% extends %}
and {% block %}
tags were available from a separate package.
i18n and l10n
Internationalization and localization tags and filters are now built in and enabled by default. Previously these were in a separate package.
See currency, datetime, money, decimal, unit, gettext, t and translate.
Serializable templates
Instances of Template
are now serializable. Use str(template)
or pickle.dump(template)
.
Better exceptions
Error messages have been improved and exceptions inheriting from LiquidError
expose line and column numbers, and have detailed_message()
and error context()
methods.
liquid2.exceptions.LiquidSyntaxError: expected IN, found WORD
-> '{% for x foo %}' 2:9
|
2 | {% for x foo %}
| ^^^ expected IN, found WORD
Features that have been removed
- We no longer offer "lax" or "warn" parsing modes, previously controlled by the
tolerance
argument toEnvironment
. The assertion is that errors should be loud and we should be made aware as early as possible, whether you're an experienced developer or not. - It's not currently possible to change Liquid markup delimiters (
{{
,}}
,{%
and%}
). - Async filters have not been implemented, but can be if there is a demand.
- Contextual template analysis has not been implemented, but can be if there is a demand.
- Template tag analysis (analyzing tokens instead of a syntax tree) has not been implemented, but can be if there is a demand.
- Liquid Babel used to allow simple, zero-argument filters in the arguments to the
translate
tag. Thetranslate
tag bundled in to Liquid2 does not allow the use of filters here.
API changes
These are the most notable changes. Please raise an issue or start a discussion if I've missed anything or you need help with migration.
- Package level
Template
can no longer be used as a convenience function for creating a template from a string. Useparse()
,render()
orDEFAULT_ENVIRONMENT.from_string()
instead. StrictUndefined
now plays nicely with thedefault
filter. Previously we had a separateStrictDefaultUndefined
class.FileSystemLoader
now takes an optional default file extension to use when looking for files that don't already have an extension. Previously there was a separateFileExtensionLoader
.AwareBoundTemplate
(a template with a built-intemplate
drop) has been removed, but can be added as a feature later if there is a demand.- The
auto_reload
andcache_size
arguments toEnvironment
have been removed. Now caching is handle by template loaders, not the environment. For example, pass aCachingFileSystemLoader
as theloader
argument toEnvironment
instead of aFileSystemLoader
. - The
strict_filters
argument toEnvironment
has been removed. Unknown filters now always raise anUnknownFilterError
. TemplateNotFound
has been renamed toTemplateNotFoundError
.Context
has been renamed toRenderContext
and now takes a mandatorytemplate
argument instead ofenv
. All other arguments toRenderContext
are now keyword only.FilterValueError
andFilterArgumentError
have been removed.LiquidValueError
andLiquidTypeError
should be used instead. In some cases whereFilterValueError
was deliberately ignored before,LiquidValueError
is now raised.- The exception
NoSuchFilterFunc
, raised when rendering a template that uses a filter that is not defined inEnvironment.filters
, has been renamed toUnknownFilterError
. - The
@liquid_filter
decorator has been removed. Now filter implementations are expected to raise aLiquidTypeError
in the event of an argument with an unacceptable type.
Custom tags
The lexer has been completely rewritten and the token's it produces bare little resemblance to those produced by any of the several parsing functions from Python Liquid. Now we have a single lexer that scans source text content, tags, statements and expressions in a single pass, and a parser that delegates the parsing of those tokens to classes implementing Tag
.
As before, Tag
instances are responsible for returning Node
s from Tag.parse()
. And nodes still have the familiar render_to_output()
abstract method.
As a result of these changes, custom tags are now limited to using tokens recognized by the lexer. Previously, custom tags would be passed their expression as a string to be parsed however you see fit, now tags are passed a sequence of tokens.
For now I recommend familiarizing yourself with the different tokens generated by the lexer, and refer to built-in tag implementations for examples of using various Expression.parse()
static methods to parse expressions. Note that the TokenStream
interface has changed too.
As always, open an issue or start a discussion if you need any help with migration.
Performance
TODO:
- Benchmarks show Python Liquid2 to be more JIT friendly
Package dependencies
The following packages are dependencies of Python Liquid2.
- Markupsafe>=3
- Babel>=2
- python-dateutil
- pytz
- typing-extensions