summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArmin Ronacher <armin.ronacher@active-4.com>2017-01-12 20:10:58 +0100
committerArmin Ronacher <armin.ronacher@active-4.com>2017-01-12 20:10:58 +0100
commitc6ddeb7d5f64789ed0bbc1ffef8596a79bc06fd9 (patch)
treeb7a49e459aee4210eede5042a57339b9bae9650a
parent9574abe564031c7f2034bfce22ed59a39d23948b (diff)
downloadjinja2-c6ddeb7d5f64789ed0bbc1ffef8596a79bc06fd9.tar.gz
Add support back for custom resolves on old resolve method
-rw-r--r--CHANGES3
-rw-r--r--jinja2/runtime.py68
-rw-r--r--tests/test_regression.py15
3 files changed, 76 insertions, 10 deletions
diff --git a/CHANGES b/CHANGES
index ed9bd0e..d7fa6b1 100644
--- a/CHANGES
+++ b/CHANGES
@@ -7,6 +7,9 @@ Version 2.9.5
- Restored the original repr of the internal `_GroupTuple` because this
caused issues with ansible and it was an unintended change. (#654)
+- Added back support for custom contexts that override the old `resolve`
+ method since it was hard for people to spot that this could cause a
+ regression.
Version 2.9.4
-------------
diff --git a/jinja2/runtime.py b/jinja2/runtime.py
index 03e303f..f39e89b 100644
--- a/jinja2/runtime.py
+++ b/jinja2/runtime.py
@@ -17,7 +17,8 @@ from jinja2.utils import Markup, soft_unicode, escape, missing, concat, \
from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \
TemplateNotFound
from jinja2._compat import imap, text_type, iteritems, \
- implements_iterator, implements_to_string, string_types, PY2
+ implements_iterator, implements_to_string, string_types, PY2, \
+ with_metaclass
# these variables are exported to the template runtime
@@ -90,7 +91,43 @@ class TemplateReference(object):
)
-class Context(object):
+def _get_func(x):
+ return getattr(x, '__func__', x)
+
+
+class ContextMeta(type):
+
+ def __new__(cls, name, bases, d):
+ rv = type.__new__(cls, name, bases, d)
+ if bases == ():
+ return rv
+
+ resolve = _get_func(rv.resolve)
+ default_resolve = _get_func(Context.resolve)
+ resolve_or_missing = _get_func(rv.resolve_or_missing)
+ default_resolve_or_missing = _get_func(Context.resolve_or_missing)
+
+ # If we have a changed resolve but no changed default or missing
+ # resolve we invert the call logic.
+ if resolve is not default_resolve and \
+ resolve_or_missing is default_resolve_or_missing:
+ rv._legacy_resolve_mode = True
+ elif resolve is default_resolve and \
+ resolve_or_missing is default_resolve_or_missing:
+ rv._fast_resolve_mode = True
+
+ return rv
+
+
+def resolve_or_missing(context, key, missing=missing):
+ if key in context.vars:
+ return context.vars[key]
+ if key in context.parent:
+ return context.parent[key]
+ return missing
+
+
+class Context(with_metaclass(ContextMeta)):
"""The template context holds the variables of a template. It stores the
values passed to the template and also the names the template exports.
Creating instances is neither supported nor useful as it's created
@@ -109,8 +146,10 @@ class Context(object):
method that doesn't fail with a `KeyError` but returns an
:class:`Undefined` object for missing variables.
"""
- __slots__ = ('parent', 'vars', 'environment', 'eval_ctx', 'exported_vars',
- 'name', 'blocks', '__weakref__')
+ # XXX: we want to eventually make this be a deprecation warning and
+ # remove it.
+ _legacy_resolve_mode = False
+ _fast_resolve_mode = False
def __init__(self, environment, parent, name, blocks):
self.parent = parent
@@ -125,6 +164,11 @@ class Context(object):
# from the template.
self.blocks = dict((k, [v]) for k, v in iteritems(blocks))
+ # In case we detect the fast resolve mode we can set up an alias
+ # here that bypasses the legacy code logic.
+ if self._fast_resolve_mode:
+ self.resolve_or_missing = resolve_or_missing
+
def super(self, name, current):
"""Render a parent block."""
try:
@@ -150,7 +194,10 @@ class Context(object):
"""Looks up a variable like `__getitem__` or `get` but returns an
:class:`Undefined` object with the name of the name looked up.
"""
- rv = self.resolve_or_missing(key)
+ if self._legacy_resolve_mode:
+ rv = resolve_or_missing(self, key)
+ else:
+ rv = self.resolve_or_missing(key)
if rv is missing:
return self.environment.undefined(name=key)
return rv
@@ -159,11 +206,12 @@ class Context(object):
"""Resolves a variable like :meth:`resolve` but returns the
special `missing` value if it cannot be found.
"""
- if key in self.vars:
- return self.vars[key]
- if key in self.parent:
- return self.parent[key]
- return missing
+ if self._legacy_resolve_mode:
+ rv = self.resolve(key)
+ if isinstance(rv, Undefined):
+ rv = missing
+ return rv
+ return resolve_or_missing(self, key)
def get_exported(self):
"""Get a new dict with the exported variables."""
diff --git a/tests/test_regression.py b/tests/test_regression.py
index 0b81f92..e6a2f0c 100644
--- a/tests/test_regression.py
+++ b/tests/test_regression.py
@@ -505,3 +505,18 @@ class TestBug(object):
assert t.list == [1, 2]
assert repr(t) == "('foo', [1, 2])"
assert str(t) == "('foo', [1, 2])"
+
+ def test_legacy_custom_context(self, env):
+ from jinja2.runtime import Context, Undefined, missing
+
+ class MyContext(Context):
+ def resolve(self, name):
+ if name == 'foo':
+ return 42
+ return super(MyContext, self).resolve(name)
+
+ x = MyContext(env, parent={'bar': 23}, name='foo', blocks={})
+ assert x._legacy_resolve_mode
+ assert x.resolve_or_missing('foo') == 42
+ assert x.resolve_or_missing('bar') == 23
+ assert x.resolve_or_missing('baz') is missing