summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst27
-rw-r--r--docs/api.rst66
-rw-r--r--src/jinja2/asyncfilters.py9
-rw-r--r--src/jinja2/compiler.py12
-rw-r--r--src/jinja2/debug.py5
-rw-r--r--src/jinja2/environment.py6
-rw-r--r--src/jinja2/filters.py2
-rw-r--r--src/jinja2/lexer.py8
-rw-r--r--src/jinja2/loaders.py162
-rw-r--r--src/jinja2/nodes.py6
-rw-r--r--src/jinja2/runtime.py6
-rw-r--r--src/jinja2/utils.py13
-rw-r--r--tests/conftest.py44
-rw-r--r--tests/res/package.zipbin1036 -> 0 bytes
-rw-r--r--tests/test_api.py25
-rw-r--r--tests/test_async.py16
-rw-r--r--tests/test_bytecode_cache.py1
-rw-r--r--tests/test_core_tags.py10
-rw-r--r--tests/test_debug.py1
-rw-r--r--tests/test_ext.py5
-rw-r--r--tests/test_filters.py8
-rw-r--r--tests/test_imports.py3
-rw-r--r--tests/test_inheritance.py2
-rw-r--r--tests/test_lexnparse.py28
-rw-r--r--tests/test_loader.py55
-rw-r--r--tests/test_regression.py2
-rw-r--r--tests/test_runtime.py19
-rw-r--r--tests/test_security.py3
-rw-r--r--tests/test_tests.py1
-rw-r--r--tests/test_utils.py8
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
deleted file mode 100644
index d4c9ce9..0000000
--- a/tests/res/package.zip
+++ /dev/null
Binary files differ
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() == "&lt;hehe&gt;"
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&lt;foo&gt;"
-@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."""