Compare commits

...

155 Commits

Author SHA1 Message Date
Kevin Brown
7e67623cbf Only support implicit tuples in variable expressions
They cause super inconsistent parsing behaviour if they show up
pretty much anywhere else because there is no difference between an
implicit tuple and a set of comma-separated parameters.
2020-05-16 17:54:52 -04:00
Kevin Brown
7151d778c4 Switch to parameterizing api tests 2020-05-16 17:17:51 -04:00
Kevin Brown
0581e3d8bd Properly handle implicit tuples
This adds support for implicit tuple literals and removes support for
identifier tuples for now.
2020-05-16 17:09:24 -04:00
Kevin Brown
a3aa821a5f Add proper parsing for {% for %} parameters
Previously a lot of the parsing was done through implicit variable
tuples, but that ended up causing a lot of special cases because we
were only allowing variables in those tuples. Now that we are going
to move towards having tuples be handled consitently, whether they
are identifier tuples or tuple literals, the logic for handling the
`{% for %}` block needed to be cleaned up.
2020-05-16 17:08:09 -04:00
Kevin Brown
140297e547 Raise expected syntax errors on bad calls 2020-05-16 15:17:48 -04:00
Kevin Brown
94efd92a59 Properly handle signed variables
Previously we were only handling signed numbers but we were handling
them as constants. Jinja implements a full system for unarary
operators that can be customized, so this switches the grammar and
the parser to use that system instead.
2020-05-16 15:03:57 -04:00
Kevin Brown
c97786d9dd Switch some tests to be parameterized
Right now they are using a for loop which makes it more difficult
to properly trace what iteration failed on. Additionally, it hides
cases where multiple items in the loop fail but not all do.
2020-05-16 15:02:53 -04:00
Kevin Brown
f872dcf65b Require at least one space between line block parameters 2020-05-15 22:29:01 -04:00
Kevin Brown
f11698df16 Do not require a newline before line statements
As we discovered in the tests, the logic for line statements only
enforces that when they are found, the remaining portion of the line
is treated as a statement declaration. This means that a line statement
can follow things such as text, which the grammar did not previously
allow. The grammar also did not allow a line statement to be the first
line in a file, wich this change also now supports. All non-newline
whitespace preceding the line statement will continue to be stripped.
2020-05-15 22:27:06 -04:00
Kevin Brown
adc2dce109 Implement proper line block closing logic
The documentation implies that line statements are terminated once a
newline statement is found, which would make sense when you think of
what a line block does. The lexer though does not quite implement this
logic, and instead will strip out additional whitespace past the newline
when the line statement is the last non-whitespace in the file.

This has to do with how the current regex is "\s*(\n|$)" which means
"strip any whitespace until a newline or end of file is reached".
Because the regex is greedy, this will strip any whitespace (including
newlines) to the end of the file, or it will only strip whitespace to
the first newline found if the end of the file is not possible. In
order to remain consistent with the old parser, the grammar has been
updated to reflect this behaviour.
2020-05-15 22:21:55 -04:00
Kevin Brown
61060096bd Properly split up line blocks from line block pairs
This is necessary for us to do proper pairing of line blocks since
otherwise the rejection would disallow line blocks of any type.
2020-05-15 22:20:51 -04:00
Kevin Brown
143517e89f Allow assignment values to be complex expressions 2020-05-15 22:19:28 -04:00
Kevin Brown
9f3853815a Fix print block
The print block would not actually print the value before because it
was not able to properly parse the variable that needed to be printed.
2020-05-15 22:18:48 -04:00
Kevin Brown
d760329966 Add line block pair checking
Similar to how it is done in regular Jinja blocks, line blocks will
now check to make sure that start and end blocks are properly paired
together. When they are not paired together, it will fall back to
parsing them separately like one would expect.
2020-05-15 22:17:46 -04:00
Kevin Brown
32a5f9312a Fix parsing math1 expressions in comparisons
When a math1 expression was used in a comparison before, the right
side would consume the comparison instead of letting it fall back to
the comparison being above it in the AST. This was fixed by restricting
the right side of math1 comparisons to be complex operations only, so
that is any operation that does not involve a comparison, since it's
unlikely that you are looking to do math expressions on the result of
a comparison.

Additionally, this also allows conditional expressions involving basic
operators to consume a complex expression on the left side. Previously
they were restricted to only allowing variables on the left side, so
this allows for more complex comparisons to be made.

Because of the way that conditional expressions with parentheses are
consumed, they have been moved to be the last check within conditional
expressions. This should help to guard against other conditional
expressions which can consume parenthese themselves from being blocked
from parsing because the parentheses have already been consumed.

In order to make things generally easier to understand, the complex
expressions wihch return an actual value (instread of just comparisons)
are now grouped together within the grammar.
2020-05-15 19:05:42 -04:00
Kevin Brown
b7c80abdac Fix parsing of the {% from %} block
There was an issue in the parser before where it was not properly
hanlding variable tuples within the parameters. Additionally, it was
not validating that "import" came after the template name, or that
when it did that any variables were actually specified. Both of those
are now being enforced properly.
2020-05-15 18:06:31 -04:00
Kevin Brown
9bb793c190 Fix grammar parsing for block separators
Previously this allows blocks to be separated by commas and have one
trailing off. Now this only allows blocks to be properly separated
by commas and trailing comms are no longer allowed. This also now
enforces that when parameters are not separated by commas, they are
spearated by at least one space.
2020-05-15 18:03:35 -04:00
Kevin Brown
f86503d13a Fix "not" expressions consuming variable starting with not
The `not` expression should have a space between the word "not" and
the expression which is being negated. Otherwise it will incorrectly
pick up things like "nothing" as "not thing" because it technically
meets the criteria.
2020-05-15 17:17:39 -04:00
Kevin Brown
639e8d2dff Add support for dot accessors to be numbers
This is an interesting special case in the old parser where numbers
are allowed as dot accessors, but they are specifically convertred
to being an item accessor during the parsing phase. We now support
numbers being parsed for dot accessors in the new parser.
2020-05-15 17:07:10 -04:00
Kevin Brown
1907179e06 Add support for the filter block 2020-05-15 16:15:46 -04:00
Kevin Brown
d8ad01caee Add support for negative numbers
This also fixes a bug where the exponent was not being properly
captured as well, just the sign of the number
2020-05-15 15:52:16 -04:00
Kevin Brown
a0364dd019 Allow symbols to be overwritten by the environment
This introduces a change to both the grammar and the parsing
environment that allows people to override start/end symbols in the
grammar through the environment. This finally brings the parser on
the same level as the old parser and lexer when it comes to handling
those customizations.

This means that the grammar must be compiled dynamically to account
for these customizations per environment. A module-level LRU cache
has been implemented to handle this fact, so grammars can be cached
instead of compiled every time. This should handle most cases other
than the unit tests, since most people aren't frequently changing up
their environment within their applications.

This also adds proper handling to the closing line block statement
so it waits for the end of a line or the end of the expression.
2020-05-15 12:27:10 -04:00
Kevin Brown
52a618e587 Support chained comparisons in parser 2020-05-15 10:32:07 -04:00
Kevin Brown
4ebdaf162e Add support for complex math expressions with +/-
Support for complex math expressions with other operators will come
eventually.
2020-05-14 23:49:34 -04:00
Kevin Brown
fca9030c92 Fix broken arguments for call blocks 2020-05-14 23:41:22 -04:00
Kevin Brown
591b250f71 Support for dyanamic args and kwargs in calls 2020-05-14 23:35:38 -04:00
Kevin Brown
4d39c1693f Fix call argument parsing
This temporarily break how `{% call %}` blocks work when arguments
are passed into them because it did not work consistently before. It
only worked for a single argument being passed in because of how
conditional expressions stripped out the parentheses if they were
present. Now they are properly captured but the parser does not yet
put them into the correct location within the Jinja AST.
2020-05-14 23:34:06 -04:00
Kevin Brown
2c53564fa1 Add undocumented print block support 2020-05-14 22:55:34 -04:00
Kevin Brown
1a6bc1cec5 Support variables in lists 2020-05-14 21:19:49 -04:00
Kevin Brown
5c5b819434 Fix whitespace handling on blocks
Because of the way it was ordered, it was properly picking up the
non-whitespace stripping block in some cases and failing to parse
as a result.
2020-05-14 20:22:26 -04:00
Kevin Brown
27d6eeba57 Handle undocumented whitespace parsing in variables 2020-05-14 20:15:15 -04:00
Kevin Brown
9d473e0e8a Parse test variable as conditional expression
This was necessary to fix some of the tests for tests which relied
on parentheses in place.
2020-05-14 20:05:43 -04:00
Kevin Brown
23a145dc2d Fix handling of tests which check constants
Because of how the parser works, it does not differentiate between
constants and function names, so this needs to convert it back.
2020-05-14 20:04:57 -04:00
Kevin Brown
e5d018fdef Fix parsing of the ** operator 2020-05-14 20:04:02 -04:00
Kevin Brown
06bcc5f9ac Fix blank iterables not parsing correctly
The AST returns `None` instead of an empty array of the value of an
empty iterable literal, so we need to special case when that happens
to get them to parse consistently.
2020-05-14 11:43:05 -04:00
Kevin
67e082fdf3 Add support for math expressions to grammar/parser 2020-05-14 11:21:20 -04:00
Kevin
7a34fa03b4 Tuples must contain a comma
This fixes an issue where parentheses-wrapped variables were being
interpreted by the grammar as tuples. This was because we were lacking
a definiiton for variable wrapped in parentheses and because the grammar
wasn't enforcing multiple values to be present in tuples.
2020-05-14 10:00:09 -04:00
Kevin
99947cf39a Test function single parameter should a variable
Previusly we were expecting it to be a conditional expression which
allowed it to swallow conditional expressions as if they were a single
variable. This fixes that issue so it only swallows variables.
2020-05-14 09:55:37 -04:00
Kevin
46da157031 Fix parsing of filters to allow chained calls 2020-05-13 23:06:25 -04:00
Kevin
9e20ca14bf Properly set environment on parsed template 2020-05-13 23:05:52 -04:00
Kevin
1a776bf469 Support filters being call chained
Previously filters were not treated the same as variable, as a
result it was not possible to call the result of a filter. Since
filters are treated as regular variable and therefore can be called
any number of times, this change was necessary to allow them to be
parsed the same way.
2020-05-13 23:04:32 -04:00
Kevin
7ea592b69a Switch testing to use jinja internals 2020-05-13 22:05:00 -04:00
Kevin
fa7c886eaf Swap out the old parser with the new one
This will allow us to figure out where the gaps are within the test
suite and also start running a comparison on the timing changes.
2020-05-13 22:01:54 -04:00
Kevin
d12c147d56 Add TatSu as a depdendency 2020-05-13 21:48:39 -04:00
Kevin
cb6d4938e5 Move semantics object into new_parser 2020-05-13 20:40:54 -04:00
Kevin
05a3ad7be1 Moved new_parser into jinja package 2020-05-13 20:38:31 -04:00
Kevin
fcea3e0de4 Remove empty extra files 2020-05-13 20:36:17 -04:00
Kevin
ea76328cfb Add support for test function arguments 2020-05-13 20:33:56 -04:00
Kevin
60bcc59151 Add support for scoped blocks 2020-05-13 20:23:19 -04:00
Kevin
658dbaaf12 Add support for tests in {% for %} 2020-05-13 20:14:40 -04:00
Kevin
c9a65a9e7f Add full support for {% if %} block 2020-05-13 18:48:17 -04:00
Kevin
4ac2db7a16 Add support for call block 2020-05-12 22:24:40 -04:00
Kevin
e8689ca98c Add support for parsing {% else %} blocks in for loops 2020-05-12 22:10:49 -04:00
Kevin
00e950e831 Added semantics for pairing blocks together
This required us to modify how the parser works so that once it
detects a pair of blocks, it kicks it back to our specific function
which allows us to detect if the pair of blocks it detected were a
matching pair. This is required in order to allow single blocks to
be included within paired blocks, as otherwise it would always match
the last single block to the end block.

This required changing the grammar so the pair blocks had their own
named expression. This allows us to reject the parse as invalid with
incorrect semantics and allows it to try to just parse the first
block alone.
2020-05-12 21:27:40 -04:00
Kevin
9cf1c578d4 Add support for {% include ignore missing %} 2020-05-12 16:58:09 -04:00
Kevin
66908d0c52 Fix with_context defaulting to false on include block 2020-05-11 23:03:30 -04:00
Kevin
4f194fcf41 Add support for import block parsing 2020-05-11 23:00:29 -04:00
Kevin
3f8bcd5f0b Add support for parsing {% include %} tags
This does not yet support the `ignore_missing` flag that can be
passed as a parameter to an `{% include %}` tag
2020-05-11 22:48:12 -04:00
Kevin
9d408a7d5c Add support for {% from with context %} 2020-05-11 22:47:58 -04:00
Kevin
dadb931c84 Add support for not() expressions in parser 2020-05-11 22:28:11 -04:00
Kevin
487fa26d79 Add support for parentheses grouping in expressions 2020-05-11 22:27:51 -04:00
Kevin
05d348e29d Add support for negated test expressions 2020-05-11 22:27:38 -04:00
Kevin
72a327ae4e Add support for not() expressions to grammar
This allows for boolean expressions to be negated on the fly
2020-05-11 22:26:09 -04:00
Kevin
8c5a124c5c Add support for "not in"/"notin" to grammar/parser 2020-05-11 22:25:16 -04:00
Kevin
b948be4625 Fix {% for in %} loop parsing
This fixes the fact that most `{% for in %}` loops will be parsed
using the `in` operator now, so that operator must be detected and
extracted out in order to make it parse the same way as before. This
is the start of the special cases within the parser for handling
Jinja's previous parsing style.
2020-05-11 21:50:57 -04:00
Kevin
cf8262bd47 Support in/notin operator expressions in parser 2020-05-11 21:50:51 -04:00
Kevin
9b2bee5f5e Support "in" operator in grammar
This changes the previous comparison operations from being marked
as solely comparion expressions and expands them out to be generation
operator expressions. This allows us to easily support the "in"
operator, which in the current Jinja parser is handled exactly the
same as the other operations, but it does require us to special case
the automated conversion of the "not ... in" expression to a "notin"
expression.
2020-05-11 21:45:43 -04:00
Kevin
6c179cf714 Add concat expression support to grammar and parser
This is probably going to be reclassified in the grammar and parser
as something different from the conditional expressions once more
support for math operators is added in.
2020-05-11 21:25:29 -04:00
Kevin
983f480532 Simplify variable values in grammars
Now that variable identifers are able to be used as conditional
expressions, we can just specify that variable expressions in the
grammar are looking for a conditional expression as the name of the
variable.
2020-05-11 21:11:32 -04:00
Kevin
83665a1706 Add support for the extends block 2020-05-11 21:10:26 -04:00
Kevin
07a3d8988f Support if/else expressions in parser 2020-05-11 21:10:08 -04:00
Kevin
64d169038b Allow variable identifiers to be conditionals
This aligns with the Python behaviour and pre-existing Jinja behaviour
where a variable expression can be used as a test for a conditional.
This was necessary to allow variable identifiers to be used in places
which was expecting a possible conditional expression, like in a if/else
expression.
2020-05-11 21:08:06 -04:00
Kevin
a877716f53 Restrict where conditional expressions are allowed
Previously conditional expressions were only allowed in things which
accepted parameters to call accessors, which is most things, but this
was found to be too broad. Many contexts to not actually allow conditional
expressions so this was restricted back to block parameters and variable
calls.
2020-05-11 21:07:58 -04:00
Kevin
5d2e372899 Add support for if/else expressions in grammar
These fall under a special type of conditional expression and can
only be used in certain places. The grammar for test functions
needed to be updated to rejected test function parameters if they
are only called "else", since that is likely to be for an if/else
expression. This matches the existing behaviour of the Jinja parser.
2020-05-11 21:03:45 -04:00
Kevin
0d9afd99ed Add support for "None" and "none" to grammar/parser 2020-05-11 20:19:42 -04:00
Kevin
33d66c4003 Add support for parsing dictionary literals 2020-05-11 20:15:28 -04:00
Kevin
650eb73721 Add support dictionary literals to grammar
This also allows dictionary values to be variables instead of just
regular identifiers, a change which might be made to other literals
such as lists and tuples in the future as we determine what those
also support.
2020-05-11 20:14:40 -04:00
Kevin
826ad27552 Support single-parameter tests without parentheses
This adds support for the optional parantheses in tests that are only
being supplied a single parameter. Tests which use parantheses are
currently not supported in the parser, but are supported in the grammar.
2020-05-11 20:02:48 -04:00
Kevin
374f1ed29f Remove unused variable_tests from grammar 2020-05-11 19:54:10 -04:00
Kevin
fb82cba2c8 Support the autoescape block 2020-05-11 19:40:15 -04:00
Kevin
2f9cca0b75 Add parsing of conditional expressions 2020-05-10 17:36:33 -04:00
Kevin
9e94e24afd Fix AST for conditional expressions
Previously all conditional expressions were left associative which
produced an AST that was nothing like the one in Jinja and one which
did not respect order of operations within conditions. The conditional
expression part of the grammar has been rewritten to be more explicit
about what can and cannot match which appears to have fixed those
issues with the AST.
2020-05-10 17:34:50 -04:00
Kevin
53c8bdba35 Fix macros not marking args as params
It was only marking keyword arguments as params which is not totally
correct.
2020-05-10 17:06:49 -04:00
Kevin
6ddfd8c565 Fix block set params being marked for load 2020-05-10 17:03:35 -04:00
Kevin
c5bcac08bd Fix macro params being marked to load
These are being used as variables within the macro itself so these
should be marked as parameters so they do not escape the scope of the
block.
2020-05-10 17:02:21 -04:00
Kevin
b091d41a54 Fixed elif/else sections of if node being None
The default should be an empty list to match the existing AST but
we were incorrectly setting it to `None`.
2020-05-10 17:01:33 -04:00
Kevin
00d9c0cfce Combine common template data/outputs in AST
This makes it easier to compare the AST generated by the old Jinja
parser and the AST generated by the new one, since the new AST
separates out template data character by character currently.
2020-05-10 16:44:08 -04:00
Kevin
f0f03f745b Filter out None from parsed blocks
This allows for comments to not appear in the generated AST.
2020-05-10 16:30:58 -04:00
Kevin
70227394b0 Allow variable identifiers to be aliased in block params
This is only really supported on the `{% from %}` block currently,
but the ability exists to use this elsewhere if someone is looking
for the ability to alias variable identifiers. This also allows
value-only parameters to be comma-separated within the block
parameters, since before that was only allows for key-value parameters.
2020-05-10 16:24:12 -04:00
Kevin
90f036a934 Mark with targets as parameters
This fixes a bug where the targets of a `{% with %}` block would not
be marked as a parameter. This is because they were not being marked
at all as a variable which results in an invalid AST. For reference
counting purposes, this must also be marked specifically as a parameter
variable instead of as a stored variable to ensure it does not leak
out of the block.
2020-05-10 15:52:07 -04:00
Kevin
6116df58b6 Properly mark set target as variable
Previously it wasn't being marked as a variable at all if it was just
a string literal, so this fixes it so Jinja knows that the assignment
should be stored on the target variable.
2020-05-10 15:50:15 -04:00
Kevin
2c2a04f3c6 Switch From node to use constructor
This ensures that `with_context` is properly initialized even though
we don't currently support it.
2020-05-10 15:49:26 -04:00
Kevin
272a7e9b7f Allow optional comma after block keyword parameters
It looks like this is only currently support within `{% with %}`
blocks in Jinja, instead of space-separating the parameters, but
this may also be happening in extensions as well.
2020-05-10 15:47:53 -04:00
Kevin
9b29c5e1c3 Parse macro blocks 2020-05-10 14:29:54 -04:00
Kevin
77147d8c10 Support assignment blocks using {% set %}
This adds support for the usage of `{% set %}` where the contents of
the block are assigned to the variable instead of handling that within
the block parameters.

Because Jinja separates the filter from the variable within the
`AssignBlock` node, we have to detect when there is a wrapping filter
and extract it so that it can slot in properly.
2020-05-10 14:08:48 -04:00
Kevin
807b6effbc Parse isolated set blocks
This adds support for set blocks where a key-value pair is being
sent in so there is no matching pair of statement.
2020-05-10 13:59:23 -04:00
Kevin
1e98d2b19c Parse arguments to calls
This can be combined with the logic for parsing arguments to filters
then they both generate the same AST.
2020-05-10 12:54:08 -04:00
Kevin
d54d93ef2f Initial support for if blocks
This currently only supports tests which are a simple condition and
do not span multiple sides of a comparison.
2020-05-10 12:42:22 -04:00
Kevin
51f4815dde Add parsing of boolean literals
This should finish off all of the parsing of currently supported
literals.
2020-05-10 12:41:31 -04:00
Kevin
a221d0a387 Support parsing {% block %} blocks
This does not currently parse out the scoped parameter but that will
come soon enough.
2020-05-10 12:27:20 -04:00
Kevin
f524b56a42 Properly support recursive in for loops
This adds an example to the test file that is being used to verify
that the generated ASTs are comparable.
2020-05-10 12:16:16 -04:00
Kevin
f38c803529 Add initial parsing of for blocks
This will correctly parse out most of the starting block as well as
the contents, but it does not yet handle the parsing of the `{% else %}`
block that can be contained within the contents of the block.
2020-05-10 12:04:38 -04:00
Kevin
58d0b06100 Parse variable tuples
Right now these are custom parsed since they are not the same as
tuple literals and generally serve a different purpose. They are
tuples which contain variable identifiers and are generally used
for assignment.
2020-05-10 12:03:32 -04:00
Kevin
ae6773932a Add initial support for call accessors
This only currently supports call accessors that do not take any
parameters. For now it is hard-coded to have no parameters.
2020-05-10 12:02:28 -04:00
Kevin
38a2839dd0 Add list and tuple literal parsing 2020-05-10 12:01:52 -04:00
Kevin
ba5b4ec205 Enable line parsing in Jinja environment
Right now the grammar is being tested with line parsing enabled so
this makes them match
2020-05-10 11:50:41 -04:00
Kevin
30505b5df6 Parse raw blocks and comments
Comments get ignored by the parser and raw blocks just get output
directly as a string into the AST.
2020-05-10 11:28:26 -04:00
Kevin
1bc1068258 Add parsing for numeric literals 2020-05-10 11:19:12 -04:00
Kevin
cf6c74b8ab Add filter parsing for variables
Filters and their arguments are now properly parsed into the right
Jinja nodes. Dynamic arguments (`*args`) and dynamic keyword arguments
(`**kwargs`) are not currently parsed as they are not supported by
the grammar.
2020-05-10 11:13:15 -04:00
Kevin
e0c739aae5 Dot accessors can only be identifiers
This fixes a bug where the grammar allowed dot accessors to be any
variable type. This results in the filters that were meant for the
variable that was being accessed to be captured by the accessor
itself, which generated an unexpected and invalid AST.
2020-05-10 10:48:16 -04:00
Kevin
a2d262fde4 Output nodes are always lists
Right now we don't optimize Output nodes so that ones next to each
other are combined, but they should all contain a list of a single
node in preparation for eventually doing that.
2020-05-10 10:43:50 -04:00
Kevin
80a6e5b3e7 Parse the contents of the body of with block
This logic will inevitable be generalized out in the future for
other full blocks, but for now it properly handled parsing the
contents of the block and converting it to the Jinja AST.
2020-05-10 10:43:00 -04:00
Kevin
e9f327c2ee Properly parse with targets
The targets of with expressions (the variables to load) should be
properly parsed as a variable using the same load logic which is
used in other areas of the parser.
2020-05-10 10:42:21 -04:00
Kevin
ff7b3202fd Start working on new AST parser
This parser will take the Tatsu-generated AST and generate a compatible
Jinja AST from it. This should allow us to refine the grammar to
generate a better AST and also verify that it is producing comparable
Jinja ASTs that can be used by the current compiler.
2020-05-10 10:29:48 -04:00
Kevin
17945c23a5 Support filters on all variables
This adds support to filters on all variables instead of to just
variables which are being printed within templates. This now allows
us to pass in variables that are using filters as parameters within
a block, so they can be used for setting new variables or to perform
transformations on the fly.
2020-05-09 22:23:32 -04:00
Kevin
1a2512a1bc Rename grammar to grammar.ebnf
This will enable syntax higlighting on GitHub and also makes it more
clear what format is being used within the file.
2020-05-09 21:54:14 -04:00
Kevin
ebf4fea5e9 Case insensistive exponent parsing 2020-05-09 21:50:06 -04:00
Kevin Brown
266caac0ee Variable filters should not capture whitespace
This should have no functional difference on the behaviour of the
parser but it does make filters consistent with other areas where
it expects to start with the first character instead of starting
with whitespace.

This also changes the test script to time the individual steps so
we can confirm that the grammar compilation is considerably slower
than the parsing of the template using the grammar.
2020-05-09 21:50:06 -04:00
Kevin Brown
ae85164da6 Enable leading whitespace stripping for blocks 2020-05-09 21:50:06 -04:00
Kevin Brown
41c23c37cc Enable whitespace control within closing blocks
This enables the ability to force whitespace to be stripped following
the end of a block statement by adding a `-` before the closing
tag. This is supported both for stripped the space at the beginning
of a block as well as stripping the space that follows a block.
2020-05-09 21:50:06 -04:00
Kevin Brown
060a6f301f Name contents of raw blocks
This will also strip the open/close blocks from the AST which cleans
up the final part of it.
2020-05-09 21:50:06 -04:00
Kevin Brown
f9acfe01dd Name parts of a block 2020-05-09 21:50:06 -04:00
Kevin Brown
fdc7c285d4 Remove whitespace parsing from inside block
This fixes an issue where trailing whitespace within blocks was being
processed as an expression instead of being collected into the block
definition. Now the whitespace is consisitently stored within the
block expression which should make whitespace handling easier to
implement.
2020-05-09 21:50:06 -04:00
Kevin Brown
9174140c79 Add support for optional ":" at end of line statement
This does not fully work for the example file because the grammar
for a test expression can span multiple lines which causes the HTML
in the line following the block expression to be detected as a test
expression instead of being detected as content.
2020-05-09 21:50:06 -04:00
Kevin Brown
dccbd0ba48 Fix parameters eating whitespace
This was causing issues for anything that depends on significant
whitespace to follow a parameter, since it wasn't being made
available within the grammar.
2020-05-09 21:50:06 -04:00
Kevin Brown
35c9bc10c2 Support independent line statement
This allows you to have a line statement that is not paired with an
ending one.
2020-05-09 21:50:06 -04:00
Kevin Brown
f96c34565f Strip spaces before line statements and prefixes 2020-05-09 21:50:06 -04:00
Kevin Brown
bcf99099b6 Add suport for line comments
This also switches comments to produce the same AST whether it is
an inline comment or a line comment.
2020-05-09 21:50:06 -04:00
Kevin Brown
5799223303 Consistent formatting of OR within grammar 2020-05-09 21:50:06 -04:00
Kevin Brown
b030045a7a Add support for line statements
These are block expresions but the start/end tags take up an entire
line and are marked by a leading symbol.
2020-05-09 21:50:06 -04:00
Kevin Brown
c098a6089d Ensure macro definitions are properly parsed 2020-05-09 21:50:06 -04:00
Kevin Brown
6a80dfd6fb Test specific imports are supported 2020-05-09 21:50:06 -04:00
Kevin Brown
711e47bf5c Converted more of parse tree to dictionary
This should make it easier to detect what type of literal has been
parsed (we don't differentiate between single and double quoted
strings) as well as determining the specific accessors that are
called on a given variable.

Tuple and list literals have also been normalized to hold their
values in a key called `value` which is the same as other literals.
Implicit identifier tuples have not been switched yet because those
are not currently parses like tuple literals.
2020-05-09 21:50:06 -04:00
Kevin Brown
7949f22fd1 Support implicit tuples in block parameters
This fixes an issue where implicit tuples were not parsed correctly
when they were used as a key in a block parameter. Now for loops
and set statements with implicit tuples work properly. This only
supports implicit tuples when all values are identifiers, since
these are generally used for assignment and you cannot assign a
new value to a literal.
2020-05-09 21:50:06 -04:00
Kevin Brown
7ac9e2d978 Add support for tests
Right now these are very basic and don't appear to form the correct
parse tree for logical comparisons which use tests as well. But it
parses at lease somewaht correctly, so there is something to work
with here.
2020-05-09 21:50:06 -04:00
Kevin Brown
e7ff13e6e1 Support assignment expressions in generic blocks
Because of the way assignment expressions handle implicit tuples,
it's now possible to support the complex for loops within the
standard generic block grammar.
2020-05-09 21:50:06 -04:00
Kevin Brown
f69ad20fb5 Added grammar for for loops
Since this needs to support assignment targets, it is difficult to
properly support this within a generic block syntax because of the
ability to create implicit tuples.
2020-05-09 21:50:06 -04:00
Kevin Brown
caf8992e99 Added tuple literal support
This maintains the expectation that tuple literals will always end
with a comma if there is a single item.

The example is the combined one from the Jinja docs but it does not
include the comma for the tuple assignment since the grammar does
not currently support that.
2020-05-09 21:50:06 -04:00
Kevin Brown
a9d8449fa1 Support list literals 2020-05-09 21:50:06 -04:00
Kevin Brown
5fdbdec06c Support boolean literals
This will transparently convert them into an actual boolean which
should skip any variable-like parsing that would otherwise be done.
2020-05-09 21:50:06 -04:00
Kevin Brown
e9e098cc48 Add integer and float literals
These align with the one already supported by Jinja.
2020-05-09 21:50:06 -04:00
Kevin Brown
d8dafa2e18 Switch blocks and variables to dictionaries
This should make it easier to differentiate them based on their
type but also allow for the different pieces to have proper names.
2020-05-09 21:50:06 -04:00
Kevin Brown
ba6e96207d Switch to named dict for filters
This will make it easier to determine what filters need to be
called like a function with parameters and which ones assume
default parameters.
2020-05-09 21:50:06 -04:00
Kevin Brown
080489c63c Support key-value block parameters
Block parameters should support all of the things that a function
call parameter would normally support. This includes key-value
paramters and in our test we include the transaltion example from
the documentation.
2020-05-09 21:50:06 -04:00
Kevin Brown
3480d1fa0e Support named parameters to calls
This also switches the parameters in calls to be returned as a
dictionary which should make it easier to differentiate between a
value-only parameter and a key-value parameter.
2020-05-09 21:50:06 -04:00
Kevin Brown
611bdcfcc4 Variable calls support multiple parameters
This also updates filter parameters to be handled the same as calling
a variable.
2020-05-09 21:50:06 -04:00
Kevin Brown
4a610f5357 Filter parameters are variable identifiers
The filter parameters list is actually the same as a variable
accessor for a call but that does not currently support multiple
parameters.
2020-05-09 21:50:06 -04:00
Kevin Brown
f12d76704d Add function calls for variables
This allows for functions to be called, currently with a single
variable optionally passed.

This also adds a check to make sure that variable literals are
supported.
2020-05-09 21:50:06 -04:00
Kevin Brown
9f92f5c9e8 Parse block parameter values like variables
This allows for complex values to be used in parameters while also
supporting standard literals.
2020-05-09 21:50:06 -04:00
Kevin Brown
bf331cafba Support dot accessors on variables
This adds support for dot accessors on variables in such a way that
it is flexible enough to match the handling provided by the existing
template engine.
2020-05-09 21:50:06 -04:00
Kevin Brown
22764094bb Start definining variable identifiers
Variables are standard identifiers or literals that can be
augmented by accessors (either dictionary or dot style). This
finally defines what a string is.
2020-05-09 21:50:06 -04:00
Kevin Brown
ca3ecffd9a Strip parens from filter params list
This isn't necessary, we will always know that the second item within
the filter list is the set of filter parameters.

This also ensures that any time the filter separator is detected,
we force parsing the next token as a filter parameter.
2020-05-09 21:50:06 -04:00
Kevin Brown
1604a0c87b Use variable names in content definition
This should make it easier later to override these variables without
needing to make additional changes.
2020-05-09 21:50:06 -04:00
Kevin Brown
8bebf88507 Fix content overtaking expressions
This fixes a issue where content would try to overtake everything
following it, even if there was a better expression to match after
the content. This was fixed by telling content to match everything
but the start of different expressions, which appears to solve a
bunch of issues.
2020-05-09 21:50:06 -04:00
Kevin Brown
db4ad51426 Initial commit 2020-05-09 21:50:06 -04:00
10 changed files with 1831 additions and 41 deletions

5
.gitignore vendored
View File

@ -18,3 +18,8 @@ venv-*/
htmlcov
.pytest_cache/
/.vscode/
tatsu_jinja.json
tatsu_jinja.py
parsed_jinja.py
test_template.jinja

664
grammar.ebnf Normal file
View File

@ -0,0 +1,664 @@
start
=
expressions
$
;
expressions
=
{expression}*
;
expression
=
| content
| raw_block_expression
| block_expression
| line_block_expression
| variable_expression
| comment_expression
| line_comment_expression
;
raw_block_expression
=
raw_block_start
raw:{ !raw_block_end CHAR }*
raw_block_end
;
raw_block_start
=
block_open "raw" {SP}* block_close
;
raw_block_end
=
block_open "endraw" {SP}* block_close
;
block_expression
=
| block_expression_pair
| block_expression_single
;
block_expression_pair
=
start:block_start contents:expressions end:block_end
;
block_expression_single
=
block:block_start
;
block_start
=
block_open !("end") name:IDENTIFIER [ "(" name_call_parameters:variable_accessor_call_parameters ")" ]
[ {SP}+ parameters:block_parameters ]
{SP}* block_close
;
block_end
=
block_open "end" name:IDENTIFIER {SP}* block_close
;
block_open
=
| ( {SP}* block_open_symbol "-" {SP}* )
| block_open_symbol {SP}*
;
block_open_symbol
=
"{%"
;
block_close
=
| ( "-" block_close_symbol {SP}* )
| block_close_symbol
;
block_close_symbol
=
"%}"
;
line_block_expression
=
| line_block_expression_pair
| line_block_expression_single
;
line_block_expression_pair
=
start:line_block_start contents:expressions end:line_block_end
;
line_block_expression_single
=
block:line_block_start
;
line_block_start
=
line_block_open !("end") name:IDENTIFIER { !"\n" SP}* parameters:[ line_block_parameters ] [ { !"\n" SP }* ":" ] line_block_close
;
line_block_end
=
line_block_open "end" name:IDENTIFIER line_block_close
;
line_block_open
=
{ !"\n" SP }* line_block_open_symbol { !"\n" SP }*
;
line_block_open_symbol
=
"#"
;
line_block_close
=
| ( {SP}* $ )
| ( { !"\n" SP }* "\n" )
;
line_block_parameters
=
@+:block_parameter { { !"\n" SP }+ @+:block_parameter }*
;
block_parameters
=
@+:block_parameter
{
block_parameter_separator
@+:block_parameter
}*
;
block_parameter_separator
=
| ( {SP}* "," {SP}* )
| ( {SP}+ )
;
block_parameter
=
| block_parameter_key_value
| block_parameter_value_only
;
block_parameter_key_value
=
key:block_parameter_key {SP}* "=" {SP}* value:variable_accessor_call_parameter_value
;
block_parameter_key
=
variable_accessor_call_parameter_key
;
block_parameter_value_only
=
| value:variable_identifier_with_alias
| value:variable_accessor_call_parameter_value
| value:conditional_expression
;
variable_expression
=
variable_open type:`variable` name:variable_expression_name variable_close
;
variable_open
=
| ( {SP}* variable_open_symbol "-" {SP}* )
| ( variable_open_symbol {SP}* )
;
variable_open_symbol
=
"{{"
;
variable_close
=
| ( {SP}* "-" variable_close_symbol {SP}* )
| ( {SP}* variable_close_symbol )
;
variable_close_symbol
=
"}}"
;
variable_expression_name
=
| TUPLE_LITERAL
| conditional_expression
;
variable_identifier
=
| variable_identifier_parentheses
| variable_identifier_raw
;
variable_identifier_parentheses
=
"(" @:conditional_expression ")"
;
variable_identifier_raw
=
[ signed:( "-" | "+" ) ]
variable:( LITERAL | IDENTIFIER )
accessors:{ variable_accessor }*
{ {SP}* filters+:variable_filter }*
;
variable_identifier_with_alias
=
variable:IDENTIFIER
{SP}* "as" {SP}*
alias:IDENTIFIER
;
variable_accessor
=
| variable_accessor_brackets
| variable_accessor_call
| variable_accessor_dot
;
variable_accessor_brackets
=
accessor_type:`brackets`
"[" parameter:variable_identifier "]"
;
variable_accessor_call
=
accessor_type:`call`
"(" parameters:[variable_accessor_call_parameters] ")"
;
variable_accessor_dot
=
accessor_type:`dot`
"." parameter:( IDENTIFIER | NUMBER_LITERAL )
;
variable_accessor_call_parameters
=
@+:variable_accessor_call_parameter
{ {SP}* "," {SP}* @+:variable_accessor_call_parameter }*
;
variable_accessor_call_parameter
=
| variable_accessor_call_parameter_key_value
| variable_accessor_call_parameter_value_only
| variable_accessor_call_parameter_vararg
| variable_accessor_call_parameter_varkwarg
;
variable_accessor_call_parameter_vararg
=
"*" dynamic_argument:variable_identifier
;
variable_accessor_call_parameter_varkwarg
=
"**" dynamic_keyword_argument:variable_identifier
;
variable_accessor_call_parameter_key_value
=
key:variable_accessor_call_parameter_key {SP}* "=" {SP}* value:variable_accessor_call_parameter_value
;
variable_accessor_call_parameter_value_only
=
value:variable_accessor_call_parameter_value
;
variable_accessor_call_parameter_key
=
IDENTIFIER
;
variable_accessor_call_parameter_value
=
conditional_expression
;
conditional_expression
=
| conditional_expression_not
| conditional_expression_if
| conditional_expression_logical
| conditional_expression_operator
| conditional_expression_test
| complex_expression
| variable_identifier
| conditional_expression_parentheses
;
complex_expression
=
| complex_expression_powers
| complex_expression_math2
| concatenate_expression
| complex_expression_math1
| complex_expression_parentheses
| variable_identifier
;
complex_expression_powers
=
left:variable_identifier {SP}* math_operator:"**" {SP}* right:variable_identifier
;
complex_expression_math2
=
left:variable_identifier
{SP}* math_operator:complex_expression_math2_operations {SP}*
right:variable_identifier
;
complex_expression_math2_operations
=
| "*"
| "/"
| "//"
| "%"
;
complex_expression_math1
=
left:variable_identifier
{SP}* math_operator:complex_expression_math1_operations {SP}*
right:complex_expression
;
complex_expression_math1_operations
=
| "+"
| "-"
;
complex_expression_parentheses
=
"(" {SP}*
complex_expression
{SP}* ")"
;
conditional_expression_parentheses
=
"(" {SP}* @:conditional_expression {SP}* ")"
;
conditional_expression_not
=
"not" {SP}+ not:conditional_expression
;
conditional_expression_if
=
true_value:variable_identifier
{SP}* "if" {SP}*
test_expression:conditional_expression
[ {SP}* "else" {SP}* false_value:conditional_expression ]
;
conditional_expression_logical
=
left:conditional_expression
{SP}* logical_operator:variable_tests_logical_operator {SP}*
right:conditional_expression
;
conditional_expression_operator
=
conditional_expression_operator_in
| (
left:complex_expression
{SP}* operator:conditional_expression_operator_operations {SP}*
right:conditional_expression
)
;
conditional_expression_operator_in
=
| (
"not"
left:variable_identifier
{SP}* operator:`"notin"` "in" {SP}*
right:variable_identifier
)
| (
left:variable_identifier
{SP}+
(
| ( "not" {SP}* "in" operator:`"notin"` )
| operator:"in"
)
{SP}+
right:variable_identifier
)
;
conditional_expression_test
=
test_variable:variable_identifier
{SP}* "is"
[ {SP}+ "not" {SP} negated:`True` ]
{SP}*
test_function:variable_identifier
[
{SP}*
!( (variable_tests_logical_operator | "is" | "else" ) {SP}* )
test_function_parameter:variable_identifier
]
;
conditional_expression_operator_operations
=
| "=="
| "!="
| ">"
| ">="
| "<"
| "<="
;
variable_tests_logical_operator
=
| "and"
| "or"
;
concatenate_expression
=
concatenate+:variable_identifier
{ {SP}* "~" {SP}* concatenate+:variable_identifier }+
;
variable_filter
=
"|" {SP}* @:filter
;
filter =
variable:IDENTIFIER
accessors:{ variable_accessor_call }*
;
comment_expression
=
comment_open comment:comment_content comment_close
;
comment_open =
comment_open_symbol
;
comment_open_symbol
=
"{#"
;
comment_close
=
comment_close_symbol
;
comment_close_symbol
=
"#}"
;
comment_content
=
{ !comment_close CHAR }*
;
line_comment_expression
=
line_comment_open comment:line_comment_content &"\n"
;
line_comment_open
=
{SP}* line_comment_open_symbol
;
line_comment_open_symbol
=
'##'
;
line_comment_content
=
{ !"\n" CHAR }*
;
content
=
!(
| line_block_open
| block_open
| variable_open
| comment_open
| line_comment_open
) CHAR ;
LITERAL
=
| NONE_LITERAL
| STRING_LITERAL
| NUMBER_LITERAL
| BOOLEAN_LITERAL
| DICTIONARY_LITERAL
| LIST_LITERAL
| EXPLICIT_TUPLE_LITERAL
;
DICTIONARY_LITERAL
=
literal_type:`dictionary`
(
| ( "{" {SP}* value+:dictionary_key_value { {SP}* "," {SP}* value+:dictionary_key_value } {SP}* "}" )
| ( "{" {SP}* "}" )
)
;
dictionary_key_value
=
key:STRING_LITERAL {SP}* ":" {SP}* value:variable_identifier
;
LIST_LITERAL
=
literal_type:`list`
(
| ( "[" {SP}* value+:variable_identifier {SP}* { "," {SP}* value+:variable_identifier }* {SP}* "]" )
| ( "[" {SP}* "]" )
)
;
TUPLE_LITERAL
=
| EXPLICIT_TUPLE_LITERAL
| IMPLICIT_TUPLE_LITERAL
| EMPTY_TUPLE_LITERAL
;
EXPLICIT_TUPLE_LITERAL
=
literal_type:`tuple`
"(" value:TUPLE_LITERAL_CONTENTS ")"
;
IMPLICIT_TUPLE_LITERAL
=
literal_type:`tuple`
value:TUPLE_LITERAL_CONTENTS
;
TUPLE_LITERAL_CONTENTS
=
| ( @+:variable_identifier {SP}* { "," {SP}* @+:variable_identifier {SP}* }+ )
| ( @+:variable_identifier {SP}* "," {SP}* )
;
EMPTY_TUPLE_LITERAL
=
literal_type:`tuple`
"(" {SP}* ")"
;
INTEGER_LITERAL
=
/[\d_]*\d+/
;
SIGNED_INTEGER_LITERAL
=
/[+-]?[\d_]*\d+/
;
NUMBER_LITERAL
=
literal_type:`number`
whole:INTEGER_LITERAL
[ "." fractional:INTEGER_LITERAL ]
[ ( "e" | "E" ) exponent:SIGNED_INTEGER_LITERAL ]
;
STRING_LITERAL
=
| STRING_LITERAL_SINGLE_QUOTE
| STRING_LITERAL_DOUBLE_QUOTE
;
STRING_LITERAL_SINGLE_QUOTE
=
literal_type:`string`
"'" value:{ !"'" /./ }* "'"
;
STRING_LITERAL_DOUBLE_QUOTE
=
literal_type:`string`
'"' value:{ !'"' /./ }* '"'
;
BOOLEAN_LITERAL
=
literal_type:`boolean`
(
| ( ("true" | "True") value:`True`)
| ( ("false" | "False") value:`False`)
)
;
NONE_LITERAL
=
literal_type:`none`
( "none" | "None" ) value:`None`
;
IDENTIFIER
=
/[a-zA-Z_][a-zA-Z0-9_]*/
;
ALPHA
=
/[a-zA-Z]/
;
DIGIT
=
/[0-9]/
;
SP
=
/\s/
;
CHAR
=
| ?'.'
| ?'\s'
;

View File

@ -34,7 +34,7 @@ setup(
package_dir={"": "src"},
include_package_data=True,
python_requires=">=3.6",
install_requires=["MarkupSafe>=1.1"],
install_requires=["MarkupSafe>=1.1", "TatSu"],
extras_require={"i18n": ["Babel>=2.1"]},
entry_points={"babel.extractors": ["jinja2 = jinja2.ext:babel_extract[i18n]"]},
)

View File

@ -50,6 +50,7 @@ from .utils import missing
# for direct template usage we have up to ten living environments
_spontaneous_environments = LRUCache(10)
_grammar_cache = LRUCache(10)
def get_spontaneous_environment(cls, *args):
@ -527,6 +528,69 @@ class Environment:
"""Internal parsing function used by `parse` and `compile`."""
return Parser(self, source, name, filename).parse()
def get_grammar(self):
import tatsu
grammar_extensions = ''
with open('grammar.ebnf', 'r') as grammar_file:
base_grammar = grammar_file.read()
if self.block_start_string:
grammar_extensions += '''
@override
block_open_symbol = %r;
''' % (self.block_start_string)
if self.block_end_string:
grammar_extensions += '''
@override
block_close_symbol = %r;
''' % (self.block_end_string)
if self.variable_start_string:
grammar_extensions += '''
@override
variable_open_symbol = %r;
''' % (self.variable_start_string)
if self.variable_end_string:
grammar_extensions += '''
@override
variable_close_symbol = %r;
''' % (self.variable_end_string)
if self.comment_start_string:
grammar_extensions += '''
@override
comment_open_symbol = %r;
''' % (self.comment_start_string)
if self.comment_end_string:
grammar_extensions += '''
@override
comment_close_symbol = %r;
''' % (self.comment_end_string)
if self.line_statement_prefix:
grammar_extensions += '''
@override
line_block_open_symbol = %r;
''' % (self.line_statement_prefix)
if self.line_comment_prefix:
grammar_extensions += '''
@override
line_comment_open_symbol = %r;
''' % (self.line_comment_prefix)
final_grammar = base_grammar + grammar_extensions
if final_grammar not in _grammar_cache:
_grammar_cache[final_grammar] = tatsu.compile(final_grammar)
return _grammar_cache[final_grammar]
def lex(self, source, name=None, filename=None):
"""Lex the given sourcecode and return a generator that yields
tokens as tuples in the form ``(lineno, token_type, value)``.

998
src/jinja2/new_parser.py Normal file
View File

@ -0,0 +1,998 @@
from tatsu.exceptions import FailedSemantics
from . import nodes
from .exceptions import TemplateSyntaxError
class JinjaSemantics(object):
def block_expression_pair(self, ast):
start_block = ast['start']
end_block = ast['end']
if start_block['name'] != end_block['name']:
raise FailedSemantics()
return ast
def line_block_expression_pair(self, ast):
return self.block_expression_pair(ast)
def lineno_from_parseinfo(parseinfo):
return parseinfo.line + 1
def parse(ast):
def merge_output(blocks):
if len(blocks) < 2:
return blocks
for idx in range(len(blocks) - 1, 0, -1):
block = blocks[idx]
previous_block = blocks[idx - 1]
if isinstance(block, nodes.Output) and isinstance(previous_block, nodes.Output):
previous_block.nodes += block.nodes
del blocks[idx]
return blocks
def merge_template_data(blocks):
for block in blocks:
if isinstance(block, nodes.Output):
if len(block.nodes) < 2:
continue
outputs = block.nodes
for idx in range(len(outputs) - 1, 0, -1):
output = outputs[idx]
previous_output = outputs[idx - 1]
if isinstance(output, nodes.TemplateData) and isinstance(previous_output, nodes.TemplateData):
previous_output.data += output.data
del outputs[idx]
return blocks
def remove_none(blocks):
return [block for block in blocks if block is not None]
if isinstance(ast, list):
blocks = [parse(item) for item in ast]
return merge_template_data(merge_output(remove_none(blocks)))
if isinstance(ast, str):
return parse_output(ast)
if 'type' in ast and ast['type'] == 'variable':
return parse_print(ast)
if 'block' in ast:
return parse_block(ast)
if 'start' in ast and 'end' in ast:
return parse_block_pair(ast)
if 'raw' in ast:
return parse_raw(ast)
if 'comment' in ast:
return parse_comment(ast)
return None
def parse_block(ast):
block_name = ast['block']['name']
if block_name == 'extends':
return parse_block_extends(ast)
if block_name == 'from':
return parse_block_from(ast)
if block_name == 'import':
return parse_block_import(ast)
if block_name == 'include':
return parse_block_include(ast)
if block_name == 'print':
return parse_block_print(ast)
if block_name == 'set':
return parse_block_set(ast)
return None
def parse_block_pair(ast):
block_name = ast['start']['name']
if block_name == 'autoescape':
return parse_block_autoescape(ast)
if block_name == 'block':
return parse_block_block(ast)
if block_name == 'call':
return parse_block_call(ast)
if block_name == 'filter':
return parse_block_filter(ast)
if block_name == 'for':
return parse_block_for(ast)
if block_name == 'if':
return parse_block_if(ast)
if block_name == 'macro':
return parse_block_macro(ast)
if block_name == 'set':
return parse_block_set(ast)
if block_name == 'with':
return parse_block_with(ast)
return None
def parse_block_autoescape(ast):
return nodes.Scope(
[nodes.ScopedEvalContextModifier(
[nodes.Keyword(
'autoescape',
parse_variable(ast['start']['parameters'][0]['value']),
lineno=lineno_from_parseinfo(ast['start']['parameters'][0]['parseinfo'])
)],
parse(ast['contents']),
lineno=lineno_from_parseinfo(ast['parseinfo'])
)],
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_block_block(ast):
name = parse_variable(ast['start']['parameters'][0]['value']).name
scoped = False
if len(ast['start']['parameters']) > 1:
scoped = ast['start']['parameters'][-1]['value']['variable'] == 'scoped'
return nodes.Block(
name,
parse(ast['contents']),
scoped,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_block_call(ast):
parameters = ast['start']['parameters']
call = parse_variable(parameters[0]['value'])
args = []
defaults = []
body = parse(ast['contents'])
if 'name_call_parameters' in ast['start']:
for arg in ast['start']['name_call_parameters']:
args.append(parse_variable(arg['value'], variable_context='param'))
return nodes.CallBlock(
call,
args,
defaults,
body,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_block_extends(ast):
return nodes.Extends(
parse_conditional_expression(ast['block']['parameters'][0]['value'])
)
def parse_block_filter(ast):
body = parse(ast['contents'])
filter_parameter = ast['start']['parameters'][0]['value']
filter_base = parse_variable(filter_parameter)
if isinstance(filter_base, nodes.Filter):
filter = filter_base
while isinstance(filter.node, nodes.Filter):
filter = filter.node
args = []
kwargs = []
dynamic_args = None
dynamic_kwargs = None
inner_filter = filter.node
if isinstance(inner_filter, nodes.Call):
args = inner_filter.args
kwargs = inner_filter.kwargs
dynamic_args = inner_filter.dyn_args
dynamic_kwargs = inner_filter.dyn_kwargs
inner_filter = inner_filter.node
inner_filter = nodes.Filter(
None,
inner_filter.name,
args,
kwargs,
dynamic_args,
dynamic_kwargs,
lineno=inner_filter.lineno
)
filter.node = inner_filter
return nodes.FilterBlock(
body,
filter_base,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_block_for(ast):
iter = None
body = ast['contents']
else_ = []
test = None
recursive = False
block_parameters = ast['start']['parameters']
target = []
for param_number, param in enumerate(block_parameters):
if param['value']['variable'] == 'in':
break
if param['value']['operator'] == 'in':
block_parameters[param_number:param_number + 1] = [
{
"value": param['value']['left']
},
{
"value": {
"variable": "in"
}
},
{
"value": param['value']['right']
},
]
target.append(
parse_variable(
param['value']['left'],
variable_context='store'
)
)
break
target.append(parse_variable(param['value'], variable_context='store'))
if len(target) == 0:
raise TemplateSyntaxError(
"expected token 'in'",
lineno=lineno_from_parseinfo(ast['start']['parseinfo'])
)
if len(target) == len(block_parameters):
raise TemplateSyntaxError(
"expected token 'in'",
lineno=target[1].lineno
)
if len(target) == 1:
target = target[0]
else:
target = nodes.Tuple(
target,
'store',
lineno=target[0].lineno
)
param_number += 2
iter = parse_variable(block_parameters[param_number]['value'])
param_number += 1
if len(block_parameters) > param_number + 1:
if block_parameters[param_number]['value']['variable'] == 'if':
param_number += 1
test = parse_conditional_expression(
block_parameters[param_number]['value']
)
param_number += 1
if len(block_parameters) > param_number + 2:
raise
if len(block_parameters) == param_number + 1:
recursive = block_parameters[param_number]['value']['variable'] == 'recursive'
else_ = _split_contents_at_block(ast['contents'], 'else')
if else_ is not None:
body, _, else_ = else_
else:
else_ = []
return nodes.For(
target, iter, parse(body), parse(else_), test, recursive,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_block_from(ast):
parameters = ast['block']['parameters']
template = parse_variable(parameters[0]['value'])
names = []
with_context = _parse_import_context(parameters)
if with_context is None:
with_context = False
else:
del parameters[-2:]
if len(parameters) > 1 and parameters[1]['value']['variable'] != 'import':
raise TemplateSyntaxError(
"Expecting 'import' but did not find it",
lineno=lineno_from_parseinfo(parameters[1]['parseinfo'])
)
if len(parameters) == 2:
raise TemplateSyntaxError(
"expected token 'name', got 'end of statement block'",
lineno=lineno_from_parseinfo(parameters[1]['parseinfo'])
)
def _variable_to_name(variable):
if isinstance(variable, str):
return variable
if 'alias' in variable:
return (
variable['variable'],
variable['alias']
)
return variable['variable']
for parameter in parameters[2:]:
if 'tuple' in parameter['value']:
for variable in parameter['value']['tuple']:
names.append(_variable_to_name(variable))
else:
names.append(_variable_to_name(parameter['value']))
from_import = nodes.FromImport(
template,
names,
with_context,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
return from_import
def parse_block_if(ast):
test = parse_conditional_expression(ast['start']['parameters'][0]['value'])
body = ast['contents']
elif_ = []
else_ = _split_contents_at_block(body, 'else')
if else_ is not None:
body, _, else_ = else_
else:
else_ = []
elif_contents = _split_contents_at_block(body, 'elif')
if elif_contents is not None:
body, _, _ = elif_contents
while elif_contents is not None:
_, elif_condition, elif_contents = elif_contents
elif_parsed = _split_contents_at_block(elif_contents, 'elif')
if elif_parsed is not None:
elif_body, _, _ = elif_parsed
else:
elif_body = elif_contents
elif_.append(
nodes.If(
parse_conditional_expression(elif_condition['block']['parameters'][0]['value']),
parse(elif_body),
[],
[],
lineno=lineno_from_parseinfo(elif_condition['parseinfo'])
)
)
elif_contents = elif_parsed
return nodes.If(
test,
parse(body),
elif_,
parse(else_),
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_block_import(ast):
block_parameters = ast['block']['parameters']
template = parse_variable(block_parameters[0]['value'])
target = None
with_context = _parse_import_context(block_parameters) or False
if len(block_parameters) > 2 and block_parameters[1]['value']['variable'] == 'as':
target = parse_variable(block_parameters[2]['value']).name
return nodes.Import(
template,
target,
with_context,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_block_include(ast):
block_parameters = ast['block']['parameters']
template = parse_conditional_expression(block_parameters[0]['value'])
with_context = _parse_import_context(block_parameters)
ignore_missing = False
if with_context is None:
with_context = True
else:
del block_parameters[-2:]
if len(block_parameters) == 3:
ignore_missing = True
if block_parameters[1]['value']['variable'] != 'ignore' and block_parameters[2]['value']['variable'] != 'missing':
raise
return nodes.Include(
template,
with_context,
ignore_missing,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_block_macro(ast):
definition = parse_variable(ast['start']['parameters'][0]['value'])
name = definition.node.name
params = []
defaults = []
body = parse(ast['contents'])
for arg in definition.args:
arg.set_ctx('param')
params.append(arg)
for kwarg in definition.kwargs:
params.append(
nodes.Name(kwarg.key, 'param')
)
defaults.append(kwarg.value)
return nodes.Macro(
name,
params,
defaults,
body,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_block_print(ast):
node = parse_variable(ast['block']['parameters'][0]['value'])
return nodes.Output([node])
def parse_block_set(ast):
if 'block' in ast:
assignment = ast['block']['parameters'][0]
if isinstance(assignment['key'], str):
key = nodes.Name(assignment['key'], 'store')
else:
key = parse_variable(assignment['key'], variable_context="store")
return nodes.Assign(
key,
parse_conditional_expression(assignment['value']),
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
elif 'start' in ast:
key = parse_variable(ast['start']['parameters'][0]['value'], variable_context="store")
filter = None
if isinstance(key, nodes.Filter):
filter = key
key = key.node
filter.node = None
return nodes.AssignBlock(
key,
filter,
parse(ast['contents']),
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
return None
def parse_block_with(ast):
with_node = nodes.With(
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
targets = []
values = []
for parameter in ast['start']['parameters']:
if 'key' not in parameter:
raise
targets.append(nodes.Name(parameter['key'], 'param'))
values.append(parse_variable(parameter['value']))
with_node.targets = targets
with_node.values = values
with_node.body = parse(ast['contents'])
return with_node
def parse_comment(ast):
return
def parse_concatenate_expression(ast):
vars = [
parse_variable(var) for var in ast['concatenate']
]
return nodes.Concat(
vars,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_conditional_expression(ast):
if 'variable' in ast:
return parse_variable(ast)
if 'literal_type' in ast:
return parse_literal(ast)
if 'concatenate' in ast:
return parse_concatenate_expression(ast)
if 'logical_operator' in ast:
return parse_conditional_expression_logical(ast)
if 'math_operator' in ast:
return parse_conditional_expression_math(ast)
if 'not' in ast:
return parse_conditional_expression_not(ast)
if 'operator' in ast:
return parse_conditional_expression_operator(ast)
if 'test_expression' in ast:
return parse_conditional_expression_if(ast)
if 'test_function' in ast:
return parse_conditional_expression_test(ast)
return None
def parse_conditional_expression_if(ast):
test = parse_conditional_expression(ast['test_expression'])
expr1 = parse_variable(ast['true_value'])
expr2 = None
if 'false_value' in ast:
expr2 = parse_variable(ast['false_value'])
return nodes.CondExpr(
test,
expr1,
expr2,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_conditional_expression_logical(ast):
node_class_map = {
'and': nodes.And,
'or': nodes.Or,
}
node_class = node_class_map[ast['logical_operator']]
return node_class(
parse_conditional_expression(ast['left']),
parse_conditional_expression(ast['right']),
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_conditional_expression_math(ast):
node_class_map = {
'+': nodes.Add,
'-': nodes.Sub,
'*': nodes.Mul,
'**': nodes.Pow,
'/': nodes.Div,
'//': nodes.FloorDiv,
'%': nodes.Mod,
}
node_class = node_class_map[ast['math_operator']]
return node_class(
parse_conditional_expression(ast['left']),
parse_conditional_expression(ast['right']),
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_conditional_expression_not(ast):
return nodes.Not(
parse_conditional_expression(ast['not']),
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_conditional_expression_operator(ast):
operand_map = {
'>': 'gt',
'>=': 'gteq',
'==': 'eq',
'!=': 'ne',
'<': 'lt',
'<=': 'lteq',
}
expr = parse_conditional_expression(ast['left'])
operator = operand_map.get(ast['operator'], ast['operator'])
operands = []
right = parse_conditional_expression(ast['right'])
if isinstance(right, nodes.Compare):
operands.append(
nodes.Operand(
operator,
right.expr
)
)
operands.extend(right.ops)
else:
operands.append(
nodes.Operand(
operator,
right
)
)
return nodes.Compare(
expr,
operands,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
def parse_conditional_expression_test(ast):
node = parse_conditional_expression(ast['test_variable'])
test_function = parse_variable(ast['test_function'])
args = []
kwargs = []
dynamic_args = None
dynamic_kwargs = None
if isinstance(test_function, nodes.Call):
call = test_function
name = call.node.name
args = call.args
kwargs = call.kwargs
dynamic_args = call.dyn_args
dynamic_kwargs = call.dyn_kwargs
elif isinstance(test_function, nodes.Const):
const_map = {
None: 'none',
True: 'true',
False: 'false',
}
name = const_map[test_function.value]
else:
name = test_function.name
if ast['test_function_parameter']:
args = [
parse_conditional_expression(ast['test_function_parameter'])
]
test_node = nodes.Test(
node,
name,
args,
kwargs,
dynamic_args,
dynamic_kwargs,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
if 'negated' in ast and ast['negated']:
test_node = nodes.Not(
test_node,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
return test_node
def parse_literal(ast):
if 'literal_type' not in ast:
raise
literal_type = ast['literal_type']
if literal_type == 'boolean':
return nodes.Const(
ast['value'],
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
elif literal_type == 'string':
return nodes.Const(
''.join(ast['value']),
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
elif literal_type == 'number':
if 'fractional' not in ast and 'exponent' not in ast:
const = int(ast['whole'])
else:
number = ast['whole']
if 'fractional' in ast:
number += '.' + ast['fractional']
if 'exponent' in ast:
number += 'e' + ast['exponent']
const = float(number)
return nodes.Const(
const,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
elif literal_type == 'dictionary':
if not ast['value']:
ast['value'] = []
items = [
nodes.Pair(
parse_literal(item['key']),
parse_variable(item['value']),
lineno=lineno_from_parseinfo(item['parseinfo'])
)
for item in ast['value']
]
return nodes.Dict(
items,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
elif literal_type == 'none':
return nodes.Const(
None,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
elif literal_type == 'list':
if not ast['value']:
ast['value'] = []
items = [
parse_variable(item) for item in ast['value']
]
return nodes.List(
items,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
elif literal_type == 'tuple':
if not ast['value']:
ast['value'] = []
items = [
parse_variable(item) for item in ast['value']
]
return nodes.Tuple(
items,
'load',
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
return None
def parse_output(ast):
return nodes.Output(
[nodes.TemplateData(ast)]
)
def parse_print(ast):
variable = ast['name']
node = parse_conditional_expression(variable)
return nodes.Output([node])
def parse_raw(ast):
return parse_output(
''.join(ast['raw'])
)
def parse_template(ast):
return nodes.Template(parse(ast), lineno=1)
def parse_variable(ast, variable_context='load'):
name = ast['variable']
if 'literal_type' in name:
node = parse_literal(name)
else:
node = nodes.Name(
name,
variable_context,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
for accessor_ast in ast['accessors']:
node = parse_variable_accessor(node, accessor_ast)
signed_node_map = {
'-': nodes.Neg,
'+': nodes.Pos,
}
if 'signed' in ast:
node_class = signed_node_map[ast['signed']]
node = node_class(node)
if ast['filters']:
for filter_ast in ast['filters']:
node = parse_variable_filter(node, filter_ast)
return node
def parse_variable_accessor(node, ast):
accessor_type = ast['accessor_type']
if accessor_type == 'brackets':
accessor_node = nodes.Getitem()
accessor_node.arg = parse_variable(ast['parameter'])
elif accessor_type == 'dot':
if isinstance(ast['parameter'], str):
accessor_node = nodes.Getattr()
accessor_node.attr = ast['parameter']
else:
accessor_node = nodes.Getitem()
accessor_node.arg = parse_literal(ast['parameter'])
elif accessor_type == 'call':
accessor_node = parse_variable_accessor_call(ast)
accessor_node.node = node
accessor_node.ctx = "load"
accessor_node.lineno = lineno_from_parseinfo(ast['parseinfo'])
return accessor_node
def parse_variable_accessor_call(ast):
args = []
kwargs = []
dynamic_args = None
dynamic_kwargs = None
if ast['parameters']:
for argument in ast['parameters']:
if dynamic_kwargs is not None:
raise TemplateSyntaxError(
'invalid syntax for function call expression',
lineno=lineno_from_parseinfo(argument['parseinfo'])
)
if 'dynamic_keyword_argument' in argument:
dynamic_kwargs = parse_variable(argument['dynamic_keyword_argument'])
continue
if dynamic_args is not None:
raise TemplateSyntaxError(
'invalid syntax for function call expression',
lineno=lineno_from_parseinfo(argument['parseinfo'])
)
if 'dynamic_argument' in argument:
dynamic_args = parse_variable(argument['dynamic_argument'])
continue
value = parse_variable(argument['value'])
if 'key' in argument:
kwargs.append(
nodes.Keyword(argument['key'], value)
)
else:
args.append(value)
node = nodes.Call()
node.args = args
node.kwargs = kwargs
node.dyn_args = dynamic_args
node.dyn_kwargs = dynamic_kwargs
return node
def parse_variable_filter(node, ast):
args = []
kwargs = []
dynamic_args = None
dynamic_kwargs = None
variable = parse_variable(ast)
filter_node = None
last_filter = None
start_variable = variable
while not isinstance(variable, nodes.Name):
if isinstance(variable, nodes.Call):
last_filter = filter_node
filter_node = variable
variable = variable.node
new_filter = nodes.Filter(
node,
variable.name,
args,
kwargs,
dynamic_args,
dynamic_kwargs,
lineno=lineno_from_parseinfo(ast['parseinfo'])
)
if filter_node is not None:
new_filter.args = filter_node.args
new_filter.kwargs = filter_node.kwargs
if last_filter is None:
return new_filter
last_filter.node = new_filter
return last_filter
def _parse_import_context(block_parameters):
if block_parameters[-1]['value']['variable'] != 'context':
return None
if block_parameters[-2]['value']['variable'] not in ['with', 'without']:
return None
return block_parameters[-2]['value']['variable'] == 'with'
def _split_contents_at_block(contents, block_name):
for index, expression in enumerate(contents):
if 'block' in expression:
if expression['block']['name'] == block_name:
return (contents[:index], expression, contents[index + 1:])
return None

View File

@ -5,6 +5,7 @@ from .exceptions import TemplateSyntaxError
from .lexer import describe_token
from .lexer import describe_token_expr
_statement_keywords = frozenset(
[
"for",
@ -40,6 +41,8 @@ class Parser:
def __init__(self, environment, source, name=None, filename=None, state=None):
self.environment = environment
self.source = source
self.grammar = environment.get_grammar()
self.stream = environment._tokenize(source, name, filename, state)
self.name = name
self.filename = filename
@ -927,8 +930,23 @@ class Parser:
return body
def parse(self):
"""Parse the whole template into a `Template` node."""
def parse_old(self):
result = nodes.Template(self.subparse(), lineno=1)
result.set_environment(self.environment)
return result
def parse(self):
"""Parse the whole template into a `Template` node."""
from .new_parser import JinjaSemantics, parse_template
result = parse_template(
self.grammar.parse(
self.source.rstrip('\n'),
whitespace='',
parseinfo=True,
semantics=JinjaSemantics(),
)
)
result.set_environment(self.environment)
return result

33
test_tatsu.py Normal file
View File

@ -0,0 +1,33 @@
from datetime import datetime
import pprint
from jinja2.environment import Environment
from jinja2.parser import Parser
with open('grammar.ebnf', 'r') as tatsu_grammar:
with open('test_template.jinja', 'r') as test_template:
template_string = test_template.read()
env = Environment(line_statement_prefix='#', line_comment_prefix='##')
parser = Parser(env, template_string)
new_parse_start = datetime.now()
new_ast = parser.parse()
new_parse_end = datetime.now()
with open('tatsu_jinja.py', 'w') as new_ast_file:
pprint.pprint(new_ast, indent=2, stream=new_ast_file)
jinja_parse_start = datetime.now()
jinja_ast = parser.parse_old()
jinja_parse_end = datetime.now()
with open('parsed_jinja.py', 'w') as jinja_ast_file:
pprint.pprint(jinja_ast, indent=2, stream=jinja_ast_file)
print("New Parser", new_parse_end - new_parse_start)
print("Jinja Parser", jinja_parse_end - jinja_parse_start)

View File

@ -194,22 +194,19 @@ class TestMeta:
i = meta.find_referenced_templates(ast)
assert list(i) == ["layout.html", "test.html", "meh.html", "muh.html"]
def test_find_included_templates(self, env):
ast = env.parse('{% include ["foo.html", "bar.html"] %}')
@pytest.mark.parametrize(
"include,templates",
(
('{% include ["foo.html", "bar.html"] %}', ["foo.html", "bar.html"]),
('{% include ("foo.html", "bar.html") %}', ["foo.html", "bar.html"]),
('{% include ["foo.html", "bar.html", foo] %}', ["foo.html", "bar.html", None]),
('{% include ("foo.html", "bar.html", foo) %}', ["foo.html", "bar.html", None])
)
)
def test_find_included_templates(self, env, include, templates):
ast = env.parse(include)
i = meta.find_referenced_templates(ast)
assert list(i) == ["foo.html", "bar.html"]
ast = env.parse('{% include ("foo.html", "bar.html") %}')
i = meta.find_referenced_templates(ast)
assert list(i) == ["foo.html", "bar.html"]
ast = env.parse('{% include ["foo.html", "bar.html", foo] %}')
i = meta.find_referenced_templates(ast)
assert list(i) == ["foo.html", "bar.html", None]
ast = env.parse('{% include ("foo.html", "bar.html", foo) %}')
i = meta.find_referenced_templates(ast)
assert list(i) == ["foo.html", "bar.html", None]
assert list(i) == templates
class TestStreaming:

View File

@ -449,8 +449,9 @@ class TestSyntax:
tmpl = env.from_string('{{ "foo"|upper + "bar"|upper }}')
assert tmpl.render() == "FOOBAR"
def test_function_calls(self, env):
tests = [
@pytest.mark.parametrize(
"should_fail,sig",
(
(True, "*foo, bar"),
(True, "*foo, *bar"),
(True, "**foo, *bar"),
@ -466,16 +467,18 @@ class TestSyntax:
(False, "*foo, **bar"),
(False, "*foo, bar=42, **baz"),
(False, "foo, *args, bar=23, **baz"),
]
for should_fail, sig in tests:
if should_fail:
with pytest.raises(TemplateSyntaxError):
env.from_string(f"{{{{ foo({sig}) }}}}")
else:
env.from_string(f"foo({sig})")
)
)
def test_function_calls(self, env, should_fail, sig):
if should_fail:
with pytest.raises(TemplateSyntaxError):
env.from_string(f"{{{{ foo({sig}) }}}}")
else:
env.from_string(f"foo({sig})")
def test_tuple_expr(self, env):
for tmpl in [
@pytest.mark.parametrize(
"tmpl",
(
"{{ () }}",
"{{ (1, 2) }}",
"{{ (1, 2,) }}",
@ -484,8 +487,10 @@ class TestSyntax:
"{% for foo, bar in seq %}...{% endfor %}",
"{% for x in foo, bar %}...{% endfor %}",
"{% for x in foo, %}...{% endfor %}",
]:
assert env.from_string(tmpl)
)
)
def test_tuple_expr(self, env, tmpl):
assert env.from_string(tmpl)
def test_trailing_comma(self, env):
tmpl = env.from_string("{{ (1, 2,) }}|{{ [1, 2,] }}|{{ {1: 2,} }}")

View File

@ -110,19 +110,25 @@ class TestSandbox:
with pytest.raises(TemplateRuntimeError):
t.render(ctx)
def test_unary_operator_intercepting(self, env):
@pytest.mark.parametrize(
"expr,ctx,rv",
(
("-1", {}, "-1"),
("-a", {"a": 2}, "-2")
)
)
def test_unary_operator_intercepting(self, env, expr, ctx, rv):
def disable_op(arg):
raise TemplateRuntimeError("that operator so does not work")
for expr, ctx, rv in ("-1", {}, "-1"), ("-a", {"a": 2}, "-2"):
env = SandboxedEnvironment()
env.unop_table["-"] = disable_op
t = env.from_string(f"{{{{ {expr} }}}}")
assert t.render(ctx) == rv
env.intercepted_unops = frozenset(["-"])
t = env.from_string(f"{{{{ {expr} }}}}")
with pytest.raises(TemplateRuntimeError):
t.render(ctx)
env = SandboxedEnvironment()
env.unop_table["-"] = disable_op
t = env.from_string(f"{{{{ {expr} }}}}")
assert t.render(ctx) == rv
env.intercepted_unops = frozenset(["-"])
t = env.from_string(f"{{{{ {expr} }}}}")
with pytest.raises(TemplateRuntimeError):
t.render(ctx)
class TestStringFormat: