Skip to content

Commit 81d5d5f

Browse files
jinja2: remove legacy support for zero-prefixed integers
* Reverts #140, #146 * Closes #374 * Integers with leading zeros were supported by Jinja2 v2 when running under Python 2, but not when using Jinja2 v3 under Python 3. * In order to permit Cylc 7 workflows to be inter-operable with Cylc 8, a hack was put in place to extend support for zero-prefixed integers into Cylc 8 (Jinja2 v3, Python 3). * Cylc 7 compatibility mode has been removed in the 8.7 meta-release, so the hack is no longer required.
1 parent 4d41817 commit 81d5d5f

3 files changed

Lines changed: 18 additions & 181 deletions

File tree

changes.d/429.break.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Legacy support for zero-prefixed integers (e.g, `01`, `02`, `03` as opposed to `1`, `2`, `3`) has been removed. This support was dropped in Jinja2 v3.0, but support was extended in cylc-rose past this date in order to facilitate Cylc 7/8 interoperability.

cylc/rose/jinja2_parser.py

Lines changed: 0 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,8 @@
1616
"""Utility for parsing Jinja2 expressions."""
1717

1818
from ast import literal_eval as python_literal_eval
19-
from contextlib import contextmanager
20-
from copy import deepcopy
2119
import re
2220

23-
from cylc.flow import LOG
24-
import jinja2.lexer
2521
from jinja2.nativetypes import NativeEnvironment # type: ignore
2622
from jinja2.nodes import ( # type: ignore
2723
Literal,
@@ -33,157 +29,6 @@
3329
)
3430

3531

36-
def _strip_leading_zeros(string):
37-
"""Strip leading zeros from a string.
38-
39-
Examples:
40-
>>> _strip_leading_zeros('1')
41-
'1'
42-
>>> _strip_leading_zeros('01')
43-
'1'
44-
>>> _strip_leading_zeros('001')
45-
'1'
46-
>>> _strip_leading_zeros('0001')
47-
'1'
48-
>>> _strip_leading_zeros('0')
49-
'0'
50-
>>> _strip_leading_zeros('000')
51-
'0'
52-
53-
"""
54-
ret = string.lstrip('0')
55-
return ret or '0'
56-
57-
58-
def _lexer_wrap(fcn):
59-
"""Helper for patch_jinja2_leading_zeros.
60-
61-
Patches the jinja2.lexer.Lexer.wrap method.
62-
"""
63-
instances = set() # record of uses of deprecated syntax
64-
65-
def _stream(stream):
66-
"""Patch the token stream to strip the leading zero where necessary."""
67-
for lineno, token, value_str in stream:
68-
if (
69-
token == jinja2.lexer.TOKEN_INTEGER
70-
and len(value_str) > 1
71-
and value_str[0] == '0'
72-
):
73-
instances.add(value_str)
74-
yield (lineno, token, _strip_leading_zeros(value_str))
75-
76-
def _inner(
77-
self,
78-
stream, # : t.Iterable[t.Tuple[int, str, str]],
79-
name, # : t.Optional[str] = None,
80-
filename, # : t.Optional[str] = None,
81-
): # -> t.Iterator[Token]:
82-
return fcn(self, _stream(stream), name, filename)
83-
84-
_inner.__wrapped__ = fcn # save the un-patched function
85-
_inner._instances = instances # save the set of uses of deprecated syntax
86-
87-
return _inner
88-
89-
90-
@contextmanager
91-
def patch_jinja2_leading_zeros():
92-
"""Back support integers with leading zeros in Jinja2 v3.
93-
94-
Jinja2 v3 dropped support for integers with leading zeros, these are
95-
heavily used throughout Rose configurations. Since there was no deprecation
96-
warning in Jinja2 v2 we have implemented this patch to extend support for
97-
a short period to help our users to transition.
98-
99-
This patch will issue a warning if usage of the deprecated syntax is
100-
detected during the course of its usage.
101-
102-
Warning:
103-
This is a *global* patch applied to the Jinja2 library whilst the
104-
context manager is open. Do not use this with parallel/async code
105-
as the patch could apply to code outside of the context manager.
106-
107-
Examples:
108-
>>> env = NativeEnvironment()
109-
110-
The integer "1" is ok:
111-
>>> env.parse('{{ 1 }}')
112-
Template(body=[Output(nodes=[Const(value=1)])])
113-
114-
However "01" is no longer supported:
115-
>>> env.parse('{{ 01 }}')
116-
Traceback (most recent call last):
117-
jinja2.exceptions.TemplateSyntaxError: expected token ...
118-
119-
The patch returns support (the leading-zero gets stripped):
120-
>>> with patch_jinja2_leading_zeros():
121-
... env.parse('{{ 01 }}')
122-
Template(body=[Output(nodes=[Const(value=1)])])
123-
124-
The patch can handle any number of arbitrary leading zeros:
125-
>>> with patch_jinja2_leading_zeros():
126-
... env.parse('{{ 0000000001 }}')
127-
Template(body=[Output(nodes=[Const(value=1)])])
128-
129-
Once the "with" closes we go back to vanilla Jinja2 behaviour:
130-
>>> env.parse('{{ 01 }}')
131-
Traceback (most recent call last):
132-
jinja2.exceptions.TemplateSyntaxError: expected token ...
133-
134-
"""
135-
# clear any previously cashed lexer instances
136-
jinja2.lexer._lexer_cache.clear()
137-
138-
# apply the code patch (new lexer instances will pick up these changes)
139-
_integer_re = deepcopy(jinja2.lexer.integer_re)
140-
jinja2.lexer.integer_re = re.compile(
141-
rf'''
142-
# Jinja2 no longer recognises zero-padded integers as integers
143-
# so we must patch its regex to allow them to be detected.
144-
(
145-
[0-9](_?\d)* # decimal (which supports zero-padded integers)
146-
|
147-
{jinja2.lexer.integer_re.pattern}
148-
)
149-
''',
150-
re.IGNORECASE | re.VERBOSE,
151-
)
152-
jinja2.lexer.Lexer.wrap = _lexer_wrap(jinja2.lexer.Lexer.wrap)
153-
154-
# execute the body of the "with" statement
155-
try:
156-
yield
157-
finally:
158-
# report any usage of deprecated syntax
159-
if jinja2.lexer.Lexer.wrap._instances:
160-
num_examples = 5
161-
LOG.warning(
162-
'Support for integers with leading zeros (including'
163-
' lists of integers) was dropped in Jinja2 v3.'
164-
' Rose will extend support until a future version.'
165-
'\nPlease amend your Rose configuration files,'
166-
' which currently contain:'
167-
'\n * '
168-
+ (
169-
'\n * '.join(
170-
f'{before} => {_strip_leading_zeros(before)}'
171-
for before in list(
172-
jinja2.lexer.Lexer.wrap._instances
173-
)[:num_examples]
174-
)
175-
)
176-
177-
)
178-
179-
# revert the code patch
180-
jinja2.lexer.integer_re = _integer_re
181-
jinja2.lexer.Lexer.wrap = jinja2.lexer.Lexer.wrap.__wrapped__
182-
183-
# clear any patched lexers to return Jinja2 to normal operation
184-
jinja2.lexer._lexer_cache.clear()
185-
186-
18732
class Parser(NativeEnvironment):
18833

18934
_LITERAL_NODES = (
@@ -248,11 +93,6 @@ def literal_eval(self, value):
24893
>>> parser.literal_eval('1,\n2,\n3')
24994
(1, 2, 3)
25095
251-
# back-supported jinja2 variants
252-
>>> with patch_jinja2_leading_zeros():
253-
... parser.literal_eval('042')
254-
42
255-
25696
# invalid examples
25797
>>> parser.literal_eval('1 + 1')
25898
Traceback (most recent call last):

cylc/rose/utilities.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
from metomi.rose.config_tree import ConfigTree
5353
from metomi.rose.env import UnboundEnvironmentVariableError, env_var_process
5454

55-
from cylc.rose.jinja2_parser import Parser, patch_jinja2_leading_zeros
55+
from cylc.rose.jinja2_parser import Parser
5656

5757
if TYPE_CHECKING:
5858
from cylc.flow.option_parsers import Values
@@ -184,26 +184,22 @@ def process_config(
184184

185185
# Add the entire plugin_result to ROSE_SUITE_VARIABLES to allow for
186186
# programatic access.
187-
with patch_jinja2_leading_zeros():
188-
# BACK COMPAT: patch_jinja2_leading_zeros
189-
# back support zero-padded integers for a limited time to help
190-
# users migrate before upgrading cylc-flow to Jinja2>=3.1
191-
parser = Parser()
192-
for key, value in plugin_result['template_variables'].items():
193-
# The special variables are already Python variables.
194-
if key not in ['ROSE_ORIG_HOST', 'ROSE_VERSION', 'ROSE_SITE']:
195-
try:
196-
plugin_result['template_variables'][key] = (
197-
parser.literal_eval(value)
198-
)
199-
except Exception:
200-
raise ConfigProcessError(
201-
[templating, key],
202-
value,
203-
f'Invalid template variable: {value}'
204-
'\nMust be a valid Python or Jinja2 literal'
205-
' (note strings "must be quoted").',
206-
) from None
187+
parser = Parser()
188+
for key, value in plugin_result['template_variables'].items():
189+
# The special variables are already Python variables.
190+
if key not in ['ROSE_ORIG_HOST', 'ROSE_VERSION', 'ROSE_SITE']:
191+
try:
192+
plugin_result['template_variables'][key] = (
193+
parser.literal_eval(value)
194+
)
195+
except Exception:
196+
raise ConfigProcessError(
197+
[templating, key],
198+
value,
199+
f'Invalid template variable: {value}'
200+
'\nMust be a valid Python or Jinja2 literal'
201+
' (note strings "must be quoted").',
202+
) from None
207203

208204
# Add ROSE_SUITE_VARIABLES to plugin_result of templating engines in use.
209205
plugin_result['template_variables']['ROSE_SUITE_VARIABLES'] = (

0 commit comments

Comments
 (0)