diff --git a/cylc/flow/parsec/jinja2support.py b/cylc/flow/parsec/jinja2support.py index 7156a8e0ee0..43dcdc99aef 100644 --- a/cylc/flow/parsec/jinja2support.py +++ b/cylc/flow/parsec/jinja2support.py @@ -19,6 +19,7 @@ """ from contextlib import suppress +from functools import lru_cache, wraps from glob import glob import importlib import os @@ -51,6 +52,65 @@ CONTEXT_LINES = 3 +def restore_deprecated_interfaces(): + """Extend support for some deprecated interfaces in Jinja2 3.1.x. + + Jinja2 renamed a bunch of interfaces. Warnings were added in version 3.0.0, + support for the old names was removed in 3.1.0, however, our users did not + notice these warnings (you have to manually turn them on) so did not take + action. + + https://jinja.palletsprojects.com/en/stable/changes/#version-3-1-0 + + In https://github.com/cylc/cylc-flow/pull/7365 (8.6.6) we've captured + Jinja2 warnings and turned them into Cylc warnings to ensure they cannot be + missed. + + To give users a chance to take action, this patch restores the renamed + interfaces just for ONE minor Cylc version. + + BACK COMPAT: restore_deprecated_interfaces + FROM: 8.6.x + TO: 8.7.0 + REMOVE AT: 8.8.0 + """ + renamed_interfaces = { + 'contextfilter': 'pass_context', + 'contextfunction': 'pass_context', + 'evalcontextfilter': 'pass_eval_context', + 'evalcontextfunction': 'pass_eval_context', + 'environmentfilter': 'pass_environment', + 'environmentfunction': 'pass_environment', + } + url = 'https://jinja.palletsprojects.com/en/stable/changes/#version-3-1-0' + + @lru_cache # suppress duplicate warnings + def _log_warning(old, new): + LOG.warning( + f'The Jinja2 function {old} was renamed to {new}.' + f'\nCylc has extended support for {old} until 8.8.0.' + f' Please search your workflow for {old} and upgrade any uses.' + f'\nSee {url}' + ) + + def _warn(fcn, old, new): + @wraps(fcn) + def _inner(*args, **kwargs): + _log_warning(old, new) + return fcn(*args, **kwargs) + return _inner + + import jinja2 + import jinja2.filters + for old, new in renamed_interfaces.items(): + setattr(jinja2, old, _warn(getattr(jinja2, new), old, new)) + setattr(jinja2.filters, old, _warn(getattr(jinja2, new), old, new)) + + +# BACK COMPAT +restore_deprecated_interfaces() + + class PyModuleLoader(BaseLoader): """Load python module as Jinja2 template. diff --git a/tests/unit/parsec/test_jinja2support.py b/tests/unit/parsec/test_jinja2support.py index 5f1d286a27b..d6eb8e78339 100644 --- a/tests/unit/parsec/test_jinja2support.py +++ b/tests/unit/parsec/test_jinja2support.py @@ -30,6 +30,7 @@ jinja2process, raise_helper, ) +from cylc.flow.util import sstrip def test_raise_helper(): @@ -137,3 +138,40 @@ def test_pymoduleloader_invalid_module(tmp_path): module_loader = PyModuleLoader() with pytest.raises(jinja2.TemplateNotFound): module_loader.load(environment=env, name='no way jose') + + +def test_restore_deprecated_interfaces(tmp_path, log_filter): + """It should log warnings when back-supported Jinja2 interfaces are used. + + See https://github.com/cylc/cylc-flow/pull/7325 + """ + # flow.cylc + flow_cylc = tmp_path / 'flow.cylc' + flow_cylc.write_text(sstrip(''' + #!Jinja2 + # {{ 1 | foo }} + ''')) + + # Jinja2Filters/foo.py + (tmp_path / 'Jinja2Filters').mkdir() + ((tmp_path / 'Jinja2Filters') / 'foo.py').write_text(sstrip(''' + from jinja2 import contextfilter + + @contextfilter # NOTE: contextfilter is deprecated! + def foo(x, y): + return y * 2 + ''' + '\n')) + + # process Jinja2 template + assert jinja2process( + flow_cylc, + flow_cylc.read_text().splitlines(), + str(tmp_path), + ) == ['# 2'] + + # warning should be raised + assert log_filter( + contains=( + 'The Jinja2 function contextfilter was renamed to pass_context' + ) + )