diff options
-rw-r--r-- | CHANGES.rst | 27 | ||||
-rw-r--r-- | docs/api.rst | 66 | ||||
-rw-r--r-- | src/jinja2/asyncfilters.py | 9 | ||||
-rw-r--r-- | src/jinja2/compiler.py | 12 | ||||
-rw-r--r-- | src/jinja2/debug.py | 5 | ||||
-rw-r--r-- | src/jinja2/environment.py | 6 | ||||
-rw-r--r-- | src/jinja2/filters.py | 2 | ||||
-rw-r--r-- | src/jinja2/lexer.py | 8 | ||||
-rw-r--r-- | src/jinja2/loaders.py | 162 | ||||
-rw-r--r-- | src/jinja2/nodes.py | 6 | ||||
-rw-r--r-- | src/jinja2/runtime.py | 6 | ||||
-rw-r--r-- | src/jinja2/utils.py | 13 | ||||
-rw-r--r-- | tests/conftest.py | 44 | ||||
-rw-r--r-- | tests/res/package.zip | bin | 1036 -> 0 bytes | |||
-rw-r--r-- | tests/test_api.py | 25 | ||||
-rw-r--r-- | tests/test_async.py | 16 | ||||
-rw-r--r-- | tests/test_bytecode_cache.py | 1 | ||||
-rw-r--r-- | tests/test_core_tags.py | 10 | ||||
-rw-r--r-- | tests/test_debug.py | 1 | ||||
-rw-r--r-- | tests/test_ext.py | 5 | ||||
-rw-r--r-- | tests/test_filters.py | 8 | ||||
-rw-r--r-- | tests/test_imports.py | 3 | ||||
-rw-r--r-- | tests/test_inheritance.py | 2 | ||||
-rw-r--r-- | tests/test_lexnparse.py | 28 | ||||
-rw-r--r-- | tests/test_loader.py | 55 | ||||
-rw-r--r-- | tests/test_regression.py | 2 | ||||
-rw-r--r-- | tests/test_runtime.py | 19 | ||||
-rw-r--r-- | tests/test_security.py | 3 | ||||
-rw-r--r-- | tests/test_tests.py | 1 | ||||
-rw-r--r-- | tests/test_utils.py | 8 |
30 files changed, 214 insertions, 339 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 51f4984..579b80b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,32 @@ .. currentmodule:: jinja2 +2.11.2 +------ + +Unreleased + +- Fix a bug that caused callable objects with ``__getattr__``, like + :class:`~unittest.mock.Mock` to be treated as a + :func:`contextfunction`. :issue:`1145` +- Update ``wordcount`` filter to trigger :class:`Undefined` methods + by wrapping the input in :func:`soft_unicode`. :pr:`1160` +- Fix a hang when displaying tracebacks on Python 32-bit. + :issue:`1162` +- Showing an undefined error for an object that raises + ``AttributeError`` on access doesn't cause a recursion error. + :issue:`1177` +- Revert changes to :class:`~loaders.PackageLoader` from 2.10 which + removed the dependency on setuptools and pkg_resources, and added + limited support for namespace packages. The changes caused issues + when using Pytest. Due to the difficulty in supporting Python 2 and + :pep:`451` simultaneously, the changes are reverted until 3.0. + :pr:`1182` +- Fix line numbers in error messages when newlines are stripped. + :pr:`1178` +- The special ``namespace()`` assignment object in templates works in + async environments. :issue:`1180` + + Version 2.11.1 -------------- diff --git a/docs/api.rst b/docs/api.rst index 871b326..40f9849 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5,9 +5,10 @@ API :noindex: :synopsis: public Jinja API -This document describes the API to Jinja and not the template language. It -will be most useful as reference to those implementing the template interface -to the application and not those who are creating Jinja templates. +This document describes the API to Jinja and not the template language +(for that, see :doc:`/templates`). It will be most useful as reference +to those implementing the template interface to the application and not +those who are creating Jinja templates. Basics ------ @@ -529,37 +530,38 @@ Builtin bytecode caches: Async Support ------------- -Starting with version 2.9, Jinja also supports the Python `async` and -`await` constructs. As far as template designers go this feature is -entirely opaque to them however as a developer you should be aware of how -it's implemented as it influences what type of APIs you can safely expose -to the template environment. - -First you need to be aware that by default async support is disabled as -enabling it will generate different template code behind the scenes which -passes everything through the asyncio event loop. This is important to -understand because it has some impact to what you are doing: - -* template rendering will require an event loop to be set for the - current thread (``asyncio.get_event_loop`` needs to return one) -* all template generation code internally runs async generators which - means that you will pay a performance penalty even if the non sync - methods are used! -* The sync methods are based on async methods if the async mode is - enabled which means that `render` for instance will internally invoke - `render_async` and run it as part of the current event loop until the - execution finished. +.. versionadded:: 2.9 + +Jinja supports the Python ``async`` and ``await`` syntax. For the +template designer, this support (when enabled) is entirely transparent, +templates continue to look exactly the same. However, developers should +be aware of the implementation as it affects what types of APIs you can +use. + +By default, async support is disabled. Enabling it will cause the +environment to compile different code behind the scenes in order to +handle async and sync code in an asyncio event loop. This has the +following implications: + +- Template rendering requires an event loop to be available to the + current thread. :func:`asyncio.get_event_loop` must return an event + loop. +- The compiled code uses ``await`` for functions and attributes, and + uses ``async for`` loops. In order to support using both async and + sync functions in this context, a small wrapper is placed around + all calls and access, which add overhead compared to purely async + code. +- Sync methods and filters become wrappers around their corresponding + async implementations where needed. For example, ``render`` invokes + ``async_render``, and ``|map`` supports async iterables. Awaitable objects can be returned from functions in templates and any -function call in a template will automatically await the result. This -means that you can provide a method that asynchronously loads data -from a database if you so desire and from the template designer's point of -view this is just another function they can call. This means that the -``await`` you would normally issue in Python is implied. However this -only applies to function calls. If an attribute for instance would be an -awaitable object then this would not result in the expected behavior. - -Likewise iterations with a `for` loop support async iterators. +function call in a template will automatically await the result. The +``await`` you would normally add in Python is implied. For example, you +can provide a method that asynchronously loads data from a database, and +from the template designer's point of view it can be called like any +other function. + .. _policies: diff --git a/src/jinja2/asyncfilters.py b/src/jinja2/asyncfilters.py index d29f6c6..3d98dbc 100644 --- a/src/jinja2/asyncfilters.py +++ b/src/jinja2/asyncfilters.py @@ -26,17 +26,16 @@ async def async_select_or_reject(args, kwargs, modfunc, lookup_attr): def dualfilter(normal_filter, async_filter): wrap_evalctx = False - if getattr(normal_filter, "environmentfilter", False): + if getattr(normal_filter, "environmentfilter", False) is True: def is_async(args): return args[0].is_async wrap_evalctx = False else: - if not getattr(normal_filter, "evalcontextfilter", False) and not getattr( - normal_filter, "contextfilter", False - ): - wrap_evalctx = True + has_evalctxfilter = getattr(normal_filter, "evalcontextfilter", False) is True + has_ctxfilter = getattr(normal_filter, "contextfilter", False) is True + wrap_evalctx = not has_evalctxfilter and not has_ctxfilter def is_async(args): return args[0].environment.is_async diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py index f450ec6..63297b4 100644 --- a/src/jinja2/compiler.py +++ b/src/jinja2/compiler.py @@ -1307,13 +1307,13 @@ class CodeGenerator(NodeVisitor): def finalize(value): return default(env_finalize(value)) - if getattr(env_finalize, "contextfunction", False): + if getattr(env_finalize, "contextfunction", False) is True: src += "context, " finalize = None # noqa: F811 - elif getattr(env_finalize, "evalcontextfunction", False): + elif getattr(env_finalize, "evalcontextfunction", False) is True: src += "context.eval_ctx, " finalize = None - elif getattr(env_finalize, "environmentfunction", False): + elif getattr(env_finalize, "environmentfunction", False) is True: src += "environment, " def finalize(value): @@ -1689,11 +1689,11 @@ class CodeGenerator(NodeVisitor): func = self.environment.filters.get(node.name) if func is None: self.fail("no filter named %r" % node.name, node.lineno) - if getattr(func, "contextfilter", False): + if getattr(func, "contextfilter", False) is True: self.write("context, ") - elif getattr(func, "evalcontextfilter", False): + elif getattr(func, "evalcontextfilter", False) is True: self.write("context.eval_ctx, ") - elif getattr(func, "environmentfilter", False): + elif getattr(func, "environmentfilter", False) is True: self.write("environment, ") # if the filter node is None we are inside a filter block diff --git a/src/jinja2/debug.py b/src/jinja2/debug.py index d2c5a06..5d8aec3 100644 --- a/src/jinja2/debug.py +++ b/src/jinja2/debug.py @@ -245,10 +245,7 @@ else: class _CTraceback(ctypes.Structure): _fields_ = [ # Extra PyObject slots when compiled with Py_TRACE_REFS. - ( - "PyObject_HEAD", - ctypes.c_byte * (32 if hasattr(sys, "getobjects") else 16), - ), + ("PyObject_HEAD", ctypes.c_byte * object().__sizeof__()), # Only care about tb_next as an object, not a traceback. ("tb_next", ctypes.py_object), ] diff --git a/src/jinja2/environment.py b/src/jinja2/environment.py index bf44b9d..8430390 100644 --- a/src/jinja2/environment.py +++ b/src/jinja2/environment.py @@ -492,20 +492,20 @@ class Environment(object): if func is None: fail_for_missing_callable("no filter named %r", name) args = [value] + list(args or ()) - if getattr(func, "contextfilter", False): + if getattr(func, "contextfilter", False) is True: if context is None: raise TemplateRuntimeError( "Attempted to invoke context filter without context" ) args.insert(0, context) - elif getattr(func, "evalcontextfilter", False): + elif getattr(func, "evalcontextfilter", False) is True: if eval_ctx is None: if context is not None: eval_ctx = context.eval_ctx else: eval_ctx = EvalContext(self) args.insert(0, eval_ctx) - elif getattr(func, "environmentfilter", False): + elif getattr(func, "environmentfilter", False) is True: args.insert(0, self) return func(*args, **(kwargs or {})) diff --git a/src/jinja2/filters.py b/src/jinja2/filters.py index 1af7ac8..9741567 100644 --- a/src/jinja2/filters.py +++ b/src/jinja2/filters.py @@ -761,7 +761,7 @@ def do_wordwrap( def do_wordcount(s): """Count the words in that string.""" - return len(_word_re.findall(s)) + return len(_word_re.findall(soft_unicode(s))) def do_int(value, default=0, base=10): diff --git a/src/jinja2/lexer.py b/src/jinja2/lexer.py index a2b44e9..5fa940d 100644 --- a/src/jinja2/lexer.py +++ b/src/jinja2/lexer.py @@ -681,6 +681,7 @@ class Lexer(object): source_length = len(source) balancing_stack = [] lstrip_unless_re = self.lstrip_unless_re + newlines_stripped = 0 while 1: # tokenizer loop @@ -717,7 +718,9 @@ class Lexer(object): if strip_sign == "-": # Strip all whitespace between the text and the tag. - groups = (text.rstrip(),) + groups[1:] + stripped = text.rstrip() + newlines_stripped = text[len(stripped) :].count("\n") + groups = (stripped,) + groups[1:] elif ( # Not marked for preserving whitespace. strip_sign != "+" @@ -758,7 +761,8 @@ class Lexer(object): data = groups[idx] if data or token not in ignore_if_empty: yield lineno, token, data - lineno += data.count("\n") + lineno += data.count("\n") + newlines_stripped + newlines_stripped = 0 # strings as token just are yielded as it. else: diff --git a/src/jinja2/loaders.py b/src/jinja2/loaders.py index ce5537a..457c4b5 100644 --- a/src/jinja2/loaders.py +++ b/src/jinja2/loaders.py @@ -3,11 +3,9 @@ sources. """ import os -import pkgutil import sys import weakref from hashlib import sha1 -from importlib import import_module from os import path from types import ModuleType @@ -217,141 +215,75 @@ class FileSystemLoader(BaseLoader): class PackageLoader(BaseLoader): - """Load templates from a directory in a Python package. + """Load templates from python eggs or packages. It is constructed with + the name of the python package and the path to the templates in that + package:: - :param package_name: Import name of the package that contains the - template directory. - :param package_path: Directory within the imported package that - contains the templates. - :param encoding: Encoding of template files. + loader = PackageLoader('mypackage', 'views') - The following example looks up templates in the ``pages`` directory - within the ``project.ui`` package. + If the package path is not given, ``'templates'`` is assumed. - .. code-block:: python - - loader = PackageLoader("project.ui", "pages") - - Only packages installed as directories (standard pip behavior) or - zip/egg files (less common) are supported. The Python API for - introspecting data in packages is too limited to support other - installation methods the way this loader requires. - - There is limited support for :pep:`420` namespace packages. The - template directory is assumed to only be in one namespace - contributor. Zip files contributing to a namespace are not - supported. - - .. versionchanged:: 2.11.0 - No longer uses ``setuptools`` as a dependency. - - .. versionchanged:: 2.11.0 - Limited PEP 420 namespace package support. + Per default the template encoding is ``'utf-8'`` which can be changed + by setting the `encoding` parameter to something else. Due to the nature + of eggs it's only possible to reload templates if the package was loaded + from the file system and not a zip file. """ def __init__(self, package_name, package_path="templates", encoding="utf-8"): - if package_path == os.path.curdir: - package_path = "" - elif package_path[:2] == os.path.curdir + os.path.sep: - package_path = package_path[2:] + from pkg_resources import DefaultProvider + from pkg_resources import get_provider + from pkg_resources import ResourceManager - package_path = os.path.normpath(package_path).rstrip(os.path.sep) - self.package_path = package_path - self.package_name = package_name + provider = get_provider(package_name) self.encoding = encoding - - # Make sure the package exists. This also makes namespace - # packages work, otherwise get_loader returns None. - import_module(package_name) - self._loader = loader = pkgutil.get_loader(package_name) - - # Zip loader's archive attribute points at the zip. - self._archive = getattr(loader, "archive", None) - self._template_root = None - - if hasattr(loader, "get_filename"): - # A standard directory package, or a zip package. - self._template_root = os.path.join( - os.path.dirname(loader.get_filename(package_name)), package_path - ) - elif hasattr(loader, "_path"): - # A namespace package, limited support. Find the first - # contributor with the template directory. - for root in loader._path: - root = os.path.join(root, package_path) - - if os.path.isdir(root): - self._template_root = root - break - - if self._template_root is None: - raise ValueError( - "The %r package was not installed in a way that" - " PackageLoader understands." % package_name - ) + self.manager = ResourceManager() + self.filesystem_bound = isinstance(provider, DefaultProvider) + self.provider = provider + self.package_path = package_path def get_source(self, environment, template): - p = os.path.join(self._template_root, *split_template_path(template)) + pieces = split_template_path(template) + p = "/".join((self.package_path,) + tuple(pieces)) - if self._archive is None: - # Package is a directory. - if not os.path.isfile(p): - raise TemplateNotFound(template) + if not self.provider.has_resource(p): + raise TemplateNotFound(template) - with open(p, "rb") as f: - source = f.read() + filename = uptodate = None - mtime = os.path.getmtime(p) + if self.filesystem_bound: + filename = self.provider.get_resource_filename(self.manager, p) + mtime = path.getmtime(filename) - def up_to_date(): - return os.path.isfile(p) and os.path.getmtime(p) == mtime + def uptodate(): + try: + return path.getmtime(filename) == mtime + except OSError: + return False - else: - # Package is a zip file. - try: - source = self._loader.get_data(p) - except OSError: - raise TemplateNotFound(template) + source = self.provider.get_resource_string(self.manager, p) + return source.decode(self.encoding), filename, uptodate - # Could use the zip's mtime for all template mtimes, but - # would need to safely reload the module if it's out of - # date, so just report it as always current. - up_to_date = None + def list_templates(self): + path = self.package_path - return source.decode(self.encoding), p, up_to_date + if path[:2] == "./": + path = path[2:] + elif path == ".": + path = "" - def list_templates(self): + offset = len(path) results = [] - if self._archive is None: - # Package is a directory. - offset = len(self._template_root) - - for dirpath, _, filenames in os.walk(self._template_root): - dirpath = dirpath[offset:].lstrip(os.path.sep) - results.extend( - os.path.join(dirpath, name).replace(os.path.sep, "/") - for name in filenames - ) - else: - if not hasattr(self._loader, "_files"): - raise TypeError( - "This zip import does not have the required" - " metadata to list templates." - ) - - # Package is a zip file. - prefix = ( - self._template_root[len(self._archive) :].lstrip(os.path.sep) - + os.path.sep - ) - offset = len(prefix) + def _walk(path): + for filename in self.provider.resource_listdir(path): + fullname = path + "/" + filename - for name in self._loader._files.keys(): - # Find names under the templates directory that aren't directories. - if name.startswith(prefix) and name[-1] != os.path.sep: - results.append(name[offset:].replace(os.path.sep, "/")) + if self.provider.resource_isdir(fullname): + _walk(fullname) + else: + results.append(fullname[offset:].lstrip("/")) + _walk(path) results.sort() return results diff --git a/src/jinja2/nodes.py b/src/jinja2/nodes.py index 9f3edc0..95bd614 100644 --- a/src/jinja2/nodes.py +++ b/src/jinja2/nodes.py @@ -671,7 +671,7 @@ class Filter(Expr): # python 3. because of that, do not rename filter_ to filter! filter_ = self.environment.filters.get(self.name) - if filter_ is None or getattr(filter_, "contextfilter", False): + if filter_ is None or getattr(filter_, "contextfilter", False) is True: raise Impossible() # We cannot constant handle async filters, so we need to make sure @@ -684,9 +684,9 @@ class Filter(Expr): args, kwargs = args_as_const(self, eval_ctx) args.insert(0, self.node.as_const(eval_ctx)) - if getattr(filter_, "evalcontextfilter", False): + if getattr(filter_, "evalcontextfilter", False) is True: args.insert(0, eval_ctx) - elif getattr(filter_, "environmentfilter", False): + elif getattr(filter_, "environmentfilter", False) is True: args.insert(0, self.environment) try: diff --git a/src/jinja2/runtime.py b/src/jinja2/runtime.py index 527d4b5..3ad7968 100644 --- a/src/jinja2/runtime.py +++ b/src/jinja2/runtime.py @@ -280,11 +280,11 @@ class Context(with_metaclass(ContextMeta)): break if callable(__obj): - if getattr(__obj, "contextfunction", 0): + if getattr(__obj, "contextfunction", False) is True: args = (__self,) + args - elif getattr(__obj, "evalcontextfunction", 0): + elif getattr(__obj, "evalcontextfunction", False) is True: args = (__self.eval_ctx,) + args - elif getattr(__obj, "environmentfunction", 0): + elif getattr(__obj, "environmentfunction", False) is True: args = (__self.environment,) + args try: return __obj(*args, **kwargs) diff --git a/src/jinja2/utils.py b/src/jinja2/utils.py index e3285e8..b422ba9 100644 --- a/src/jinja2/utils.py +++ b/src/jinja2/utils.py @@ -165,11 +165,15 @@ def object_type_repr(obj): return "None" elif obj is Ellipsis: return "Ellipsis" + + cls = type(obj) + # __builtin__ in 2.x, builtins in 3.x - if obj.__class__.__module__ in ("__builtin__", "builtins"): - name = obj.__class__.__name__ + if cls.__module__ in ("__builtin__", "builtins"): + name = cls.__name__ else: - name = obj.__class__.__module__ + "." + obj.__class__.__name__ + name = cls.__module__ + "." + cls.__name__ + return "%s object" % name @@ -693,7 +697,8 @@ class Namespace(object): self.__attrs = dict(*args, **kwargs) def __getattribute__(self, name): - if name == "_Namespace__attrs": + # __class__ is needed for the awaitable check in async mode + if name in {"_Namespace__attrs", "__class__"}: return object.__getattribute__(self, name) try: return self.__attrs[name] diff --git a/tests/conftest.py b/tests/conftest.py index bb26409..23088a3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,50 +14,6 @@ def pytest_ignore_collect(path): return False -def pytest_configure(config): - """Register custom marks for test categories.""" - custom_markers = [ - "api", - "byte_code_cache", - "core_tags", - "debug", - "escapeUrlizeTarget", - "ext", - "extended", - "filesystemloader", - "filter", - "for_loop", - "helpers", - "if_condition", - "imports", - "includes", - "inheritance", - "lexer", - "lexnparse", - "loaders", - "loremIpsum", - "lowlevel", - "lrucache", - "lstripblocks", - "macros", - "meta", - "moduleloader", - "parser", - "regression", - "sandbox", - "set", - "streaming", - "syntax", - "test_tests", - "tokenstream", - "undefined", - "utils", - "with_", - ] - for mark in custom_markers: - config.addinivalue_line("markers", mark + ": test category") - - @pytest.fixture def env(): """returns a new environment.""" diff --git a/tests/res/package.zip b/tests/res/package.zip Binary files differdeleted file mode 100644 index d4c9ce9..0000000 --- a/tests/res/package.zip +++ /dev/null diff --git a/tests/test_api.py b/tests/test_api.py index 0c262dc..7a1cae8 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -25,8 +25,6 @@ from jinja2.utils import environmentfunction from jinja2.utils import evalcontextfunction -@pytest.mark.api -@pytest.mark.extended class TestExtendedAPI(object): def test_item_and_attribute(self, env): from jinja2.sandbox import SandboxedEnvironment @@ -163,8 +161,6 @@ class TestExtendedAPI(object): t.render(total=MAX_RANGE + 1) -@pytest.mark.api -@pytest.mark.meta class TestMeta(object): def test_find_undeclared_variables(self, env): ast = env.parse("{% set foo = 42 %}{{ bar + foo }}") @@ -218,8 +214,6 @@ class TestMeta(object): assert list(i) == ["foo.html", "bar.html", None] -@pytest.mark.api -@pytest.mark.streaming class TestStreaming(object): def test_basic_streaming(self, env): t = env.from_string( @@ -261,8 +255,6 @@ class TestStreaming(object): shutil.rmtree(tmp) -@pytest.mark.api -@pytest.mark.undefined class TestUndefined(object): def test_stopiteration_is_undefined(self): def test(): @@ -277,6 +269,21 @@ class TestUndefined(object): with pytest.raises(AttributeError): Undefined("Foo").__dict__ + def test_undefined_attribute_error(self): + # Django's LazyObject turns the __class__ attribute into a + # property that resolves the wrapped function. If that wrapped + # function raises an AttributeError, printing the repr of the + # object in the undefined message would cause a RecursionError. + class Error(object): + @property + def __class__(self): + raise AttributeError() + + u = Undefined(obj=Error(), name="hello") + + with pytest.raises(UndefinedError): + getattr(u, "recursion", None) + def test_logging_undefined(self): _messages = [] @@ -400,8 +407,6 @@ class TestUndefined(object): Undefined(obj=42, name="upper")() -@pytest.mark.api -@pytest.mark.lowlevel class TestLowLevel(object): def test_custom_code_generator(self): class CustomCodeGenerator(CodeGenerator): diff --git a/tests/test_async.py b/tests/test_async.py index d6c4a23..2b9974e 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -130,7 +130,6 @@ def test_env_async(): return env -@pytest.mark.imports class TestAsyncImports(object): def test_context_imports(self, test_env_async): t = test_env_async.from_string('{% import "module" as m %}{{ m.test() }}') @@ -180,8 +179,6 @@ class TestAsyncImports(object): assert not hasattr(m, "notthere") -@pytest.mark.imports -@pytest.mark.includes class TestAsyncIncludes(object): def test_context_include(self, test_env_async): t = test_env_async.from_string('{% include "header" %}') @@ -279,8 +276,6 @@ class TestAsyncIncludes(object): assert t.render().strip() == "(FOO)" -@pytest.mark.core_tags -@pytest.mark.for_loop class TestAsyncForLoop(object): def test_simple(self, test_env_async): tmpl = test_env_async.from_string("{% for item in seq %}{{ item }}{% endfor %}") @@ -583,3 +578,14 @@ class TestAsyncForLoop(object): def test_awaitable_property_slicing(self, test_env_async): t = test_env_async.from_string("{% for x in a.b[:1] %}{{ x }}{% endfor %}") assert t.render(a=dict(b=[1, 2, 3])) == "1" + + +def test_namespace_awaitable(test_env_async): + async def _test(): + t = test_env_async.from_string( + '{% set ns = namespace(foo="Bar") %}{{ ns.foo }}' + ) + actual = await t.render_async() + assert actual == "Bar" + + run(_test()) diff --git a/tests/test_bytecode_cache.py b/tests/test_bytecode_cache.py index 2970cb5..c7882b1 100644 --- a/tests/test_bytecode_cache.py +++ b/tests/test_bytecode_cache.py @@ -14,7 +14,6 @@ def env(package_loader, tmp_path): return Environment(loader=package_loader, bytecode_cache=bytecode_cache) -@pytest.mark.byte_code_cache class TestByteCodeCache(object): def test_simple(self, env): tmpl = env.get_template("test.html") diff --git a/tests/test_core_tags.py b/tests/test_core_tags.py index 4132c4f..1bd96c4 100644 --- a/tests/test_core_tags.py +++ b/tests/test_core_tags.py @@ -13,8 +13,6 @@ def env_trim(): return Environment(trim_blocks=True) -@pytest.mark.core_tags -@pytest.mark.for_loop class TestForLoop(object): def test_simple(self, env): tmpl = env.from_string("{% for item in seq %}{{ item }}{% endfor %}") @@ -305,8 +303,6 @@ class TestForLoop(object): assert tmpl.render(x=0, seq=[1, 2, 3]) == "919293" -@pytest.mark.core_tags -@pytest.mark.if_condition class TestIfCondition(object): def test_simple(self, env): tmpl = env.from_string("""{% if true %}...{% endif %}""") @@ -349,8 +345,6 @@ class TestIfCondition(object): assert tmpl.render() == "1" -@pytest.mark.core_tags -@pytest.mark.macros class TestMacros(object): def test_simple(self, env_trim): tmpl = env_trim.from_string( @@ -475,8 +469,6 @@ class TestMacros(object): assert tmpl.module.m(1, x=7) == "1|7|7" -@pytest.mark.core_tags -@pytest.mark.set class TestSet(object): def test_normal(self, env_trim): tmpl = env_trim.from_string("{% set foo = 1 %}{{ foo }}") @@ -584,8 +576,6 @@ class TestSet(object): assert tmpl.module.foo == u"11" -@pytest.mark.core_tags -@pytest.mark.with_ class TestWith(object): def test_with(self, env): tmpl = env.from_string( diff --git a/tests/test_debug.py b/tests/test_debug.py index 5ca92d9..284b9e9 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -17,7 +17,6 @@ def fs_env(filesystem_loader): return Environment(loader=filesystem_loader) -@pytest.mark.debug class TestDebug(object): def assert_traceback_matches(self, callback, expected_tb): with pytest.raises(Exception) as exc_info: diff --git a/tests/test_ext.py b/tests/test_ext.py index 67d2cdb..8e4b411 100644 --- a/tests/test_ext.py +++ b/tests/test_ext.py @@ -167,7 +167,6 @@ class StreamFilterExtension(Extension): yield Token(lineno, "data", token.value[pos:]) -@pytest.mark.ext class TestExtensions(object): def test_extend_late(self): env = Environment() @@ -267,7 +266,6 @@ class TestExtensions(object): assert "'{}'".format(value) in out -@pytest.mark.ext class TestInternationalization(object): def test_trans(self): tmpl = i18n_env.get_template("child.html") @@ -418,7 +416,6 @@ class TestInternationalization(object): ] -@pytest.mark.ext class TestScope(object): def test_basic_scope_behavior(self): # This is what the old with statement compiled down to @@ -452,7 +449,6 @@ class TestScope(object): assert tmpl.render(b=3, e=4) == "1|2|2|4|5" -@pytest.mark.ext class TestNewstyleInternationalization(object): def test_trans(self): tmpl = newstyle_i18n_env.get_template("child.html") @@ -544,7 +540,6 @@ class TestNewstyleInternationalization(object): assert t.render() == "%(foo)s" -@pytest.mark.ext class TestAutoEscape(object): def test_scoped_setting(self): env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=True) diff --git a/tests/test_filters.py b/tests/test_filters.py index 109f444..388c346 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -6,6 +6,8 @@ import pytest from jinja2 import Environment from jinja2 import Markup +from jinja2 import StrictUndefined +from jinja2 import UndefinedError from jinja2._compat import implements_to_string from jinja2._compat import text_type @@ -29,7 +31,6 @@ class Magic2(object): return u"(%s,%s)" % (text_type(self.value1), text_type(self.value2)) -@pytest.mark.filter class TestFilter(object): def test_filter_calling(self, env): rv = env.call_filter("sum", [1, 2, 3]) @@ -370,6 +371,11 @@ class TestFilter(object): tmpl = env.from_string('{{ "foo bar baz"|wordcount }}') assert tmpl.render() == "3" + strict_env = Environment(undefined=StrictUndefined) + t = strict_env.from_string("{{ s|wordcount }}") + with pytest.raises(UndefinedError): + t.render() + def test_block(self, env): tmpl = env.from_string("{% filter lower|escape %}<HEHE>{% endfilter %}") assert tmpl.render() == "<hehe>" diff --git a/tests/test_imports.py b/tests/test_imports.py index 0dae217..fad2eda 100644 --- a/tests/test_imports.py +++ b/tests/test_imports.py @@ -23,7 +23,6 @@ def test_env(): return env -@pytest.mark.imports class TestImports(object): def test_context_imports(self, test_env): t = test_env.from_string('{% import "module" as m %}{{ m.test() }}') @@ -94,8 +93,6 @@ class TestImports(object): assert not hasattr(m, "notthere") -@pytest.mark.imports -@pytest.mark.includes class TestIncludes(object): def test_context_include(self, test_env): t = test_env.from_string('{% include "header" %}') diff --git a/tests/test_inheritance.py b/tests/test_inheritance.py index 92f66e0..e513d2e 100644 --- a/tests/test_inheritance.py +++ b/tests/test_inheritance.py @@ -74,7 +74,6 @@ def env(): ) -@pytest.mark.inheritance class TestInheritance(object): def test_layout(self, env): tmpl = env.get_template("layout") @@ -233,7 +232,6 @@ class TestInheritance(object): assert rv == ["43", "44", "45"] -@pytest.mark.inheritance class TestBugFix(object): def test_fixed_macro_scoping_bug(self, env): assert ( diff --git a/tests/test_lexnparse.py b/tests/test_lexnparse.py index 9da9380..3355791 100644 --- a/tests/test_lexnparse.py +++ b/tests/test_lexnparse.py @@ -27,8 +27,6 @@ else: jinja_string_repr = repr -@pytest.mark.lexnparse -@pytest.mark.tokenstream class TestTokenStream(object): test_tokens = [ Token(1, TOKEN_BLOCK_BEGIN, ""), @@ -57,8 +55,6 @@ class TestTokenStream(object): ] -@pytest.mark.lexnparse -@pytest.mark.lexer class TestLexer(object): def test_raw1(self, env): tmpl = env.from_string( @@ -182,9 +178,25 @@ class TestLexer(object): else: pytest.raises(TemplateSyntaxError, env.from_string, t) + def test_lineno_with_strip(self, env): + tokens = env.lex( + """\ +<html> + <body> + {%- block content -%} + <hr> + {{ item }} + {% endblock %} + </body> +</html>""" + ) + for tok in tokens: + lineno, token_type, value = tok + if token_type == "name" and value == "item": + assert lineno == 5 + break + -@pytest.mark.lexnparse -@pytest.mark.parser class TestParser(object): def test_php_syntax(self, env): env = Environment("<?", "?>", "<?=", "?>", "<!--", "-->") @@ -318,8 +330,6 @@ and bar comment #} assert_error("{% unknown_tag %}", "Encountered unknown tag 'unknown_tag'.") -@pytest.mark.lexnparse -@pytest.mark.syntax class TestSyntax(object): def test_call(self, env): env = Environment() @@ -575,8 +585,6 @@ class TestSyntax(object): assert tmpl.render(foo={"bar": 42}) == "42" -@pytest.mark.lexnparse -@pytest.mark.lstripblocks class TestLstripBlocks(object): def test_lstrip(self, env): env = Environment(lstrip_blocks=True, trim_blocks=False) diff --git a/tests/test_loader.py b/tests/test_loader.py index 4aa6511..f10f756 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -10,14 +10,12 @@ import pytest from jinja2 import Environment from jinja2 import loaders -from jinja2 import PackageLoader from jinja2._compat import PY2 from jinja2._compat import PYPY from jinja2.exceptions import TemplateNotFound from jinja2.loaders import split_template_path -@pytest.mark.loaders class TestLoaders(object): def test_dict_loader(self, dict_loader): env = Environment(loader=dict_loader) @@ -117,8 +115,6 @@ class TestLoaders(object): pytest.raises(TemplateNotFound, split_template_path, "../foo") -@pytest.mark.loaders -@pytest.mark.filesystemloader class TestFileSystemLoader(object): searchpath = os.path.join( os.path.dirname(os.path.abspath(__file__)), "res", "templates" @@ -186,8 +182,6 @@ class TestFileSystemLoader(object): assert t.render() == expect -@pytest.mark.loaders -@pytest.mark.moduleloader class TestModuleLoader(object): archive = None @@ -326,52 +320,3 @@ class TestModuleLoader(object): self.mod_env = Environment(loader=mod_loader) self._test_common() - - -@pytest.fixture() -def package_dir_loader(monkeypatch): - monkeypatch.syspath_prepend(os.path.dirname(__file__)) - return PackageLoader("res") - - -@pytest.mark.parametrize( - ("template", "expect"), [("foo/test.html", "FOO"), ("test.html", "BAR")] -) -def test_package_dir_source(package_dir_loader, template, expect): - source, name, up_to_date = package_dir_loader.get_source(None, template) - assert source.rstrip() == expect - assert name.endswith(os.path.join(*split_template_path(template))) - assert up_to_date() - - -def test_package_dir_list(package_dir_loader): - templates = package_dir_loader.list_templates() - assert "foo/test.html" in templates - assert "test.html" in templates - - -@pytest.fixture() -def package_zip_loader(monkeypatch): - monkeypatch.syspath_prepend( - os.path.join(os.path.dirname(__file__), "res", "package.zip") - ) - return PackageLoader("t_pack") - - -@pytest.mark.parametrize( - ("template", "expect"), [("foo/test.html", "FOO"), ("test.html", "BAR")] -) -def test_package_zip_source(package_zip_loader, template, expect): - source, name, up_to_date = package_zip_loader.get_source(None, template) - assert source.rstrip() == expect - assert name.endswith(os.path.join(*split_template_path(template))) - assert up_to_date is None - - -@pytest.mark.xfail( - PYPY, - reason="PyPy's zipimporter doesn't have a _files attribute.", - raises=TypeError, -) -def test_package_zip_list(package_zip_loader): - assert package_zip_loader.list_templates() == ["foo/test.html", "test.html"] diff --git a/tests/test_regression.py b/tests/test_regression.py index accc1f6..c5a0d68 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -13,7 +13,6 @@ from jinja2 import TemplateSyntaxError from jinja2._compat import text_type -@pytest.mark.regression class TestCorner(object): def test_assigned_scoping(self, env): t = env.from_string( @@ -85,7 +84,6 @@ class TestCorner(object): assert t.render(wrapper=23) == "[1][2][3][4]23" -@pytest.mark.regression class TestBug(object): def test_keyword_folding(self, env): env = Environment() diff --git a/tests/test_runtime.py b/tests/test_runtime.py index d24f266..5e4686c 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -54,3 +54,22 @@ def test_iterator_not_advanced_early(): # groupby groups depend on the current position of the iterator. If # it was advanced early, the lists would appear empty. assert out == "1 [(1, 'a'), (1, 'b')]\n2 [(2, 'c')]\n3 [(3, 'd')]\n" + + +def test_mock_not_contextfunction(): + """If a callable class has a ``__getattr__`` that returns True-like + values for arbitrary attrs, it should not be incorrectly identified + as a ``contextfunction``. + """ + + class Calc(object): + def __getattr__(self, item): + return object() + + def __call__(self, *args, **kwargs): + return len(args) + len(kwargs) + + t = Template("{{ calc() }}") + out = t.render(calc=Calc()) + # Would be "1" if context argument was passed. + assert out == "0" diff --git a/tests/test_security.py b/tests/test_security.py index f092c96..7e8974c 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -37,7 +37,6 @@ class PublicStuff(object): return "PublicStuff" -@pytest.mark.sandbox class TestSandbox(object): def test_unsafe(self, env): env = SandboxedEnvironment() @@ -167,7 +166,6 @@ class TestSandbox(object): t.render(ctx) -@pytest.mark.sandbox class TestStringFormat(object): def test_basic_format_safety(self): env = SandboxedEnvironment() @@ -190,7 +188,6 @@ class TestStringFormat(object): assert t.render() == "a42b<foo>" -@pytest.mark.sandbox @pytest.mark.skipif( not hasattr(str, "format_map"), reason="requires str.format_map method" ) diff --git a/tests/test_tests.py b/tests/test_tests.py index 42595e8..b903e3b 100644 --- a/tests/test_tests.py +++ b/tests/test_tests.py @@ -9,7 +9,6 @@ class MyDict(dict): pass -@pytest.mark.test_tests class TestTestsCase(object): def test_defined(self, env): tmpl = env.from_string("{{ missing is defined }}|{{ true is defined }}") diff --git a/tests/test_utils.py b/tests/test_utils.py index 58165ef..b379a97 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,8 +18,6 @@ from jinja2.utils import select_autoescape from jinja2.utils import urlize -@pytest.mark.utils -@pytest.mark.lrucache class TestLRUCache(object): def test_simple(self): d = LRUCache(3) @@ -120,8 +118,6 @@ class TestLRUCache(object): assert len(d) == 2 -@pytest.mark.utils -@pytest.mark.helpers class TestHelpers(object): def test_object_type_repr(self): class X(object): @@ -150,8 +146,6 @@ class TestHelpers(object): assert not func("FOO.TXT") -@pytest.mark.utils -@pytest.mark.escapeUrlizeTarget class TestEscapeUrlizeTarget(object): def test_escape_urlize_target(self): url = "http://example.org" @@ -163,8 +157,6 @@ class TestEscapeUrlizeTarget(object): ) -@pytest.mark.utils -@pytest.mark.loremIpsum class TestLoremIpsum(object): def test_lorem_ipsum_markup(self): """Test that output of lorem_ipsum is Markup by default.""" |