|
16 | 16 | """Utility for parsing Jinja2 expressions.""" |
17 | 17 |
|
18 | 18 | from ast import literal_eval as python_literal_eval |
19 | | -from contextlib import contextmanager |
20 | | -from copy import deepcopy |
21 | 19 | import re |
22 | 20 |
|
23 | | -from cylc.flow import LOG |
24 | | -import jinja2.lexer |
25 | 21 | from jinja2.nativetypes import NativeEnvironment # type: ignore |
26 | 22 | from jinja2.nodes import ( # type: ignore |
27 | 23 | Literal, |
|
33 | 29 | ) |
34 | 30 |
|
35 | 31 |
|
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 | | - |
187 | 32 | class Parser(NativeEnvironment): |
188 | 33 |
|
189 | 34 | _LITERAL_NODES = ( |
@@ -248,11 +93,6 @@ def literal_eval(self, value): |
248 | 93 | >>> parser.literal_eval('1,\n2,\n3') |
249 | 94 | (1, 2, 3) |
250 | 95 |
|
251 | | - # back-supported jinja2 variants |
252 | | - >>> with patch_jinja2_leading_zeros(): |
253 | | - ... parser.literal_eval('042') |
254 | | - 42 |
255 | | -
|
256 | 96 | # invalid examples |
257 | 97 | >>> parser.literal_eval('1 + 1') |
258 | 98 | Traceback (most recent call last): |
|
0 commit comments