summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.azure-pipelines.yml11
-rw-r--r--.pre-commit-config.yaml5
-rw-r--r--CHANGES.rst15
-rw-r--r--docs/api.rst128
-rw-r--r--docs/conf.py6
-rw-r--r--docs/examples/cache_extension.py2
-rw-r--r--docs/examples/inline_gettext_extension.py3
-rw-r--r--docs/extensions.rst10
-rw-r--r--docs/faq.rst40
-rw-r--r--docs/index.rst29
-rw-r--r--docs/intro.rst99
-rw-r--r--docs/switching.rst12
-rw-r--r--docs/templates.rst9
-rw-r--r--examples/basic/cycle.py2
-rw-r--r--examples/basic/debugger.py2
-rw-r--r--examples/basic/inheritance.py2
-rw-r--r--examples/basic/test.py8
-rw-r--r--examples/basic/test_filter_and_linestatements.py2
-rw-r--r--examples/basic/test_loop_filter.py2
-rw-r--r--examples/basic/translate.py4
-rwxr-xr-xscripts/generate_identifier_pattern.py8
-rw-r--r--setup.cfg5
-rw-r--r--setup.py24
-rw-r--r--src/jinja2/__init__.py3
-rw-r--r--src/jinja2/_compat.py132
-rw-r--r--src/jinja2/asyncfilters.py5
-rw-r--r--src/jinja2/asyncsupport.py17
-rw-r--r--src/jinja2/bccache.py35
-rw-r--r--src/jinja2/compiler.py441
-rw-r--r--src/jinja2/constants.py3
-rw-r--r--src/jinja2/debug.py27
-rw-r--r--src/jinja2/defaults.py4
-rw-r--r--src/jinja2/environment.py146
-rw-r--r--src/jinja2/exceptions.py58
-rw-r--r--src/jinja2/ext.py40
-rw-r--r--src/jinja2/filters.py151
-rw-r--r--src/jinja2/idtracking.py15
-rw-r--r--src/jinja2/lexer.py155
-rw-r--r--src/jinja2/loaders.py217
-rw-r--r--src/jinja2/meta.py13
-rw-r--r--src/jinja2/nativetypes.py8
-rw-r--r--src/jinja2/nodes.py94
-rw-r--r--src/jinja2/optimizer.py3
-rw-r--r--src/jinja2/parser.py37
-rw-r--r--src/jinja2/runtime.py273
-rw-r--r--src/jinja2/sandbox.py105
-rw-r--r--src/jinja2/tests.py18
-rw-r--r--src/jinja2/utils.py196
-rw-r--r--src/jinja2/visitor.py6
-rw-r--r--tests/conftest.py1
-rw-r--r--tests/res/package.zipbin0 -> 1036 bytes
-rw-r--r--tests/test_api.py58
-rw-r--r--tests/test_async.py9
-rw-r--r--tests/test_bytecode_cache.py7
-rw-r--r--tests/test_core_tags.py29
-rw-r--r--tests/test_debug.py10
-rw-r--r--tests/test_ext.py155
-rw-r--r--tests/test_features.py32
-rw-r--r--tests/test_filters.py50
-rw-r--r--tests/test_idtracking.py38
-rw-r--r--tests/test_imports.py16
-rw-r--r--tests/test_inheritance.py9
-rw-r--r--tests/test_lexnparse.py123
-rw-r--r--tests/test_loader.py125
-rw-r--r--tests/test_nativetypes.py5
-rw-r--r--tests/test_regression.py37
-rw-r--r--tests/test_runtime.py2
-rw-r--r--tests/test_security.py64
-rw-r--r--tests/test_tests.py7
-rw-r--r--tests/test_utils.py38
-rw-r--r--tox.ini2
71 files changed, 1288 insertions, 2159 deletions
diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml
index 680a15c..5659611 100644
--- a/.azure-pipelines.yml
+++ b/.azure-pipelines.yml
@@ -6,7 +6,6 @@ variables:
vmImage: ubuntu-latest
python.version: '3.8'
TOXENV: py
- hasTestResults: 'true'
strategy:
matrix:
@@ -16,22 +15,16 @@ strategy:
vmImage: windows-latest
Python 3.8 Mac:
vmImage: macos-latest
- PyPy 3 Linux:
- python.version: pypy3
Python 3.7 Linux:
python.version: '3.7'
Python 3.6 Linux:
python.version: '3.6'
- Python 3.5 Linux:
- python.version: '3.5'
- Python 2.7 Linux:
- python.version: '2.7'
+ PyPy 3 Linux:
+ python.version: pypy3
Docs:
TOXENV: docs
- hasTestResults: 'false'
Style:
TOXENV: style
- hasTestResults: 'false'
pool:
vmImage: $[ variables.vmImage ]
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3a341a8..a982b5f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,4 +1,9 @@
repos:
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v1.26.2
+ hooks:
+ - id: pyupgrade
+ args: ["--py36-plus"]
- repo: https://github.com/asottile/reorder_python_imports
rev: v1.9.0
hooks:
diff --git a/CHANGES.rst b/CHANGES.rst
index 579b80b..1911ab4 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,18 @@
.. currentmodule:: jinja2
+Version 3.0.0
+-------------
+
+Unreleased
+
+- Drop support for Python 2.7 and 3.5.
+- Bump MarkupSafe dependency to >=1.1.
+- Bump Babel optional dependency to >=2.1.
+- Remove code that was marked deprecated.
+- Use :pep:`451` API to load templates with
+ :class:`~loaders.PackageLoader`. :issue:`1168`
+
+
2.11.2
------
@@ -9,7 +22,7 @@ Unreleased
: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`
+ by wrapping the input in :func:`soft_str`. :pr:`1160`
- Fix a hang when displaying tracebacks on Python 32-bit.
:issue:`1162`
- Showing an undefined error for an object that raises
diff --git a/docs/api.rst b/docs/api.rst
index 40f9849..53cb03b 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -61,63 +61,6 @@ a lot easier to use it also enables template inheritance.
configure autoescaping now instead of relying on the default.
-Unicode
--------
-
-Jinja is using Unicode internally which means that you have to pass Unicode
-objects to the render function or bytestrings that only consist of ASCII
-characters. Additionally newlines are normalized to one end of line
-sequence which is per default UNIX style (``\n``).
-
-Python 2.x supports two ways of representing string objects. One is the
-`str` type and the other is the `unicode` type, both of which extend a type
-called `basestring`. Unfortunately the default is `str` which should not
-be used to store text based information unless only ASCII characters are
-used. With Python 2.6 it is possible to make `unicode` the default on a per
-module level and with Python 3 it will be the default.
-
-To explicitly use a Unicode string you have to prefix the string literal
-with a `u`: ``u'Hänsel und Gretel sagen Hallo'``. That way Python will
-store the string as Unicode by decoding the string with the character
-encoding from the current Python module. If no encoding is specified this
-defaults to 'ASCII' which means that you can't use any non ASCII identifier.
-
-To set a better module encoding add the following comment to the first or
-second line of the Python module using the Unicode literal::
-
- # -*- coding: utf-8 -*-
-
-We recommend utf-8 as Encoding for Python modules and templates as it's
-possible to represent every Unicode character in utf-8 and because it's
-backwards compatible to ASCII. For Jinja the default encoding of templates
-is assumed to be utf-8.
-
-It is not possible to use Jinja to process non-Unicode data. The reason
-for this is that Jinja uses Unicode already on the language level. For
-example Jinja treats the non-breaking space as valid whitespace inside
-expressions which requires knowledge of the encoding or operating on an
-Unicode string.
-
-For more details about Unicode in Python have a look at the excellent
-`Unicode documentation`_.
-
-Another important thing is how Jinja is handling string literals in
-templates. A naive implementation would be using Unicode strings for
-all string literals but it turned out in the past that this is problematic
-as some libraries are typechecking against `str` explicitly. For example
-`datetime.strftime` does not accept Unicode arguments. To not break it
-completely Jinja is returning `str` for strings that fit into ASCII and
-for everything else `unicode`:
-
->>> m = Template(u"{% set a, b = 'foo', 'föö' %}").module
->>> m.a
-'foo'
->>> m.b
-u'f\xf6\xf6'
-
-
-.. _Unicode documentation: https://docs.python.org/3/howto/unicode.html
-
High Level API
--------------
@@ -302,12 +245,12 @@ Notes on Identifiers
--------------------
Jinja uses Python naming rules. Valid identifiers can be any combination
-of Unicode characters accepted by Python.
+of characters accepted by Python.
Filters and tests are looked up in separate namespaces and have slightly
modified identifier syntax. Filters and tests may contain dots to group
filters and tests by topic. For example it's perfectly valid to add a
-function into the filter dict and call it `to.unicode`. The regular
+function into the filter dict and call it `to.str`. The regular
expression for filter and test identifiers is
``[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*```.
@@ -329,8 +272,8 @@ disallows all operations beside testing if it's an undefined object.
.. attribute:: _undefined_hint
- Either `None` or an unicode string with the error message for
- the undefined object.
+ Either `None` or a string with the error message for the
+ undefined object.
.. attribute:: _undefined_obj
@@ -368,27 +311,32 @@ Undefined objects are created by calling :attr:`undefined`.
.. admonition:: Implementation
- :class:`Undefined` objects are implemented by overriding the special
- `__underscore__` methods. For example the default :class:`Undefined`
- class implements `__unicode__` in a way that it returns an empty
- string, however `__int__` and others still fail with an exception. To
- allow conversion to int by returning ``0`` you can implement your own::
+ :class:`Undefined` is implemented by overriding the special
+ ``__underscore__`` methods. For example the default
+ :class:`Undefined` class implements ``__str__`` to returns an empty
+ string, while ``__int__`` and others fail with an exception. To
+ allow conversion to int by returning ``0`` you can implement your
+ own subclass.
+
+ .. code-block:: python
class NullUndefined(Undefined):
def __int__(self):
return 0
+
def __float__(self):
return 0.0
- To disallow a method, just override it and raise
- :attr:`~Undefined._undefined_exception`. Because this is a very common
- idiom in undefined objects there is the helper method
- :meth:`~Undefined._fail_with_undefined_error` that does the error raising
- automatically. Here a class that works like the regular :class:`Undefined`
- but chokes on iteration::
+ To disallow a method, override it and raise
+ :attr:`~Undefined._undefined_exception`. Because this is very
+ common there is the helper method
+ :meth:`~Undefined._fail_with_undefined_error` that raises the error
+ with the correct information. Here's a class that works like the
+ regular :class:`Undefined` but fails on iteration::
class NonIterableUndefined(Undefined):
- __iter__ = Undefined._fail_with_undefined_error
+ def __iter__(self):
+ self._fail_with_undefined_error()
The Context
@@ -577,16 +525,6 @@ Example::
env.policies['urlize.rel'] = 'nofollow noopener'
-``compiler.ascii_str``:
- This boolean controls on Python 2 if Jinja should store ASCII only
- literals as bytestring instead of unicode strings. This used to be
- always enabled for Jinja versions below 2.9 and now can be changed.
- Traditionally it was done this way since some APIs in Python 2 failed
- badly for unicode strings (for instance the datetime strftime API).
- Now however sometimes the inverse is true (for instance str.format).
- If this is set to False then all strings are stored as unicode
- internally.
-
``truncate.leeway``:
Configures the leeway default for the `truncate` filter. Leeway as
introduced in 2.9 but to restore compatibility with older templates
@@ -678,24 +616,20 @@ Exceptions
.. attribute:: message
- The error message as utf-8 bytestring.
+ The error message.
.. attribute:: lineno
- The line number where the error occurred
+ The line number where the error occurred.
.. attribute:: name
- The load name for the template as unicode string.
+ The load name for the template.
.. attribute:: filename
- The filename that loaded the template as bytestring in the encoding
- of the file system (most likely utf-8 or mbcs on Windows systems).
-
- The reason why the filename and error message are bytestrings and not
- unicode strings is that Python 2.x is not using unicode for exceptions
- and tracebacks as well as the compiler. This will change with Python 3.
+ The filename that loaded the template in the encoding of the
+ file system (most likely utf-8, or mbcs on Windows systems).
.. autoexception:: jinja2.TemplateRuntimeError
@@ -743,12 +677,14 @@ enabled::
import re
from jinja2 import evalcontextfilter, Markup, escape
- _paragraph_re = re.compile(r'(?:\r\n|\r(?!\n)|\n){2,}')
+ _paragraph_re = re.compile(r"(?:\r\n|\r(?!\n)|\n){2,}")
@evalcontextfilter
def nl2br(eval_ctx, value):
- result = u'\n\n'.join(u'<p>%s</p>' % p.replace('\n', Markup('<br>\n'))
- for p in _paragraph_re.split(escape(value)))
+ result = "\n\n".join(
+ f"<p>{p.replace('\n', Markup('<br>\n'))}</p>"
+ for p in _paragraph_re.split(escape(value))
+ )
if eval_ctx.autoescape:
result = Markup(result)
return result
@@ -896,7 +832,7 @@ don't recommend using any of those.
that has to be created by :meth:`new_context` of the same template or
a compatible template. This render function is generated by the
compiler from the template code and returns a generator that yields
- unicode strings.
+ strings.
If an exception in the template code happens the template engine will
not rewrite the exception but pass through the original one. As a
diff --git a/docs/conf.py b/docs/conf.py
index 01e530d..783bae2 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -42,11 +42,9 @@ singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]}
html_static_path = ["_static"]
html_favicon = "_static/jinja-logo-sidebar.png"
html_logo = "_static/jinja-logo-sidebar.png"
-html_title = "Jinja Documentation ({})".format(version)
+html_title = f"Jinja Documentation ({version})"
html_show_sourcelink = False
# LaTeX ----------------------------------------------------------------
-latex_documents = [
- (master_doc, "Jinja-{}.tex".format(version), html_title, author, "manual")
-]
+latex_documents = [(master_doc, f"Jinja-{version}.tex", html_title, author, "manual")]
diff --git a/docs/examples/cache_extension.py b/docs/examples/cache_extension.py
index 387cd46..46af67c 100644
--- a/docs/examples/cache_extension.py
+++ b/docs/examples/cache_extension.py
@@ -7,7 +7,7 @@ class FragmentCacheExtension(Extension):
tags = {"cache"}
def __init__(self, environment):
- super(FragmentCacheExtension, self).__init__(environment)
+ super().__init__(environment)
# add the defaults to the environment
environment.extend(fragment_cache_prefix="", fragment_cache=None)
diff --git a/docs/examples/inline_gettext_extension.py b/docs/examples/inline_gettext_extension.py
index 47bc9cc..d75119c 100644
--- a/docs/examples/inline_gettext_extension.py
+++ b/docs/examples/inline_gettext_extension.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
import re
from jinja2.exceptions import TemplateSyntaxError
@@ -54,7 +53,7 @@ class InlineGettext(Extension):
else:
if gtok == "(" or paren_stack > 1:
yield Token(lineno, "data", gtok)
- paren_stack += gtok == ")" and -1 or 1
+ paren_stack += -1 if gtok == ")" else 1
if not paren_stack:
yield Token(lineno, "block_begin", None)
yield Token(lineno, "name", "endtrans")
diff --git a/docs/extensions.rst b/docs/extensions.rst
index 7abed65..bb81f21 100644
--- a/docs/extensions.rst
+++ b/docs/extensions.rst
@@ -44,8 +44,7 @@ additional methods:
.. method:: jinja2.Environment.install_gettext_translations(translations, newstyle=False)
Installs a translation globally for the environment. The
- ``translations`` object must implement ``gettext`` and ``ngettext``
- (or ``ugettext`` and ``ungettext`` for Python 2).
+ ``translations`` object must implement ``gettext`` and ``ngettext``.
:class:`gettext.NullTranslations`, :class:`gettext.GNUTranslations`,
and `Babel`_\s ``Translations`` are supported.
@@ -63,8 +62,7 @@ additional methods:
Install the given ``gettext`` and ``ngettext`` callables into the
environment. They should behave exactly like
- :func:`gettext.gettext` and :func:`gettext.ngettext` (or
- ``ugettext`` and ``ungettext`` for Python 2).
+ :func:`gettext.gettext` and :func:`gettext.ngettext`.
If ``newstyle`` is activated, the callables are wrapped to work like
newstyle callables. See :ref:`newstyle-gettext` for more information.
@@ -86,8 +84,8 @@ additional methods:
found.
- ``function`` is the name of the ``gettext`` function used (if
the string was extracted from embedded Python code).
- - ``message`` is the string itself (``unicode`` on Python 2), or a
- tuple of strings for functions with multiple arguments.
+ - ``message`` is the string itself, or a tuple of strings for
+ functions with multiple arguments.
If `Babel`_ is installed, see :ref:`babel-integration` to extract
the strings.
diff --git a/docs/faq.rst b/docs/faq.rst
index 294fef1..1e29e12 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -125,19 +125,18 @@ instead that one can assign to a variable by using set::
{% set comments = get_latest_comments() %}
-My tracebacks look weird. What's happening?
---------------------------------------------
+My tracebacks look weird. What's happening?
+-------------------------------------------
-If the debugsupport module is not compiled and you are using a Python
-installation without ctypes (Python 2.4 without ctypes, Jython or Google's
-AppEngine) Jinja is unable to provide correct debugging information and
-the traceback may be incomplete. There is currently no good workaround
-for Jython or the AppEngine as ctypes is unavailable there and it's not
-possible to use the debugsupport extension.
+Jinja can rewrite tracebacks so they show the template lines numbers and
+source rather than the underlying compiled code, but this requires
+special Python support. CPython <3.7 requires ``ctypes``, and PyPy
+requires transparent proxy support.
-If you are working in the Google AppEngine development server you can
-whitelist the ctypes module to restore the tracebacks. This however won't
-work in production environments::
+If you are using Google App Engine, ``ctypes`` is not available. You can
+make it available in development, but not in production.
+
+.. code-block:: python
import os
if os.environ.get('SERVER_SOFTWARE', '').startswith('Dev'):
@@ -147,25 +146,6 @@ work in production environments::
Credit for this snippet goes to `Thomas Johansson
<https://stackoverflow.com/questions/3086091/debug-jinja2-in-google-app-engine/3694434#3694434>`_
-Why is there no Python 2.3/2.4/2.5/2.6/3.1/3.2/3.3 support?
------------------------------------------------------------
-
-Python 2.3 is missing a lot of features that are used heavily in Jinja. This
-decision was made as with the upcoming Python 2.6 and 3.0 versions it becomes
-harder to maintain the code for older Python versions. If you really need
-Python 2.3 support you either have to use Jinja 1 or other templating
-engines that still support 2.3.
-
-Python 2.4/2.5/3.1/3.2 support was removed when we switched to supporting
-Python 2 and 3 by the same sourcecode (without using 2to3). It was required to
-drop support because only Python 2.6/2.7 and >=3.3 support byte and unicode
-literals in a way compatible to each other version. If you really need support
-for older Python 2 (or 3) versions, you can just use Jinja 2.6.
-
-Python 2.6/3.3 support was dropped because it got dropped in various upstream
-projects (such as wheel or pytest), which would make it difficult to continue
-supporting it. Jinja 2.10 was the last version supporting Python 2.6/3.3.
-
My Macros are overridden by something
-------------------------------------
diff --git a/docs/index.rst b/docs/index.rst
index 65d5d3d..dcaa9ff 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -7,29 +7,9 @@ Jinja
:align: center
:target: https://palletsprojects.com/p/jinja/
-Jinja is a modern and designer-friendly templating language for Python,
-modelled after Django's templates. It is fast, widely used and secure
-with the optional sandboxed template execution environment:
-
-.. sourcecode:: html+jinja
-
- <title>{% block title %}{% endblock %}</title>
- <ul>
- {% for user in users %}
- <li><a href="{{ user.url }}">{{ user.username }}</a></li>
- {% endfor %}
- </ul>
-
-Features:
-
-- sandboxed execution
-- powerful automatic HTML escaping system for XSS prevention
-- template inheritance
-- compiles down to the optimal python code just in time
-- optional ahead-of-time template compilation
-- easy to debug. Line numbers of exceptions directly point to
- the correct line in the template.
-- configurable syntax
+Jinja is a fast, expressive, extensible templating engine. Special
+placeholders in the template allow writing code similar to Python
+syntax. Then the template is passed data to render the final document.
.. toctree::
:maxdepth: 2
@@ -46,6 +26,3 @@ Features:
tricks
faq
changelog
-
-* :ref:`genindex`
-* :ref:`search`
diff --git a/docs/intro.rst b/docs/intro.rst
index c20c5e9..25c2b58 100644
--- a/docs/intro.rst
+++ b/docs/intro.rst
@@ -1,82 +1,63 @@
Introduction
============
-This is the documentation for the Jinja general purpose templating language.
-Jinja is a library for Python that is designed to be flexible, fast and secure.
+Jinja is a fast, expressive, extensible templating engine. Special
+placeholders in the template allow writing code similar to Python
+syntax. Then the template is passed data to render the final document.
+
+It includes:
+
+- Template inheritance and inclusion.
+- Define and import macros within templates.
+- HTML templates can use autoescaping to prevent XSS from untrusted
+ user input.
+- A sandboxed environment can safely render untrusted templates.
+- AsyncIO support for generating templates and calling async
+ functions.
+- I18N support with Babel.
+- Templates are compiled to optimized Python code just-in-time and
+ cached, or can be compiled ahead-of-time.
+- Exceptions point to the correct line in templates to make debugging
+ easier.
+- Extensible filters, tests, functions, and even syntax.
+
+Jinja's philosophy is that while application logic belongs in Python if
+possible, it shouldn't make the template designer's job difficult by
+restricting functionality too much.
-If you have any exposure to other text-based template languages, such as Smarty or
-Django, you should feel right at home with Jinja. It's both designer and
-developer friendly by sticking to Python's principles and adding functionality
-useful for templating environments.
-
-Prerequisites
--------------
-
-Jinja works with Python 2.7.x and >= 3.5. If you are using Python
-3.2 you can use an older release of Jinja (2.6) as support for Python 3.2
-was dropped in Jinja version 2.7. The last release which supported Python 2.6
-and 3.3 was Jinja 2.10.
-
-If you wish to use the :class:`~jinja2.PackageLoader` class, you will also
-need `setuptools`_ or `distribute`_ installed at runtime.
Installation
------------
-You can install the most recent Jinja version using `pip`_::
+We recommend using the latest version of Python. Jinja supports Python
+3.6 and newer. We also recommend using a `virtual environment`_ in order
+to isolate your project dependencies from other projects and the system.
- pip install Jinja2
+.. _virtual environment: https://packaging.python.org/tutorials/installing-packages/#creating-virtual-environments
-This will install Jinja in your Python installation's site-packages directory.
+Install the most recent Jinja version using pip:
-Installing the development version
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. code-block:: text
-1. Install `git`_
-2. ``git clone git://github.com/pallets/jinja.git``
-3. ``cd jinja2``
-4. ``ln -s jinja2 /usr/lib/python2.X/site-packages``
+ $ pip install Jinja2
-As an alternative to steps 4 you can also do ``python setup.py develop``
-which will install the package via `distribute` in development mode. This also
-has the advantage that the C extensions are compiled.
-.. _distribute: https://pypi.org/project/distribute/
-.. _setuptools: https://pypi.org/project/setuptools/
-.. _pip: https://pypi.org/project/pip/
-.. _git: https://git-scm.com/
+Dependencies
+~~~~~~~~~~~~
+These will be installed automatically when installing Jinja.
-MarkupSafe Dependency
-~~~~~~~~~~~~~~~~~~~~~
-
-As of version 2.7 Jinja depends on the `MarkupSafe`_ module. If you install
-Jinja via ``pip`` it will be installed automatically for you.
+- `MarkupSafe`_ escapes untrusted input when rendering templates to
+ avoid injection attacks.
.. _MarkupSafe: https://markupsafe.palletsprojects.com/
-Basic API Usage
----------------
-
-This section gives you a brief introduction to the Python API for Jinja
-templates.
-The most basic way to create a template and render it is through
-:class:`~jinja2.Template`. This however is not the recommended way to
-work with it if your templates are not loaded from strings but the file
-system or another data source:
+Optional Dependencies
+~~~~~~~~~~~~~~~~~~~~~
->>> from jinja2 import Template
->>> template = Template('Hello {{ name }}!')
->>> template.render(name='John Doe')
-u'Hello John Doe!'
+These distributions will not be installed automatically.
-By creating an instance of :class:`~jinja2.Template` you get back a new template
-object that provides a method called :meth:`~jinja2.Template.render` which when
-called with a dict or keyword arguments expands the template. The dict
-or keywords arguments passed to the template are the so-called "context"
-of the template.
+- `Babel`_ provides translation support in templates.
-What you can see here is that Jinja is using unicode internally and the
-return value is an unicode string. So make sure that your application is
-indeed using unicode internally.
+.. _Babel: http://babel.pocoo.org/
diff --git a/docs/switching.rst b/docs/switching.rst
index 8225b2e..b9ff954 100644
--- a/docs/switching.rst
+++ b/docs/switching.rst
@@ -32,12 +32,12 @@ Loading templates from strings
with optional additional configuration.
Automatic unicode conversion
- Jinja 1 performed automatic conversion of bytestrings in a given encoding
- into unicode objects. This conversion is no longer implemented as it
- was inconsistent as most libraries are using the regular Python ASCII
- bytestring to Unicode conversion. An application powered by Jinja 2
- *has to* use unicode internally everywhere or make sure that Jinja 2 only
- gets unicode strings passed.
+ Jinja 1 performed automatic conversion of bytes in a given encoding
+ into unicode objects. This conversion is no longer implemented as it
+ was inconsistent as most libraries are using the regular Python
+ ASCII bytes to Unicode conversion. An application powered by Jinja 2
+ *has to* use unicode internally everywhere or make sure that Jinja 2
+ only gets unicode strings passed.
i18n
Jinja 1 used custom translators for internationalization. i18n is now
diff --git a/docs/templates.rst b/docs/templates.rst
index 89c2a50..a346ef2 100644
--- a/docs/templates.rst
+++ b/docs/templates.rst
@@ -602,9 +602,8 @@ you have data that is already safe but not marked, be sure to wrap it in
Jinja functions (macros, `super`, `self.BLOCKNAME`) always return template
data that is marked as safe.
-String literals in templates with automatic escaping are considered unsafe
-because native Python strings (``str``, ``unicode``, ``basestring``) are not
-`MarkupSafe.Markup` strings with an ``__html__`` attribute.
+String literals in templates with automatic escaping are considered
+unsafe because native Python strings are not safe.
.. _list-of-control-structures:
@@ -913,9 +912,9 @@ Here's an example of how a call block can be used with arguments::
{% call(user) dump_users(list_of_user) %}
<dl>
- <dl>Realname</dl>
+ <dt>Realname</dt>
<dd>{{ user.realname|e }}</dd>
- <dl>Description</dl>
+ <dt>Description</dt>
<dd>{{ user.description }}</dd>
</dl>
{% endcall %}
diff --git a/examples/basic/cycle.py b/examples/basic/cycle.py
index 25dcb0b..1f97e37 100644
--- a/examples/basic/cycle.py
+++ b/examples/basic/cycle.py
@@ -1,5 +1,3 @@
-from __future__ import print_function
-
from jinja2 import Environment
env = Environment(
diff --git a/examples/basic/debugger.py b/examples/basic/debugger.py
index d3c1a60..f6a9627 100644
--- a/examples/basic/debugger.py
+++ b/examples/basic/debugger.py
@@ -1,5 +1,3 @@
-from __future__ import print_function
-
from jinja2 import Environment
from jinja2.loaders import FileSystemLoader
diff --git a/examples/basic/inheritance.py b/examples/basic/inheritance.py
index 4a881bf..6d928df 100644
--- a/examples/basic/inheritance.py
+++ b/examples/basic/inheritance.py
@@ -1,5 +1,3 @@
-from __future__ import print_function
-
from jinja2 import Environment
from jinja2.loaders import DictLoader
diff --git a/examples/basic/test.py b/examples/basic/test.py
index 80b9d1f..d34b0ff 100644
--- a/examples/basic/test.py
+++ b/examples/basic/test.py
@@ -1,12 +1,10 @@
-from __future__ import print_function
-
from jinja2 import Environment
from jinja2.loaders import DictLoader
env = Environment(
loader=DictLoader(
{
- "child.html": u"""\
+ "child.html": """\
{% extends master_layout or 'master.html' %}
{% include helpers = 'helpers.html' %}
{% macro get_the_answer() %}42{% endmacro %}
@@ -16,12 +14,12 @@ env = Environment(
{{ helpers.conspirate() }}
{% endblock %}
""",
- "master.html": u"""\
+ "master.html": """\
<!doctype html>
<title>{{ title }}</title>
{% block body %}{% endblock %}
""",
- "helpers.html": u"""\
+ "helpers.html": """\
{% macro conspirate() %}23{% endmacro %}
""",
}
diff --git a/examples/basic/test_filter_and_linestatements.py b/examples/basic/test_filter_and_linestatements.py
index 673b67e..9bbcbca 100644
--- a/examples/basic/test_filter_and_linestatements.py
+++ b/examples/basic/test_filter_and_linestatements.py
@@ -1,5 +1,3 @@
-from __future__ import print_function
-
from jinja2 import Environment
env = Environment(
diff --git a/examples/basic/test_loop_filter.py b/examples/basic/test_loop_filter.py
index 39be08d..6bd89fd 100644
--- a/examples/basic/test_loop_filter.py
+++ b/examples/basic/test_loop_filter.py
@@ -1,5 +1,3 @@
-from __future__ import print_function
-
from jinja2 import Environment
tmpl = Environment().from_string(
diff --git a/examples/basic/translate.py b/examples/basic/translate.py
index 71547f4..e659681 100644
--- a/examples/basic/translate.py
+++ b/examples/basic/translate.py
@@ -1,5 +1,3 @@
-from __future__ import print_function
-
from jinja2 import Environment
env = Environment(extensions=["jinja2.ext.i18n"])
@@ -7,7 +5,7 @@ env.globals["gettext"] = {"Hello %(user)s!": "Hallo %(user)s!"}.__getitem__
env.globals["ngettext"] = lambda s, p, n: {
"%(count)s user": "%(count)d Benutzer",
"%(count)s users": "%(count)d Benutzer",
-}[n == 1 and s or p]
+}[s if n == 1 else p]
print(
env.from_string(
"""\
diff --git a/scripts/generate_identifier_pattern.py b/scripts/generate_identifier_pattern.py
index 5813199..6b47953 100755
--- a/scripts/generate_identifier_pattern.py
+++ b/scripts/generate_identifier_pattern.py
@@ -1,12 +1,8 @@
-#!/usr/bin/env python3
import itertools
import os
import re
import sys
-if sys.version_info[0] < 3:
- raise RuntimeError("This needs to run on Python 3.")
-
def get_characters():
"""Find every Unicode character that is valid in a Python `identifier`_ but
@@ -52,7 +48,7 @@ def build_pattern(ranges):
out.append(a)
out.append(b)
else:
- out.append("{}-{}".format(a, b))
+ out.append(f"{a}-{b}")
return "".join(out)
@@ -70,7 +66,7 @@ def main():
f.write("import re\n\n")
f.write("# generated by scripts/generate_identifier_pattern.py\n")
f.write("pattern = re.compile(\n")
- f.write(' r"[\\w{}]+" # noqa: B950\n'.format(pattern))
+ f.write(f' r"[\\w{pattern}]+" # noqa: B950\n')
f.write(")\n")
diff --git a/setup.cfg b/setup.cfg
index 2769e96..5d3d02e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,15 +1,12 @@
[metadata]
license_file = LICENSE.rst
+long_description = file:README.rst
long_description_content_type = text/x-rst
-[bdist_wheel]
-universal = true
-
[tool:pytest]
testpaths = tests
filterwarnings =
error
- ignore:the sets module:DeprecationWarning:jinja2.sandbox
[coverage:run]
branch = True
diff --git a/setup.py b/setup.py
index 7d94cd3..f5ad968 100644
--- a/setup.py
+++ b/setup.py
@@ -1,13 +1,9 @@
-import io
import re
from setuptools import find_packages
from setuptools import setup
-with io.open("README.rst", "rt", encoding="utf8") as f:
- readme = f.read()
-
-with io.open("src/jinja2/__init__.py", "rt", encoding="utf8") as f:
+with open("src/jinja2/__init__.py", "rt", encoding="utf8") as f:
version = re.search(r'__version__ = "(.*?)"', f.read(), re.M).group(1)
setup(
@@ -20,12 +16,9 @@ setup(
"Issue tracker": "https://github.com/pallets/jinja/issues",
},
license="BSD-3-Clause",
- author="Armin Ronacher",
- author_email="armin.ronacher@active-4.com",
maintainer="Pallets",
maintainer_email="contact@palletsprojects.com",
description="A very fast and expressive template engine.",
- long_description=readme,
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
@@ -33,15 +26,6 @@ setup(
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
- "Programming Language :: Python :: 2",
- "Programming Language :: Python :: 2.7",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.5",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: Implementation :: CPython",
- "Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Text Processing :: Markup :: HTML",
@@ -49,8 +33,8 @@ setup(
packages=find_packages("src"),
package_dir={"": "src"},
include_package_data=True,
- python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
- install_requires=["MarkupSafe>=0.23"],
- extras_require={"i18n": ["Babel>=0.8"]},
+ python_requires=">=3.6",
+ install_requires=["MarkupSafe>=1.1"],
+ extras_require={"i18n": ["Babel>=2.1"]},
entry_points={"babel.extractors": ["jinja2 = jinja2.ext:babel_extract[i18n]"]},
)
diff --git a/src/jinja2/__init__.py b/src/jinja2/__init__.py
index 7f4a1c5..8fa0518 100644
--- a/src/jinja2/__init__.py
+++ b/src/jinja2/__init__.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
"""Jinja is a template engine written in pure Python. It provides a
non-XML syntax that supports inline expressions and an optional
sandboxed environment.
@@ -41,4 +40,4 @@ from .utils import evalcontextfunction
from .utils import is_undefined
from .utils import select_autoescape
-__version__ = "2.11.1"
+__version__ = "3.0.0a1"
diff --git a/src/jinja2/_compat.py b/src/jinja2/_compat.py
deleted file mode 100644
index 1f04495..0000000
--- a/src/jinja2/_compat.py
+++ /dev/null
@@ -1,132 +0,0 @@
-# -*- coding: utf-8 -*-
-# flake8: noqa
-import marshal
-import sys
-
-PY2 = sys.version_info[0] == 2
-PYPY = hasattr(sys, "pypy_translation_info")
-_identity = lambda x: x
-
-if not PY2:
- unichr = chr
- range_type = range
- text_type = str
- string_types = (str,)
- integer_types = (int,)
-
- iterkeys = lambda d: iter(d.keys())
- itervalues = lambda d: iter(d.values())
- iteritems = lambda d: iter(d.items())
-
- import pickle
- from io import BytesIO, StringIO
-
- NativeStringIO = StringIO
-
- def reraise(tp, value, tb=None):
- if value.__traceback__ is not tb:
- raise value.with_traceback(tb)
- raise value
-
- ifilter = filter
- imap = map
- izip = zip
- intern = sys.intern
-
- implements_iterator = _identity
- implements_to_string = _identity
- encode_filename = _identity
-
- marshal_dump = marshal.dump
- marshal_load = marshal.load
-
-else:
- unichr = unichr
- text_type = unicode
- range_type = xrange
- string_types = (str, unicode)
- integer_types = (int, long)
-
- iterkeys = lambda d: d.iterkeys()
- itervalues = lambda d: d.itervalues()
- iteritems = lambda d: d.iteritems()
-
- import cPickle as pickle
- from cStringIO import StringIO as BytesIO, StringIO
-
- NativeStringIO = BytesIO
-
- exec("def reraise(tp, value, tb=None):\n raise tp, value, tb")
-
- from itertools import imap, izip, ifilter
-
- intern = intern
-
- def implements_iterator(cls):
- cls.next = cls.__next__
- del cls.__next__
- return cls
-
- def implements_to_string(cls):
- cls.__unicode__ = cls.__str__
- cls.__str__ = lambda x: x.__unicode__().encode("utf-8")
- return cls
-
- def encode_filename(filename):
- if isinstance(filename, unicode):
- return filename.encode("utf-8")
- return filename
-
- def marshal_dump(code, f):
- if isinstance(f, file):
- marshal.dump(code, f)
- else:
- f.write(marshal.dumps(code))
-
- def marshal_load(f):
- if isinstance(f, file):
- return marshal.load(f)
- return marshal.loads(f.read())
-
-
-def with_metaclass(meta, *bases):
- """Create a base class with a metaclass."""
- # This requires a bit of explanation: the basic idea is to make a
- # dummy metaclass for one level of class instantiation that replaces
- # itself with the actual metaclass.
- class metaclass(type):
- def __new__(cls, name, this_bases, d):
- return meta(name, bases, d)
-
- return type.__new__(metaclass, "temporary_class", (), {})
-
-
-try:
- from urllib.parse import quote_from_bytes as url_quote
-except ImportError:
- from urllib import quote as url_quote
-
-
-try:
- from collections import abc
-except ImportError:
- import collections as abc
-
-
-try:
- from os import fspath
-except ImportError:
- try:
- from pathlib import PurePath
- except ImportError:
- PurePath = None
-
- def fspath(path):
- if hasattr(path, "__fspath__"):
- return path.__fspath__()
-
- # Python 3.5 doesn't have __fspath__ yet, use str.
- if PurePath is not None and isinstance(path, PurePath):
- return str(path)
-
- return path
diff --git a/src/jinja2/asyncfilters.py b/src/jinja2/asyncfilters.py
index 3d98dbc..0aad12c 100644
--- a/src/jinja2/asyncfilters.py
+++ b/src/jinja2/asyncfilters.py
@@ -84,7 +84,7 @@ async def do_groupby(environment, value, attribute):
@asyncfiltervariant(filters.do_join)
-async def do_join(eval_ctx, value, d=u"", attribute=None):
+async def do_join(eval_ctx, value, d="", attribute=None):
return filters.do_join(eval_ctx, await auto_to_seq(value), d, attribute)
@@ -146,8 +146,7 @@ ASYNC_FILTERS = {
"groupby": do_groupby,
"join": do_join,
"list": do_list,
- # we intentionally do not support do_last because that would be
- # ridiculous
+ # we intentionally do not support do_last because it may not be safe in async
"reject": do_reject,
"rejectattr": do_rejectattr,
"map": do_map,
diff --git a/src/jinja2/asyncsupport.py b/src/jinja2/asyncsupport.py
index 78ba373..3aef7ad 100644
--- a/src/jinja2/asyncsupport.py
+++ b/src/jinja2/asyncsupport.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-"""The code for async support. Importing this patches Jinja on supported
-Python versions.
-"""
+"""The code for async support. Importing this patches Jinja."""
import asyncio
import inspect
from functools import update_wrapper
@@ -249,16 +246,4 @@ class AsyncLoopContext(LoopContext):
return rv, self
-async def make_async_loop_context(iterable, undefined, recurse=None, depth0=0):
- import warnings
-
- warnings.warn(
- "This template must be recompiled with at least Jinja 2.11, or"
- " it will fail in 3.0.",
- DeprecationWarning,
- stacklevel=2,
- )
- return AsyncLoopContext(iterable, undefined, recurse, depth0)
-
-
patch_all()
diff --git a/src/jinja2/bccache.py b/src/jinja2/bccache.py
index 9c06610..7ddcf40 100644
--- a/src/jinja2/bccache.py
+++ b/src/jinja2/bccache.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
"""The optional bytecode cache system. This is useful if you have very
complex template situations and the compilation of all those templates
slows down your application too much.
@@ -8,22 +7,18 @@ are initialized on the first request.
"""
import errno
import fnmatch
+import marshal
import os
+import pickle
import stat
import sys
import tempfile
from hashlib import sha1
-from os import listdir
-from os import path
-
-from ._compat import BytesIO
-from ._compat import marshal_dump
-from ._compat import marshal_load
-from ._compat import pickle
-from ._compat import text_type
+from io import BytesIO
+
from .utils import open_if_exists
-bc_version = 4
+bc_version = 5
# Magic bytes to identify Jinja bytecode cache files. Contains the
# Python major and minor version to avoid loading incompatible bytecode
# if a project upgrades its Python version.
@@ -34,7 +29,7 @@ bc_magic = (
)
-class Bucket(object):
+class Bucket:
"""Buckets are used to store the bytecode for one template. It's created
and initialized by the bytecode cache and passed to the loading functions.
@@ -67,7 +62,7 @@ class Bucket(object):
return
# if marshal_load fails then we need to reload
try:
- self.code = marshal_load(f)
+ self.code = marshal.load(f)
except (EOFError, ValueError, TypeError):
self.reset()
return
@@ -78,7 +73,7 @@ class Bucket(object):
raise TypeError("can't write empty bucket")
f.write(bc_magic)
pickle.dump(self.checksum, f, 2)
- marshal_dump(self.code, f)
+ marshal.dump(self.code, f)
def bytecode_from_string(self, string):
"""Load bytecode from a string."""
@@ -91,7 +86,7 @@ class Bucket(object):
return out.getvalue()
-class BytecodeCache(object):
+class BytecodeCache:
"""To implement your own bytecode cache you have to subclass this class
and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of
these methods are passed a :class:`~jinja2.bccache.Bucket`.
@@ -145,7 +140,7 @@ class BytecodeCache(object):
hash = sha1(name.encode("utf-8"))
if filename is not None:
filename = "|" + filename
- if isinstance(filename, text_type):
+ if isinstance(filename, str):
filename = filename.encode("utf-8")
hash.update(filename)
return hash.hexdigest()
@@ -209,7 +204,7 @@ class FileSystemBytecodeCache(BytecodeCache):
if not hasattr(os, "getuid"):
_unsafe_dir()
- dirname = "_jinja2-cache-%d" % os.getuid()
+ dirname = f"_jinja2-cache-{os.getuid()}"
actual_dir = os.path.join(tmpdir, dirname)
try:
@@ -241,7 +236,7 @@ class FileSystemBytecodeCache(BytecodeCache):
return actual_dir
def _get_cache_filename(self, bucket):
- return path.join(self.directory, self.pattern % bucket.key)
+ return os.path.join(self.directory, self.pattern % (bucket.key,))
def load_bytecode(self, bucket):
f = open_if_exists(self._get_cache_filename(bucket), "rb")
@@ -264,10 +259,10 @@ class FileSystemBytecodeCache(BytecodeCache):
# normally.
from os import remove
- files = fnmatch.filter(listdir(self.directory), self.pattern % "*")
+ files = fnmatch.filter(os.listdir(self.directory), self.pattern % ("*",))
for filename in files:
try:
- remove(path.join(self.directory, filename))
+ remove(os.path.join(self.directory, filename))
except OSError:
pass
@@ -284,7 +279,7 @@ class MemcachedBytecodeCache(BytecodeCache):
- `python-memcached <https://pypi.org/project/python-memcached/>`_
(Unfortunately the django cache interface is not compatible because it
- does not support storing binary data, only unicode. You can however pass
+ does not support storing binary data, only text. You can however pass
the underlying cache client to the bytecode cache which is available
as `django.core.cache.cache._client`.)
diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py
index 63297b4..045a3a8 100644
--- a/src/jinja2/compiler.py
+++ b/src/jinja2/compiler.py
@@ -1,7 +1,7 @@
-# -*- coding: utf-8 -*-
"""Compiles nodes from the parser into Python code."""
from collections import namedtuple
from functools import update_wrapper
+from io import StringIO
from itertools import chain
from keyword import iskeyword as is_python_keyword
@@ -9,13 +9,6 @@ from markupsafe import escape
from markupsafe import Markup
from . import nodes
-from ._compat import imap
-from ._compat import iteritems
-from ._compat import izip
-from ._compat import NativeStringIO
-from ._compat import range_type
-from ._compat import string_types
-from ._compat import text_type
from .exceptions import TemplateAssertionError
from .idtracking import Symbols
from .idtracking import VAR_LOAD_ALIAS
@@ -38,30 +31,6 @@ operators = {
"notin": "not in",
}
-# what method to iterate over items do we want to use for dict iteration
-# in generated code? on 2.x let's go with iteritems, on 3.x with items
-if hasattr(dict, "iteritems"):
- dict_item_iter = "iteritems"
-else:
- dict_item_iter = "items"
-
-code_features = ["division"]
-
-# does this python version support generator stops? (PEP 0479)
-try:
- exec("from __future__ import generator_stop")
- code_features.append("generator_stop")
-except SyntaxError:
- pass
-
-# does this python version support yield from?
-try:
- exec("def f(): yield from x()")
-except SyntaxError:
- supports_yield_from = False
-else:
- supports_yield_from = True
-
def optimizeconst(f):
def new_func(self, node, frame, **kwargs):
@@ -93,20 +62,16 @@ def has_safe_repr(value):
"""Does the node have a safe representation?"""
if value is None or value is NotImplemented or value is Ellipsis:
return True
- if type(value) in (bool, int, float, complex, range_type, Markup) + string_types:
- return True
- if type(value) in (tuple, list, set, frozenset):
- for item in value:
- if not has_safe_repr(item):
- return False
- return True
- elif type(value) is dict:
- for key, value in iteritems(value):
- if not has_safe_repr(key):
- return False
- if not has_safe_repr(value):
- return False
+
+ if type(value) in {bool, int, float, complex, range, str, Markup}:
return True
+
+ if type(value) in {tuple, list, set, frozenset}:
+ return all(has_safe_repr(v) for v in value)
+
+ if type(value) is dict:
+ return all(has_safe_repr(k) and has_safe_repr(v) for k, v in value.items())
+
return False
@@ -123,7 +88,7 @@ def find_undeclared(nodes, names):
return visitor.undeclared
-class MacroRef(object):
+class MacroRef:
def __init__(self, node):
self.node = node
self.accesses_caller = False
@@ -131,12 +96,12 @@ class MacroRef(object):
self.accesses_varargs = False
-class Frame(object):
+class Frame:
"""Holds compile time information for us."""
def __init__(self, eval_ctx, parent=None, level=None):
self.eval_ctx = eval_ctx
- self.symbols = Symbols(parent and parent.symbols or None, level=level)
+ self.symbols = Symbols(parent.symbols if parent else None, level=level)
# a toplevel frame is the root + soft frames such as if conditions.
self.toplevel = False
@@ -157,7 +122,7 @@ class Frame(object):
self.buffer = None
# the name of the block we're in, otherwise None.
- self.block = parent and parent.block or None
+ self.block = parent.block if parent else None
# the parent of this frame
self.parent = parent
@@ -249,7 +214,7 @@ class CodeGenerator(NodeVisitor):
self, environment, name, filename, stream=None, defer_init=False, optimized=True
):
if stream is None:
- stream = NativeStringIO()
+ stream = StringIO()
self.environment = environment
self.name = name
self.filename = filename
@@ -320,12 +285,12 @@ class CodeGenerator(NodeVisitor):
def temporary_identifier(self):
"""Get a new unique identifier."""
self._last_identifier += 1
- return "t_%d" % self._last_identifier
+ return f"t_{self._last_identifier}"
def buffer(self, frame):
"""Enable buffering for the frame from that point onwards."""
frame.buffer = self.temporary_identifier()
- self.writeline("%s = []" % frame.buffer)
+ self.writeline(f"{frame.buffer} = []")
def return_buffer_contents(self, frame, force_unescaped=False):
"""Return the buffer contents of the frame."""
@@ -333,17 +298,17 @@ class CodeGenerator(NodeVisitor):
if frame.eval_ctx.volatile:
self.writeline("if context.eval_ctx.autoescape:")
self.indent()
- self.writeline("return Markup(concat(%s))" % frame.buffer)
+ self.writeline(f"return Markup(concat({frame.buffer}))")
self.outdent()
self.writeline("else:")
self.indent()
- self.writeline("return concat(%s)" % frame.buffer)
+ self.writeline(f"return concat({frame.buffer})")
self.outdent()
return
elif frame.eval_ctx.autoescape:
- self.writeline("return Markup(concat(%s))" % frame.buffer)
+ self.writeline(f"return Markup(concat({frame.buffer}))")
return
- self.writeline("return concat(%s)" % frame.buffer)
+ self.writeline(f"return concat({frame.buffer})")
def indent(self):
"""Indent by one."""
@@ -358,7 +323,7 @@ class CodeGenerator(NodeVisitor):
if frame.buffer is None:
self.writeline("yield ", node)
else:
- self.writeline("%s.append(" % frame.buffer, node)
+ self.writeline(f"{frame.buffer}.append(", node)
def end_write(self, frame):
"""End the writing process started by `start_write`."""
@@ -432,8 +397,8 @@ class CodeGenerator(NodeVisitor):
self.write(", ")
self.visit(kwarg, frame)
if extra_kwargs is not None:
- for key, value in iteritems(extra_kwargs):
- self.write(", %s=%s" % (key, value))
+ for key, value in extra_kwargs.items():
+ self.write(f", {key}={value}")
if node.dyn_args:
self.write(", *")
self.visit(node.dyn_args, frame)
@@ -444,12 +409,12 @@ class CodeGenerator(NodeVisitor):
else:
self.write(", **{")
for kwarg in node.kwargs:
- self.write("%r: " % kwarg.key)
+ self.write(f"{kwarg.key!r}: ")
self.visit(kwarg.value, frame)
self.write(", ")
if extra_kwargs is not None:
- for key, value in iteritems(extra_kwargs):
- self.write("%r: %s, " % (key, value))
+ for key, value in extra_kwargs.items():
+ self.write(f"{key!r}: {value}, ")
if node.dyn_kwargs is not None:
self.write("}, **")
self.visit(node.dyn_kwargs, frame)
@@ -471,38 +436,36 @@ class CodeGenerator(NodeVisitor):
for name in getattr(visitor, dependency):
if name not in mapping:
mapping[name] = self.temporary_identifier()
- self.writeline(
- "%s = environment.%s[%r]" % (mapping[name], dependency, name)
- )
+ self.writeline(f"{mapping[name]} = environment.{dependency}[{name!r}]")
def enter_frame(self, frame):
undefs = []
- for target, (action, param) in iteritems(frame.symbols.loads):
+ for target, (action, param) in frame.symbols.loads.items():
if action == VAR_LOAD_PARAMETER:
pass
elif action == VAR_LOAD_RESOLVE:
- self.writeline("%s = %s(%r)" % (target, self.get_resolve_func(), param))
+ self.writeline(f"{target} = {self.get_resolve_func()}({param!r})")
elif action == VAR_LOAD_ALIAS:
- self.writeline("%s = %s" % (target, param))
+ self.writeline(f"{target} = {param}")
elif action == VAR_LOAD_UNDEFINED:
undefs.append(target)
else:
raise NotImplementedError("unknown load instruction")
if undefs:
- self.writeline("%s = missing" % " = ".join(undefs))
+ self.writeline(f"{' = '.join(undefs)} = missing")
def leave_frame(self, frame, with_python_scope=False):
if not with_python_scope:
undefs = []
- for target, _ in iteritems(frame.symbols.loads):
+ for target in frame.symbols.loads:
undefs.append(target)
if undefs:
- self.writeline("%s = missing" % " = ".join(undefs))
+ self.writeline(f"{' = '.join(undefs)} = missing")
def func(self, name):
if self.environment.is_async:
- return "async def %s" % name
- return "def %s" % name
+ return f"async def {name}"
+ return f"def {name}"
def macro_body(self, node, frame):
"""Dump the function def of a macro or call block."""
@@ -552,7 +515,7 @@ class CodeGenerator(NodeVisitor):
# macros are delayed, they never require output checks
frame.require_output_check = False
frame.symbols.analyze_node(node)
- self.writeline("%s(%s):" % (self.func("macro"), ", ".join(args)), node)
+ self.writeline(f"{self.func('macro')}({', '.join(args)}):", node)
self.indent()
self.buffer(frame)
@@ -561,17 +524,17 @@ class CodeGenerator(NodeVisitor):
self.push_parameter_definitions(frame)
for idx, arg in enumerate(node.args):
ref = frame.symbols.ref(arg.name)
- self.writeline("if %s is missing:" % ref)
+ self.writeline(f"if {ref} is missing:")
self.indent()
try:
default = node.defaults[idx - len(node.args)]
except IndexError:
self.writeline(
- "%s = undefined(%r, name=%r)"
- % (ref, "parameter %r was not provided" % arg.name, arg.name)
+ f'{ref} = undefined("parameter {arg.name!r} was not provided",'
+ f" name={arg.name!r})"
)
else:
- self.writeline("%s = " % ref)
+ self.writeline(f"{ref} = ")
self.visit(default, frame)
self.mark_parameter_stored(ref)
self.outdent()
@@ -591,29 +554,24 @@ class CodeGenerator(NodeVisitor):
if len(macro_ref.node.args) == 1:
arg_tuple += ","
self.write(
- "Macro(environment, macro, %r, (%s), %r, %r, %r, "
- "context.eval_ctx.autoescape)"
- % (
- name,
- arg_tuple,
- macro_ref.accesses_kwargs,
- macro_ref.accesses_varargs,
- macro_ref.accesses_caller,
- )
+ f"Macro(environment, macro, {name!r}, ({arg_tuple}),"
+ f" {macro_ref.accesses_kwargs!r}, {macro_ref.accesses_varargs!r},"
+ f" {macro_ref.accesses_caller!r}, context.eval_ctx.autoescape)"
)
def position(self, node):
"""Return a human readable position for the node."""
- rv = "line %d" % node.lineno
+ rv = f"line {node.lineno}"
if self.name is not None:
- rv += " in " + repr(self.name)
+ rv = f"{rv} in {self.name!r}"
return rv
def dump_local_context(self, frame):
- return "{%s}" % ", ".join(
- "%r: %s" % (name, target)
- for name, target in iteritems(frame.symbols.dump_stores())
+ items_kv = ", ".join(
+ f"{name!r}: {target}"
+ for name, target in frame.symbols.dump_stores().items()
)
+ return f"{{{items_kv}}}"
def write_commons(self):
"""Writes a common preamble that is used by root and block functions.
@@ -660,13 +618,10 @@ class CodeGenerator(NodeVisitor):
target = self._context_reference_stack[-1]
if target == "context":
return "resolve"
- return "%s.resolve" % target
+ return f"{target}.resolve"
def derive_context(self, frame):
- return "%s.derived(%s)" % (
- self.get_context_ref(),
- self.dump_local_context(frame),
- )
+ return f"{self.get_context_ref()}.derived({self.dump_local_context(frame)})"
def parameter_is_undeclared(self, target):
"""Checks if a given target is an undeclared parameter."""
@@ -689,23 +644,21 @@ class CodeGenerator(NodeVisitor):
if len(vars) == 1:
name = next(iter(vars))
ref = frame.symbols.ref(name)
- self.writeline("context.vars[%r] = %s" % (name, ref))
+ self.writeline(f"context.vars[{name!r}] = {ref}")
else:
self.writeline("context.vars.update({")
for idx, name in enumerate(vars):
if idx:
self.write(", ")
ref = frame.symbols.ref(name)
- self.write("%r: %s" % (name, ref))
+ self.write(f"{name!r}: {ref}")
self.write("})")
if public_names:
if len(public_names) == 1:
- self.writeline("context.exported_vars.add(%r)" % public_names[0])
+ self.writeline(f"context.exported_vars.add({public_names[0]!r})")
else:
- self.writeline(
- "context.exported_vars.update((%s))"
- % ", ".join(imap(repr, public_names))
- )
+ names_str = ", ".join(map(repr, public_names))
+ self.writeline(f"context.exported_vars.update(({names_str}))")
# -- Statement Visitors
@@ -715,7 +668,7 @@ class CodeGenerator(NodeVisitor):
from .runtime import exported
- self.writeline("from __future__ import %s" % ", ".join(code_features))
+ self.writeline("from __future__ import generator_stop") # Python < 3.7
self.writeline("from jinja2.runtime import " + ", ".join(exported))
if self.environment.is_async:
@@ -726,7 +679,7 @@ class CodeGenerator(NodeVisitor):
# if we want a deferred initialization we cannot move the
# environment into a local name
- envenv = not self.defer_init and ", environment=environment" or ""
+ envenv = "" if self.defer_init else ", environment=environment"
# do we have an extends tag at all? If not, we can save some
# overhead by just not processing any inheritance code.
@@ -735,7 +688,7 @@ class CodeGenerator(NodeVisitor):
# find all blocks
for block in node.find_all(nodes.Block):
if block.name in self.blocks:
- self.fail("block %r defined twice" % block.name, block.lineno)
+ self.fail(f"block {block.name!r} defined twice", block.lineno)
self.blocks[block.name] = block
# find all imports and import them
@@ -745,16 +698,16 @@ class CodeGenerator(NodeVisitor):
self.import_aliases[imp] = alias = self.temporary_identifier()
if "." in imp:
module, obj = imp.rsplit(".", 1)
- self.writeline("from %s import %s as %s" % (module, obj, alias))
+ self.writeline(f"from {module} import {obj} as {alias}")
else:
- self.writeline("import %s as %s" % (imp, alias))
+ self.writeline(f"import {imp} as {alias}")
# add the load name
- self.writeline("name = %r" % self.name)
+ self.writeline(f"name = {self.name!r}")
# generate the root render function.
self.writeline(
- "%s(context, missing=missing%s):" % (self.func("root"), envenv), extra=1
+ f"{self.func('root')}(context, missing=missing{envenv}):", extra=1
)
self.indent()
self.write_commons()
@@ -763,7 +716,7 @@ class CodeGenerator(NodeVisitor):
frame = Frame(eval_ctx)
if "self" in find_undeclared(node.body, ("self",)):
ref = frame.symbols.declare_parameter("self")
- self.writeline("%s = TemplateReference(context)" % ref)
+ self.writeline(f"{ref} = TemplateReference(context)")
frame.symbols.analyze_node(node)
frame.toplevel = frame.rootlevel = True
frame.require_output_check = have_extends and not self.has_known_extends
@@ -781,13 +734,12 @@ class CodeGenerator(NodeVisitor):
self.indent()
self.writeline("if parent_template is not None:")
self.indent()
- if supports_yield_from and not self.environment.is_async:
+ if not self.environment.is_async:
self.writeline("yield from parent_template.root_render_func(context)")
else:
+ loop = "async for" if self.environment.is_async else "for"
self.writeline(
- "%sfor event in parent_template."
- "root_render_func(context):"
- % (self.environment.is_async and "async " or "")
+ f"{loop} event in parent_template.root_render_func(context):"
)
self.indent()
self.writeline("yield event")
@@ -795,10 +747,9 @@ class CodeGenerator(NodeVisitor):
self.outdent(1 + (not self.has_known_extends))
# at this point we now have the blocks collected and can visit them too.
- for name, block in iteritems(self.blocks):
+ for name, block in self.blocks.items():
self.writeline(
- "%s(context, missing=missing%s):"
- % (self.func("block_" + name), envenv),
+ f"{self.func('block_' + name)}(context, missing=missing{envenv}):",
block,
1,
)
@@ -811,10 +762,10 @@ class CodeGenerator(NodeVisitor):
undeclared = find_undeclared(block.body, ("self", "super"))
if "self" in undeclared:
ref = block_frame.symbols.declare_parameter("self")
- self.writeline("%s = TemplateReference(context)" % ref)
+ self.writeline(f"{ref} = TemplateReference(context)")
if "super" in undeclared:
ref = block_frame.symbols.declare_parameter("super")
- self.writeline("%s = context.super(%r, block_%s)" % (ref, name, name))
+ self.writeline(f"{ref} = context.super({name!r}, block_{name})")
block_frame.symbols.analyze_node(block)
block_frame.block = name
self.enter_frame(block_frame)
@@ -823,15 +774,10 @@ class CodeGenerator(NodeVisitor):
self.leave_frame(block_frame, with_python_scope=True)
self.outdent()
- self.writeline(
- "blocks = {%s}" % ", ".join("%r: block_%s" % (x, x) for x in self.blocks),
- extra=1,
- )
-
- # add a function that returns the debug info
- self.writeline(
- "debug_info = %r" % "&".join("%s=%s" % x for x in self.debug_info)
- )
+ blocks_kv_str = ", ".join(f"{x!r}: block_{x}" for x in self.blocks)
+ self.writeline(f"blocks = {{{blocks_kv_str}}}", extra=1)
+ debug_kv_str = "&".join(f"{k}={v}" for k, v in self.debug_info)
+ self.writeline(f"debug_info = {debug_kv_str!r}")
def visit_Block(self, node, frame):
"""Call a block and register it for the template."""
@@ -851,19 +797,14 @@ class CodeGenerator(NodeVisitor):
else:
context = self.get_context_ref()
- if (
- supports_yield_from
- and not self.environment.is_async
- and frame.buffer is None
- ):
+ if not self.environment.is_async and frame.buffer is None:
self.writeline(
- "yield from context.blocks[%r][0](%s)" % (node.name, context), node
+ f"yield from context.blocks[{node.name!r}][0]({context})", node
)
else:
- loop = self.environment.is_async and "async for" or "for"
+ loop = "async for" if self.environment.is_async else "for"
self.writeline(
- "%s event in context.blocks[%r][0](%s):" % (loop, node.name, context),
- node,
+ f"{loop} event in context.blocks[{node.name!r}][0]({context}):", node
)
self.indent()
self.simple_write("event", frame)
@@ -888,7 +829,7 @@ class CodeGenerator(NodeVisitor):
if not self.has_known_extends:
self.writeline("if parent_template is not None:")
self.indent()
- self.writeline("raise TemplateRuntimeError(%r)" % "extended multiple times")
+ self.writeline('raise TemplateRuntimeError("extended multiple times")')
# if we have a known extends already we don't need that code here
# as we know that the template execution will end here.
@@ -899,10 +840,8 @@ class CodeGenerator(NodeVisitor):
self.writeline("parent_template = environment.get_template(", node)
self.visit(node.template, frame)
- self.write(", %r)" % self.name)
- self.writeline(
- "for name, parent_block in parent_template.blocks.%s():" % dict_item_iter
- )
+ self.write(f", {self.name!r})")
+ self.writeline("for name, parent_block in parent_template.blocks.items():")
self.indent()
self.writeline("context.blocks.setdefault(name, []).append(parent_block)")
self.outdent()
@@ -924,16 +863,16 @@ class CodeGenerator(NodeVisitor):
func_name = "get_or_select_template"
if isinstance(node.template, nodes.Const):
- if isinstance(node.template.value, string_types):
+ if isinstance(node.template.value, str):
func_name = "get_template"
elif isinstance(node.template.value, (tuple, list)):
func_name = "select_template"
elif isinstance(node.template, (nodes.Tuple, nodes.List)):
func_name = "select_template"
- self.writeline("template = environment.%s(" % func_name, node)
+ self.writeline(f"template = environment.{func_name}(", node)
self.visit(node.template, frame)
- self.write(", %r)" % self.name)
+ self.write(f", {self.name!r})")
if node.ignore_missing:
self.outdent()
self.writeline("except TemplateNotFound:")
@@ -945,26 +884,20 @@ class CodeGenerator(NodeVisitor):
skip_event_yield = False
if node.with_context:
- loop = self.environment.is_async and "async for" or "for"
+ loop = "async for" if self.environment.is_async else "for"
self.writeline(
- "%s event in template.root_render_func("
- "template.new_context(context.get_all(), True, "
- "%s)):" % (loop, self.dump_local_context(frame))
+ f"{loop} event in template.root_render_func("
+ "template.new_context(context.get_all(), True,"
+ f" {self.dump_local_context(frame)})):"
)
elif self.environment.is_async:
self.writeline(
- "for event in (await "
- "template._get_default_module_async())"
+ "for event in (await template._get_default_module_async())"
"._body_stream:"
)
else:
- if supports_yield_from:
- self.writeline("yield from template._get_default_module()._body_stream")
- skip_event_yield = True
- else:
- self.writeline(
- "for event in template._get_default_module()._body_stream:"
- )
+ self.writeline("yield from template._get_default_module()._body_stream")
+ skip_event_yield = True
if not skip_event_yield:
self.indent()
@@ -976,45 +909,37 @@ class CodeGenerator(NodeVisitor):
def visit_Import(self, node, frame):
"""Visit regular imports."""
- self.writeline("%s = " % frame.symbols.ref(node.target), node)
+ self.writeline(f"{frame.symbols.ref(node.target)} = ", node)
if frame.toplevel:
- self.write("context.vars[%r] = " % node.target)
+ self.write(f"context.vars[{node.target!r}] = ")
if self.environment.is_async:
self.write("await ")
self.write("environment.get_template(")
self.visit(node.template, frame)
- self.write(", %r)." % self.name)
+ self.write(f", {self.name!r}).")
if node.with_context:
+ func = "make_module" + ("_async" if self.environment.is_async else "")
self.write(
- "make_module%s(context.get_all(), True, %s)"
- % (
- self.environment.is_async and "_async" or "",
- self.dump_local_context(frame),
- )
+ f"{func}(context.get_all(), True, {self.dump_local_context(frame)})"
)
elif self.environment.is_async:
self.write("_get_default_module_async()")
else:
self.write("_get_default_module()")
if frame.toplevel and not node.target.startswith("_"):
- self.writeline("context.exported_vars.discard(%r)" % node.target)
+ self.writeline(f"context.exported_vars.discard({node.target!r})")
def visit_FromImport(self, node, frame):
"""Visit named imports."""
self.newline(node)
- self.write(
- "included_template = %senvironment.get_template("
- % (self.environment.is_async and "await " or "")
- )
+ prefix = "await " if self.environment.is_async else ""
+ self.write(f"included_template = {prefix}environment.get_template(")
self.visit(node.template, frame)
- self.write(", %r)." % self.name)
+ self.write(f", {self.name!r}).")
if node.with_context:
+ func = "make_module" + ("_async" if self.environment.is_async else "")
self.write(
- "make_module%s(context.get_all(), True, %s)"
- % (
- self.environment.is_async and "_async" or "",
- self.dump_local_context(frame),
- )
+ f"{func}(context.get_all(), True, {self.dump_local_context(frame)})"
)
elif self.environment.is_async:
self.write("_get_default_module_async()")
@@ -1029,22 +954,18 @@ class CodeGenerator(NodeVisitor):
else:
alias = name
self.writeline(
- "%s = getattr(included_template, "
- "%r, missing)" % (frame.symbols.ref(alias), name)
+ f"{frame.symbols.ref(alias)} ="
+ f" getattr(included_template, {name!r}, missing)"
)
- self.writeline("if %s is missing:" % frame.symbols.ref(alias))
+ self.writeline(f"if {frame.symbols.ref(alias)} is missing:")
self.indent()
+ message = (
+ "the template {included_template.__name__!r}"
+ f" (imported on {self.position(node)})"
+ f" does not export the requested name {name!r}"
+ )
self.writeline(
- "%s = undefined(%r %% "
- "included_template.__name__, "
- "name=%r)"
- % (
- frame.symbols.ref(alias),
- "the template %%r (imported on %s) does "
- "not export the requested name %s"
- % (self.position(node), repr(name)),
- name,
- )
+ f"{frame.symbols.ref(alias)} = undefined(f{message!r}, name={name!r})"
)
self.outdent()
if frame.toplevel:
@@ -1055,23 +976,19 @@ class CodeGenerator(NodeVisitor):
if var_names:
if len(var_names) == 1:
name = var_names[0]
- self.writeline(
- "context.vars[%r] = %s" % (name, frame.symbols.ref(name))
- )
+ self.writeline(f"context.vars[{name!r}] = {frame.symbols.ref(name)}")
else:
- self.writeline(
- "context.vars.update({%s})"
- % ", ".join(
- "%r: %s" % (name, frame.symbols.ref(name)) for name in var_names
- )
+ names_kv = ", ".join(
+ f"{name!r}: {frame.symbols.ref(name)}" for name in var_names
)
+ self.writeline(f"context.vars.update({{{names_kv}}})")
if discarded_names:
if len(discarded_names) == 1:
- self.writeline("context.exported_vars.discard(%r)" % discarded_names[0])
+ self.writeline(f"context.exported_vars.discard({discarded_names[0]!r})")
else:
+ names_str = ", ".join(map(repr, discarded_names))
self.writeline(
- "context.exported_vars.difference_"
- "update((%s))" % ", ".join(imap(repr, discarded_names))
+ f"context.exported_vars.difference_update(({names_str}))"
)
def visit_For(self, node, frame):
@@ -1097,13 +1014,13 @@ class CodeGenerator(NodeVisitor):
if node.test:
loop_filter_func = self.temporary_identifier()
test_frame.symbols.analyze_node(node, for_branch="test")
- self.writeline("%s(fiter):" % self.func(loop_filter_func), node.test)
+ self.writeline(f"{self.func(loop_filter_func)}(fiter):", node.test)
self.indent()
self.enter_frame(test_frame)
- self.writeline(self.environment.is_async and "async for " or "for ")
+ self.writeline("async for " if self.environment.is_async else "for ")
self.visit(node.target, loop_frame)
self.write(" in ")
- self.write(self.environment.is_async and "auto_aiter(fiter)" or "fiter")
+ self.write("auto_aiter(fiter)" if self.environment.is_async else "fiter")
self.write(":")
self.indent()
self.writeline("if ", node.test)
@@ -1120,7 +1037,7 @@ class CodeGenerator(NodeVisitor):
# variable is a special one we have to enforce aliasing for it.
if node.recursive:
self.writeline(
- "%s(reciter, loop_render_func, depth=0):" % self.func("loop"), node
+ f"{self.func('loop')}(reciter, loop_render_func, depth=0):", node
)
self.indent()
self.buffer(loop_frame)
@@ -1131,7 +1048,7 @@ class CodeGenerator(NodeVisitor):
# make sure the loop variable is a special one and raise a template
# assertion error if a loop tries to write to loop
if extended_loop:
- self.writeline("%s = missing" % loop_ref)
+ self.writeline(f"{loop_ref} = missing")
for name in node.find_all(nodes.Name):
if name.ctx == "store" and name.name == "loop":
@@ -1142,20 +1059,18 @@ class CodeGenerator(NodeVisitor):
if node.else_:
iteration_indicator = self.temporary_identifier()
- self.writeline("%s = 1" % iteration_indicator)
+ self.writeline(f"{iteration_indicator} = 1")
- self.writeline(self.environment.is_async and "async for " or "for ", node)
+ self.writeline("async for " if self.environment.is_async else "for ", node)
self.visit(node.target, loop_frame)
if extended_loop:
- if self.environment.is_async:
- self.write(", %s in AsyncLoopContext(" % loop_ref)
- else:
- self.write(", %s in LoopContext(" % loop_ref)
+ prefix = "Async" if self.environment.is_async else ""
+ self.write(f", {loop_ref} in {prefix}LoopContext(")
else:
self.write(" in ")
if node.test:
- self.write("%s(" % loop_filter_func)
+ self.write(f"{loop_filter_func}(")
if node.recursive:
self.write("reciter")
else:
@@ -1170,21 +1085,21 @@ class CodeGenerator(NodeVisitor):
if node.recursive:
self.write(", undefined, loop_render_func, depth):")
else:
- self.write(extended_loop and ", undefined):" or ":")
+ self.write(", undefined):" if extended_loop else ":")
self.indent()
self.enter_frame(loop_frame)
self.blockvisit(node.body, loop_frame)
if node.else_:
- self.writeline("%s = 0" % iteration_indicator)
+ self.writeline(f"{iteration_indicator} = 0")
self.outdent()
self.leave_frame(
loop_frame, with_python_scope=node.recursive and not node.else_
)
if node.else_:
- self.writeline("if %s:" % iteration_indicator)
+ self.writeline(f"if {iteration_indicator}:")
self.indent()
self.enter_frame(else_frame)
self.blockvisit(node.else_, else_frame)
@@ -1234,9 +1149,9 @@ class CodeGenerator(NodeVisitor):
self.newline()
if frame.toplevel:
if not node.name.startswith("_"):
- self.write("context.exported_vars.add(%r)" % node.name)
- self.writeline("context.vars[%r] = " % node.name)
- self.write("%s = " % frame.symbols.ref(node.name))
+ self.write(f"context.exported_vars.add({node.name!r})")
+ self.writeline(f"context.vars[{node.name!r}] = ")
+ self.write(f"{frame.symbols.ref(node.name)} = ")
self.macro_def(macro_ref, macro_frame)
def visit_CallBlock(self, node, frame):
@@ -1262,7 +1177,7 @@ class CodeGenerator(NodeVisitor):
with_frame = frame.inner()
with_frame.symbols.analyze_node(node)
self.enter_frame(with_frame)
- for target, expr in izip(node.targets, node.values):
+ for target, expr in zip(node.targets, node.values):
self.newline()
self.visit(target, with_frame)
self.write(" = ")
@@ -1278,7 +1193,7 @@ class CodeGenerator(NodeVisitor):
#: The default finalize function if the environment isn't configured
#: with one. Or if the environment has one, this is called on that
#: function's output for constants.
- _default_finalize = text_type
+ _default_finalize = str
_finalize = None
def _make_finalize(self):
@@ -1344,7 +1259,7 @@ class CodeGenerator(NodeVisitor):
# Template data doesn't go through finalize.
if isinstance(node, nodes.TemplateData):
- return text_type(const)
+ return str(const)
return finalize.const(const)
@@ -1353,11 +1268,11 @@ class CodeGenerator(NodeVisitor):
``Output`` node.
"""
if frame.eval_ctx.volatile:
- self.write("(escape if context.eval_ctx.autoescape else to_string)(")
+ self.write("(escape if context.eval_ctx.autoescape else str)(")
elif frame.eval_ctx.autoescape:
self.write("escape(")
else:
- self.write("to_string(")
+ self.write("str(")
if finalize.src is not None:
self.write(finalize.src)
@@ -1414,9 +1329,9 @@ class CodeGenerator(NodeVisitor):
if frame.buffer is not None:
if len(body) == 1:
- self.writeline("%s.append(" % frame.buffer)
+ self.writeline(f"{frame.buffer}.append(")
else:
- self.writeline("%s.extend((" % frame.buffer)
+ self.writeline(f"{frame.buffer}.extend((")
self.indent()
@@ -1475,7 +1390,7 @@ class CodeGenerator(NodeVisitor):
if node.filter is not None:
self.visit_Filter(node.filter, block_frame)
else:
- self.write("concat(%s)" % block_frame.buffer)
+ self.write(f"concat({block_frame.buffer})")
self.write(")")
self.pop_assign_tracking(frame)
self.leave_frame(block_frame)
@@ -1499,8 +1414,7 @@ class CodeGenerator(NodeVisitor):
and not self.parameter_is_undeclared(ref)
):
self.write(
- "(undefined(name=%r) if %s is missing else %s)"
- % (node.name, ref, ref)
+ f"(undefined(name={node.name!r}) if {ref} is missing else {ref})"
)
return
@@ -1511,14 +1425,14 @@ class CodeGenerator(NodeVisitor):
# `foo.bar` notation they will be parsed as a normal attribute access
# when used anywhere but in a `set` context
ref = frame.symbols.ref(node.name)
- self.writeline("if not isinstance(%s, Namespace):" % ref)
+ self.writeline(f"if not isinstance({ref}, Namespace):")
self.indent()
self.writeline(
- "raise TemplateRuntimeError(%r)"
- % "cannot assign attribute on non-namespace object"
+ "raise TemplateRuntimeError"
+ '("cannot assign attribute on non-namespace object")'
)
self.outdent()
- self.writeline("%s[%r]" % (ref, node.attr))
+ self.writeline(f"{ref}[{node.attr!r}]")
def visit_Const(self, node, frame):
val = node.as_const(frame.eval_ctx)
@@ -1532,7 +1446,7 @@ class CodeGenerator(NodeVisitor):
self.write(repr(node.as_const(frame.eval_ctx)))
except nodes.Impossible:
self.write(
- "(Markup if context.eval_ctx.autoescape else identity)(%r)" % node.data
+ f"(Markup if context.eval_ctx.autoescape else identity)({node.data!r})"
)
def visit_Tuple(self, node, frame):
@@ -1542,7 +1456,7 @@ class CodeGenerator(NodeVisitor):
if idx:
self.write(", ")
self.visit(item, frame)
- self.write(idx == 0 and ",)" or ")")
+ self.write(",)" if idx == 0 else ")")
def visit_List(self, node, frame):
self.write("[")
@@ -1569,14 +1483,14 @@ class CodeGenerator(NodeVisitor):
self.environment.sandboxed
and operator in self.environment.intercepted_binops
):
- self.write("environment.call_binop(context, %r, " % operator)
+ self.write(f"environment.call_binop(context, {operator!r}, ")
self.visit(node.left, frame)
self.write(", ")
self.visit(node.right, frame)
else:
self.write("(")
self.visit(node.left, frame)
- self.write(" %s " % operator)
+ self.write(f" {operator} ")
self.visit(node.right, frame)
self.write(")")
@@ -1589,7 +1503,7 @@ class CodeGenerator(NodeVisitor):
self.environment.sandboxed
and operator in self.environment.intercepted_unops
):
- self.write("environment.call_unop(context, %r, " % operator)
+ self.write(f"environment.call_unop(context, {operator!r}, ")
self.visit(node.node, frame)
else:
self.write("(" + operator)
@@ -1615,12 +1529,12 @@ class CodeGenerator(NodeVisitor):
@optimizeconst
def visit_Concat(self, node, frame):
if frame.eval_ctx.volatile:
- func_name = "(context.eval_ctx.volatile and markup_join or unicode_join)"
+ func_name = "(markup_join if context.eval_ctx.volatile else str_join)"
elif frame.eval_ctx.autoescape:
func_name = "markup_join"
else:
- func_name = "unicode_join"
- self.write("%s((" % func_name)
+ func_name = "str_join"
+ self.write(f"{func_name}((")
for arg in node.nodes:
self.visit(arg, frame)
self.write(", ")
@@ -1635,7 +1549,7 @@ class CodeGenerator(NodeVisitor):
self.write(")")
def visit_Operand(self, node, frame):
- self.write(" %s " % operators[node.op])
+ self.write(f" {operators[node.op]} ")
self.visit(node.expr, frame)
@optimizeconst
@@ -1645,7 +1559,7 @@ class CodeGenerator(NodeVisitor):
self.write("environment.getattr(")
self.visit(node.node, frame)
- self.write(", %r)" % node.attr)
+ self.write(f", {node.attr!r})")
if self.environment.is_async:
self.write("))")
@@ -1688,7 +1602,7 @@ class CodeGenerator(NodeVisitor):
self.write(self.filters[node.name] + "(")
func = self.environment.filters.get(node.name)
if func is None:
- self.fail("no filter named %r" % node.name, node.lineno)
+ self.fail(f"no filter named {node.name!r}", node.lineno)
if getattr(func, "contextfilter", False) is True:
self.write("context, ")
elif getattr(func, "evalcontextfilter", False) is True:
@@ -1702,13 +1616,13 @@ class CodeGenerator(NodeVisitor):
self.visit(node.node, frame)
elif frame.eval_ctx.volatile:
self.write(
- "(context.eval_ctx.autoescape and"
- " Markup(concat(%s)) or concat(%s))" % (frame.buffer, frame.buffer)
+ f"(Markup(concat({frame.buffer}))"
+ f" if context.eval_ctx.autoescape else concat({frame.buffer}))"
)
elif frame.eval_ctx.autoescape:
- self.write("Markup(concat(%s))" % frame.buffer)
+ self.write(f"Markup(concat({frame.buffer}))")
else:
- self.write("concat(%s)" % frame.buffer)
+ self.write(f"concat({frame.buffer})")
self.signature(node, frame)
self.write(")")
if self.environment.is_async:
@@ -1718,7 +1632,7 @@ class CodeGenerator(NodeVisitor):
def visit_Test(self, node, frame):
self.write(self.tests[node.name] + "(")
if node.name not in self.environment.tests:
- self.fail("no test named %r" % node.name, node.lineno)
+ self.fail(f"no test named {node.name!r}", node.lineno)
self.visit(node.node, frame)
self.signature(node, frame)
self.write(")")
@@ -1729,12 +1643,9 @@ class CodeGenerator(NodeVisitor):
if node.expr2 is not None:
return self.visit(node.expr2, frame)
self.write(
- "cond_expr_undefined(%r)"
- % (
- "the inline if-"
- "expression on %s evaluated to false and "
- "no else section was defined." % self.position(node)
- )
+ f'cond_expr_undefined("the inline if-expression on'
+ f" {self.position(node)} evaluated to false and no else"
+ f' section was defined.")'
)
self.write("(")
@@ -1754,7 +1665,7 @@ class CodeGenerator(NodeVisitor):
else:
self.write("context.call(")
self.visit(node.node, frame)
- extra_kwargs = forward_caller and {"caller": "caller"} or None
+ extra_kwargs = {"caller": "caller"} if forward_caller else None
self.signature(node, frame, extra_kwargs)
self.write(")")
if self.environment.is_async:
@@ -1772,7 +1683,7 @@ class CodeGenerator(NodeVisitor):
self.write(")")
def visit_MarkSafeIfAutoescape(self, node, frame):
- self.write("(context.eval_ctx.autoescape and Markup or identity)(")
+ self.write("(Markup if context.eval_ctx.autoescape else identity)(")
self.visit(node.expr, frame)
self.write(")")
@@ -1780,7 +1691,7 @@ class CodeGenerator(NodeVisitor):
self.write("environment." + node.name)
def visit_ExtensionAttribute(self, node, frame):
- self.write("environment.extensions[%r].%s" % (node.identifier, node.name))
+ self.write(f"environment.extensions[{node.identifier!r}].{node.name}")
def visit_ImportedName(self, node, frame):
self.write(self.import_aliases[node.importname])
@@ -1809,8 +1720,8 @@ class CodeGenerator(NodeVisitor):
def visit_OverlayScope(self, node, frame):
ctx = self.temporary_identifier()
- self.writeline("%s = %s" % (ctx, self.derive_context(frame)))
- self.writeline("%s.vars = " % ctx)
+ self.writeline(f"{ctx} = {self.derive_context(frame)}")
+ self.writeline(f"{ctx}.vars = ")
self.visit(node.context, frame)
self.push_context_reference(ctx)
@@ -1823,7 +1734,7 @@ class CodeGenerator(NodeVisitor):
def visit_EvalContextModifier(self, node, frame):
for keyword in node.options:
- self.writeline("context.eval_ctx.%s = " % keyword.key)
+ self.writeline(f"context.eval_ctx.{keyword.key} = ")
self.visit(keyword.value, frame)
try:
val = keyword.value.as_const(frame.eval_ctx)
@@ -1835,9 +1746,9 @@ class CodeGenerator(NodeVisitor):
def visit_ScopedEvalContextModifier(self, node, frame):
old_ctx_name = self.temporary_identifier()
saved_ctx = frame.eval_ctx.save()
- self.writeline("%s = context.eval_ctx.save()" % old_ctx_name)
+ self.writeline(f"{old_ctx_name} = context.eval_ctx.save()")
self.visit_EvalContextModifier(node, frame)
for child in node.body:
self.visit(child, frame)
frame.eval_ctx.revert(saved_ctx)
- self.writeline("context.eval_ctx.revert(%s)" % old_ctx_name)
+ self.writeline(f"context.eval_ctx.revert({old_ctx_name})")
diff --git a/src/jinja2/constants.py b/src/jinja2/constants.py
index bf7f2ca..41a1c23 100644
--- a/src/jinja2/constants.py
+++ b/src/jinja2/constants.py
@@ -1,6 +1,5 @@
-# -*- coding: utf-8 -*-
#: list of lorem ipsum words used by the lipsum() helper function
-LOREM_IPSUM_WORDS = u"""\
+LOREM_IPSUM_WORDS = """\
a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at
auctor augue bibendum blandit class commodo condimentum congue consectetuer
consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus
diff --git a/src/jinja2/debug.py b/src/jinja2/debug.py
index 5d8aec3..5cac28b 100644
--- a/src/jinja2/debug.py
+++ b/src/jinja2/debug.py
@@ -1,8 +1,8 @@
+import platform
import sys
from types import CodeType
from . import TemplateSyntaxError
-from ._compat import PYPY
from .utils import internal_code
from .utils import missing
@@ -14,25 +14,18 @@ def rewrite_traceback_stack(source=None):
This must be called within an ``except`` block.
- :param exc_info: A :meth:`sys.exc_info` tuple. If not provided,
- the current ``exc_info`` is used.
:param source: For ``TemplateSyntaxError``, the original source if
known.
- :return: A :meth:`sys.exc_info` tuple that can be re-raised.
+ :return: The original exception with the rewritten traceback.
"""
- exc_type, exc_value, tb = sys.exc_info()
+ _, exc_value, tb = sys.exc_info()
if isinstance(exc_value, TemplateSyntaxError) and not exc_value.translated:
exc_value.translated = True
exc_value.source = source
-
- try:
- # Remove the old traceback on Python 3, otherwise the frames
- # from the compiler still show up.
- exc_value.with_traceback(None)
- except AttributeError:
- pass
-
+ # Remove the old traceback, otherwise the frames from the
+ # compiler still show up.
+ exc_value.with_traceback(None)
# Outside of runtime, so the frame isn't executing template
# code, but it still needs to point at the template.
tb = fake_traceback(
@@ -70,7 +63,7 @@ def rewrite_traceback_stack(source=None):
for tb in reversed(stack):
tb_next = tb_set_next(tb, tb_next)
- return exc_type, exc_value, tb_next
+ return exc_value.with_traceback(tb_next)
def fake_traceback(exc_value, tb, filename, lineno):
@@ -113,7 +106,7 @@ def fake_traceback(exc_value, tb, filename, lineno):
if function == "root":
location = "top-level template code"
elif function.startswith("block_"):
- location = 'block "%s"' % function[6:]
+ location = f"block {function[6:]!r}"
# Collect arguments for the new code object. CodeType only
# accepts positional arguments, and arguments were inserted in
@@ -123,7 +116,7 @@ def fake_traceback(exc_value, tb, filename, lineno):
for attr in (
"argcount",
"posonlyargcount", # Python 3.8
- "kwonlyargcount", # Python 3
+ "kwonlyargcount",
"nlocals",
"stacksize",
"flags",
@@ -215,7 +208,7 @@ if sys.version_info >= (3, 7):
return tb
-elif PYPY:
+elif platform.python_implementation() == "PyPy":
# PyPy might have special support, and won't work with ctypes.
try:
import tputil
diff --git a/src/jinja2/defaults.py b/src/jinja2/defaults.py
index 8e0e7d7..1f0b0ab 100644
--- a/src/jinja2/defaults.py
+++ b/src/jinja2/defaults.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-from ._compat import range_type
from .filters import FILTERS as DEFAULT_FILTERS # noqa: F401
from .tests import TESTS as DEFAULT_TESTS # noqa: F401
from .utils import Cycler
@@ -24,7 +22,7 @@ KEEP_TRAILING_NEWLINE = False
# default filters, tests and namespace
DEFAULT_NAMESPACE = {
- "range": range_type,
+ "range": range,
"dict": dict,
"lipsum": generate_lorem_ipsum,
"cycler": Cycler,
diff --git a/src/jinja2/environment.py b/src/jinja2/environment.py
index 8430390..3c93c48 100644
--- a/src/jinja2/environment.py
+++ b/src/jinja2/environment.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
"""Classes for managing templates and their runtime and compile time
options.
"""
@@ -11,15 +10,6 @@ from functools import reduce
from markupsafe import Markup
from . import nodes
-from ._compat import encode_filename
-from ._compat import implements_iterator
-from ._compat import implements_to_string
-from ._compat import iteritems
-from ._compat import PY2
-from ._compat import PYPY
-from ._compat import reraise
-from ._compat import string_types
-from ._compat import text_type
from .compiler import CodeGenerator
from .compiler import generate
from .defaults import BLOCK_END_STRING
@@ -104,19 +94,20 @@ def load_extensions(environment, extensions):
"""
result = {}
for extension in extensions:
- if isinstance(extension, string_types):
+ if isinstance(extension, str):
extension = import_string(extension)
result[extension.identifier] = extension(environment)
return result
-def fail_for_missing_callable(string, name):
- msg = string % name
+def fail_for_missing_callable(thing, name):
+ msg = f"no {thing} named {name!r}"
+
if isinstance(name, Undefined):
try:
name._fail_with_undefined_error()
except Exception as e:
- msg = "%s (%s; did you forget to quote the callable name?)" % (msg, e)
+ msg = f"{msg} ({e}; did you forget to quote the callable name?)"
raise TemplateRuntimeError(msg)
@@ -130,15 +121,15 @@ def _environment_sanity_check(environment):
!= environment.variable_start_string
!= environment.comment_start_string
), "block, variable and comment start strings must be different"
- assert environment.newline_sequence in (
+ assert environment.newline_sequence in {
"\r",
"\r\n",
"\n",
- ), "newline_sequence set to unknown line ending string."
+ }, "newline_sequence set to unknown line ending string."
return environment
-class Environment(object):
+class Environment:
r"""The core component of Jinja is the `Environment`. It contains
important shared variables like configuration, filters, tests,
globals and others. Instances of this class may be modified if
@@ -256,9 +247,8 @@ class Environment(object):
See :ref:`bytecode-cache` for more information.
`enable_async`
- If set to true this enables async template execution which allows
- you to take advantage of newer Python features. This requires
- Python 3.6 or later.
+ If set to true this enables async template execution which
+ allows using async functions and generators.
"""
#: if this environment is sandboxed. Modifying this variable won't make
@@ -378,7 +368,7 @@ class Environment(object):
yet. This is used by :ref:`extensions <writing-extensions>` to register
callbacks and configuration values without breaking inheritance.
"""
- for key, value in iteritems(attributes):
+ for key, value in attributes.items():
if not hasattr(self, key):
setattr(self, key, value)
@@ -423,7 +413,7 @@ class Environment(object):
rv.overlayed = True
rv.linked_to = self
- for key, value in iteritems(args):
+ for key, value in args.items():
if value is not missing:
setattr(rv, key, value)
@@ -433,7 +423,7 @@ class Environment(object):
rv.cache = copy_cache(self.cache)
rv.extensions = {}
- for key, value in iteritems(self.extensions):
+ for key, value in self.extensions.items():
rv.extensions[key] = value.bind(rv)
if extensions is not missing:
rv.extensions.update(load_extensions(rv, extensions))
@@ -451,7 +441,7 @@ class Environment(object):
try:
return obj[argument]
except (AttributeError, TypeError, LookupError):
- if isinstance(argument, string_types):
+ if isinstance(argument, str):
try:
attr = str(argument)
except Exception:
@@ -479,18 +469,17 @@ class Environment(object):
def call_filter(
self, name, value, args=None, kwargs=None, context=None, eval_ctx=None
):
- """Invokes a filter on a value the same way the compiler does it.
+ """Invokes a filter on a value the same way the compiler does.
- Note that on Python 3 this might return a coroutine in case the
- filter is running from an environment in async mode and the filter
- supports async execution. It's your responsibility to await this
- if needed.
+ This might return a coroutine if the filter is running from an
+ environment in async mode and the filter supports async
+ execution. It's your responsibility to await this if needed.
.. versionadded:: 2.7
"""
func = self.filters.get(name)
if func is None:
- fail_for_missing_callable("no filter named %r", name)
+ fail_for_missing_callable("filter", name)
args = [value] + list(args or ())
if getattr(func, "contextfilter", False) is True:
if context is None:
@@ -516,7 +505,7 @@ class Environment(object):
"""
func = self.tests.get(name)
if func is None:
- fail_for_missing_callable("no test named %r", name)
+ fail_for_missing_callable("test", name)
return func(value, *(args or ()), **(kwargs or {}))
@internalcode
@@ -536,7 +525,7 @@ class Environment(object):
def _parse(self, source, name, filename):
"""Internal parsing function used by `parse` and `compile`."""
- return Parser(self, source, name, encode_filename(filename)).parse()
+ return Parser(self, source, name, filename).parse()
def lex(self, source, name=None, filename=None):
"""Lex the given sourcecode and return a generator that yields
@@ -548,7 +537,7 @@ class Environment(object):
of the extensions to be applied you have to filter source through
the :meth:`preprocess` method.
"""
- source = text_type(source)
+ source = str(source)
try:
return self.lexer.tokeniter(source, name, filename)
except TemplateSyntaxError:
@@ -562,7 +551,7 @@ class Environment(object):
return reduce(
lambda s, e: e.preprocess(s, name, filename),
self.iter_extensions(),
- text_type(source),
+ str(source),
)
def _tokenize(self, source, name, filename=None, state=None):
@@ -623,7 +612,7 @@ class Environment(object):
"""
source_hint = None
try:
- if isinstance(source, string_types):
+ if isinstance(source, str):
source_hint = source
source = self._parse(source, name, filename)
source = self._generate(source, name, filename, defer_init=defer_init)
@@ -631,8 +620,6 @@ class Environment(object):
return source
if filename is None:
filename = "<template>"
- else:
- filename = encode_filename(filename)
return self._compile(source, filename)
except TemplateSyntaxError:
self.handle_exception(source=source_hint)
@@ -689,7 +676,6 @@ class Environment(object):
zip="deflated",
log_function=None,
ignore_errors=True,
- py_compile=False,
):
"""Finds all the templates the loader can find, compiles them
and stores them in `target`. If `zip` is `None`, instead of in a
@@ -706,11 +692,6 @@ class Environment(object):
syntax errors to abort the compilation you can set `ignore_errors`
to `False` and you will get an exception on syntax errors.
- If `py_compile` is set to `True` .pyc files will be written to the
- target instead of standard .py files. This flag does not do anything
- on pypy and Python 3 where pyc files are not picked up by itself and
- don't give much benefit.
-
.. versionadded:: 2.4
"""
from .loaders import ModuleLoader
@@ -720,34 +701,13 @@ class Environment(object):
def log_function(x):
pass
- if py_compile:
- if not PY2 or PYPY:
- import warnings
-
- warnings.warn(
- "'py_compile=True' has no effect on PyPy or Python"
- " 3 and will be removed in version 3.0",
- DeprecationWarning,
- stacklevel=2,
- )
- py_compile = False
- else:
- import imp
- import marshal
-
- py_header = imp.get_magic() + u"\xff\xff\xff\xff".encode("iso-8859-15")
-
- # Python 3.3 added a source filesize to the header
- if sys.version_info >= (3, 3):
- py_header += u"\x00\x00\x00\x00".encode("iso-8859-15")
-
def write_file(filename, data):
if zip:
info = ZipInfo(filename)
info.external_attr = 0o755 << 16
zip_file.writestr(info, data)
else:
- if isinstance(data, text_type):
+ if isinstance(data, str):
data = data.encode("utf8")
with open(os.path.join(target, filename), "wb") as f:
@@ -759,11 +719,11 @@ class Environment(object):
zip_file = ZipFile(
target, "w", dict(deflated=ZIP_DEFLATED, stored=ZIP_STORED)[zip]
)
- log_function('Compiling into Zip archive "%s"' % target)
+ log_function(f"Compiling into Zip archive {target!r}")
else:
if not os.path.isdir(target):
os.makedirs(target)
- log_function('Compiling into folder "%s"' % target)
+ log_function(f"Compiling into folder {target!r}")
try:
for name in self.list_templates(extensions, filter_func):
@@ -773,18 +733,13 @@ class Environment(object):
except TemplateSyntaxError as e:
if not ignore_errors:
raise
- log_function('Could not compile "%s": %s' % (name, e))
+ log_function(f'Could not compile "{name}": {e}')
continue
filename = ModuleLoader.get_module_filename(name)
- if py_compile:
- c = self._compile(code, encode_filename(filename))
- write_file(filename + "c", py_header + marshal.dumps(c))
- log_function('Byte-compiled "%s" as %s' % (name, filename + "c"))
- else:
- write_file(filename, code)
- log_function('Compiled "%s" as %s' % (name, filename))
+ write_file(filename, code)
+ log_function(f'Compiled "{name}" as {filename}')
finally:
if zip:
zip_file.close()
@@ -829,7 +784,7 @@ class Environment(object):
"""
from .debug import rewrite_traceback_stack
- reraise(*rewrite_traceback_stack(source=source))
+ raise rewrite_traceback_stack(source=source)
def join_path(self, template, parent):
"""Join a template with the parent. By default all the lookups are
@@ -904,7 +859,7 @@ class Environment(object):
if not names:
raise TemplatesNotFound(
- message=u"Tried to select from an empty list " u"of templates."
+ message="Tried to select from an empty list of templates."
)
globals = self.make_globals(globals)
for name in names:
@@ -926,7 +881,7 @@ class Environment(object):
.. versionadded:: 2.3
"""
- if isinstance(template_name_or_list, (string_types, Undefined)):
+ if isinstance(template_name_or_list, (str, Undefined)):
return self.get_template(template_name_or_list, parent, globals)
elif isinstance(template_name_or_list, Template):
return template_name_or_list
@@ -947,7 +902,7 @@ class Environment(object):
return dict(self.globals, **d)
-class Template(object):
+class Template:
"""The central template object. This class represents a compiled template
and is used to evaluate it.
@@ -1081,7 +1036,7 @@ class Template(object):
template.render(knights='that say nih')
template.render({'knights': 'that say nih'})
- This will return the rendered template as unicode string.
+ This will return the rendered template as a string.
"""
vars = dict(*args, **kwargs)
try:
@@ -1113,14 +1068,13 @@ class Template(object):
"""For very large templates it can be useful to not render the whole
template at once but evaluate each statement after another and yield
piece for piece. This method basically does exactly that and returns
- a generator that yields one item after another as unicode strings.
+ a generator that yields one item after another as strings.
It accepts the same arguments as :meth:`render`.
"""
vars = dict(*args, **kwargs)
try:
- for event in self.root_render_func(self.new_context(vars)):
- yield event
+ yield from self.root_render_func(self.new_context(vars))
except Exception:
yield self.environment.handle_exception()
@@ -1213,17 +1167,16 @@ class Template(object):
def __repr__(self):
if self.name is None:
- name = "memory:%x" % id(self)
+ name = f"memory:{id(self):x}"
else:
name = repr(self.name)
- return "<%s %s>" % (self.__class__.__name__, name)
+ return f"<{self.__class__.__name__} {name}>"
-@implements_to_string
-class TemplateModule(object):
+class TemplateModule:
"""Represents an imported template. All the exported names of the
template are available as attributes on this object. Additionally
- converting it into an unicode- or bytestrings renders the contents.
+ converting it into a string renders the contents.
"""
def __init__(self, template, context, body_stream=None):
@@ -1248,13 +1201,13 @@ class TemplateModule(object):
def __repr__(self):
if self.__name__ is None:
- name = "memory:%x" % id(self)
+ name = f"memory:{id(self):x}"
else:
name = repr(self.__name__)
- return "<%s %s>" % (self.__class__.__name__, name)
+ return f"<{self.__class__.__name__} {name}>"
-class TemplateExpression(object):
+class TemplateExpression:
"""The :meth:`jinja2.Environment.compile_expression` method returns an
instance of this object. It encapsulates the expression-like access
to the template with an expression it wraps.
@@ -1273,15 +1226,14 @@ class TemplateExpression(object):
return rv
-@implements_iterator
-class TemplateStream(object):
+class TemplateStream:
"""A template stream works pretty much like an ordinary python generator
but it can buffer multiple items to reduce the number of total iterations.
Per default the output is unbuffered which means that for every unbuffered
- instruction in the template one unicode string is yielded.
+ instruction in the template one string is yielded.
If buffering is enabled with a buffer size of 5, five items are combined
- into a new unicode string. This is mainly useful if you are streaming
+ into a new string. This is mainly useful if you are streaming
big templates to a client via WSGI which flushes after each iteration.
"""
@@ -1291,7 +1243,7 @@ class TemplateStream(object):
def dump(self, fp, encoding=None, errors="strict"):
"""Dump the complete stream into a file or file-like object.
- Per default unicode strings are written, if you want to encode
+ Per default strings are written, if you want to encode
before writing specify an `encoding`.
Example usage::
@@ -1299,7 +1251,7 @@ class TemplateStream(object):
Template('Hello {{ name }}!').stream(name='foo').dump('hello.html')
"""
close = False
- if isinstance(fp, string_types):
+ if isinstance(fp, str):
if encoding is None:
encoding = "utf-8"
fp = open(fp, "wb")
diff --git a/src/jinja2/exceptions.py b/src/jinja2/exceptions.py
index 0bf2003..07cfba2 100644
--- a/src/jinja2/exceptions.py
+++ b/src/jinja2/exceptions.py
@@ -1,44 +1,15 @@
-# -*- coding: utf-8 -*-
-from ._compat import imap
-from ._compat import implements_to_string
-from ._compat import PY2
-from ._compat import text_type
-
-
class TemplateError(Exception):
"""Baseclass for all template errors."""
- if PY2:
-
- def __init__(self, message=None):
- if message is not None:
- message = text_type(message).encode("utf-8")
- Exception.__init__(self, message)
-
- @property
- def message(self):
- if self.args:
- message = self.args[0]
- if message is not None:
- return message.decode("utf-8", "replace")
-
- def __unicode__(self):
- return self.message or u""
-
- else:
-
- def __init__(self, message=None):
- Exception.__init__(self, message)
+ def __init__(self, message=None):
+ super().__init__(message)
- @property
- def message(self):
- if self.args:
- message = self.args[0]
- if message is not None:
- return message
+ @property
+ def message(self):
+ if self.args:
+ return self.args[0]
-@implements_to_string
class TemplateNotFound(IOError, LookupError, TemplateError):
"""Raised if a template does not exist.
@@ -47,8 +18,8 @@ class TemplateNotFound(IOError, LookupError, TemplateError):
provided, an :exc:`UndefinedError` is raised.
"""
- # looks weird, but removes the warning descriptor that just
- # bogusly warns us about message being deprecated
+ # Silence the Python warning about message being deprecated since
+ # it's not valid here.
message = None
def __init__(self, name, message=None):
@@ -94,14 +65,13 @@ class TemplatesNotFound(TemplateNotFound):
else:
parts.append(name)
- message = u"none of the templates given were found: " + u", ".join(
- imap(text_type, parts)
+ message = "none of the templates given were found: " + ", ".join(
+ map(str, parts)
)
- TemplateNotFound.__init__(self, names and names[-1] or None, message)
+ TemplateNotFound.__init__(self, names[-1] if names else None, message)
self.templates = list(names)
-@implements_to_string
class TemplateSyntaxError(TemplateError):
"""Raised to tell the user that there is a problem with the template."""
@@ -122,10 +92,10 @@ class TemplateSyntaxError(TemplateError):
return self.message
# otherwise attach some stuff
- location = "line %d" % self.lineno
+ location = f"line {self.lineno}"
name = self.filename or self.name
if name:
- location = 'File "%s", %s' % (name, location)
+ location = f'File "{name}", {location}'
lines = [self.message, " " + location]
# if the source is set, add the line to the output
@@ -137,7 +107,7 @@ class TemplateSyntaxError(TemplateError):
if line:
lines.append(" " + line.strip())
- return u"\n".join(lines)
+ return "\n".join(lines)
def __reduce__(self):
# https://bugs.python.org/issue1692335 Exceptions that take
diff --git a/src/jinja2/ext.py b/src/jinja2/ext.py
index 9141be4..533ff17 100644
--- a/src/jinja2/ext.py
+++ b/src/jinja2/ext.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
"""Extension API for adding custom tags and behavior."""
import pprint
import re
@@ -7,9 +6,6 @@ from sys import version_info
from markupsafe import Markup
from . import nodes
-from ._compat import iteritems
-from ._compat import string_types
-from ._compat import with_metaclass
from .defaults import BLOCK_END_STRING
from .defaults import BLOCK_START_STRING
from .defaults import COMMENT_END_STRING
@@ -30,11 +26,9 @@ from .runtime import concat
from .utils import contextfunction
from .utils import import_string
-# the only real useful gettext functions for a Jinja template. Note
-# that ugettext must be assigned to gettext as Jinja doesn't support
-# non unicode strings.
+# I18N functions available in Jinja templates. If the I18N library
+# provides ugettext, it will be assigned to gettext.
GETTEXT_FUNCTIONS = ("_", "gettext", "ngettext")
-
_ws_re = re.compile(r"\s*\n\s*")
@@ -43,11 +37,11 @@ class ExtensionRegistry(type):
def __new__(mcs, name, bases, d):
rv = type.__new__(mcs, name, bases, d)
- rv.identifier = rv.__module__ + "." + rv.__name__
+ rv.identifier = f"{rv.__module__}.{rv.__name__}"
return rv
-class Extension(with_metaclass(ExtensionRegistry, object)):
+class Extension(metaclass=ExtensionRegistry):
"""Extensions can be used to add extra functionality to the Jinja template
system at the parser level. Custom extensions are bound to an environment
but may not store environment specific data on `self`. The reason for
@@ -196,6 +190,8 @@ class InternationalizationExtension(Extension):
)
def _install(self, translations, newstyle=None):
+ # ugettext and ungettext are preferred in case the I18N library
+ # is providing compatibility with older Python versions.
gettext = getattr(translations, "ugettext", None)
if gettext is None:
gettext = translations.gettext
@@ -206,7 +202,7 @@ class InternationalizationExtension(Extension):
def _install_null(self, newstyle=None):
self._install_callables(
- lambda x: x, lambda s, p, n: (n != 1 and (p,) or (s,))[0], newstyle
+ lambda x: x, lambda s, p, n: s if n == 1 else p, newstyle
)
def _install_callables(self, gettext, ngettext, newstyle=None):
@@ -222,7 +218,7 @@ class InternationalizationExtension(Extension):
self.environment.globals.pop(key, None)
def _extract(self, source, gettext_functions=GETTEXT_FUNCTIONS):
- if isinstance(source, string_types):
+ if isinstance(source, str):
source = self.environment.parse(source)
return extract_from_ast(source, gettext_functions)
@@ -249,7 +245,7 @@ class InternationalizationExtension(Extension):
name = parser.stream.expect("name")
if name.value in variables:
parser.fail(
- "translatable variable %r defined twice." % name.value,
+ f"translatable variable {name.value!r} defined twice.",
name.lineno,
exc=TemplateAssertionError,
)
@@ -297,7 +293,7 @@ class InternationalizationExtension(Extension):
name = parser.stream.expect("name")
if name.value not in variables:
parser.fail(
- "unknown variable %r for pluralization" % name.value,
+ f"unknown variable {name.value!r} for pluralization",
name.lineno,
exc=TemplateAssertionError,
)
@@ -356,7 +352,7 @@ class InternationalizationExtension(Extension):
next(parser.stream)
name = parser.stream.expect("name").value
referenced.append(name)
- buf.append("%%(%s)s" % name)
+ buf.append(f"%({name})s")
parser.stream.expect("variable_end")
elif parser.stream.current.type == "block_begin":
next(parser.stream)
@@ -409,7 +405,7 @@ class InternationalizationExtension(Extension):
# enough to handle the variable expansion and autoescape
# handling itself
if self.environment.newstyle_gettext:
- for key, value in iteritems(variables):
+ for key, value in variables.items():
# the function adds that later anyways in case num was
# called num, so just skip it.
if num_called_num and key == "num":
@@ -439,7 +435,7 @@ class ExprStmtExtension(Extension):
that it doesn't print the return value.
"""
- tags = set(["do"])
+ tags = {"do"}
def parse(self, parser):
node = nodes.ExprStmt(lineno=next(parser.stream).lineno)
@@ -450,7 +446,7 @@ class ExprStmtExtension(Extension):
class LoopControlExtension(Extension):
"""Adds break and continue to the template engine."""
- tags = set(["break", "continue"])
+ tags = {"break", "continue"}
def parse(self, parser):
token = next(parser.stream)
@@ -538,8 +534,8 @@ def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS, babel_style=True
* ``lineno`` is the number of the line on which the string was found,
* ``function`` is the name of the ``gettext`` function used (if the
string was extracted from embedded Python code), and
- * ``message`` is the string itself (a ``unicode`` object, or a tuple
- of ``unicode`` objects for functions with multiple string arguments).
+ * ``message`` is the string, or a tuple of strings for functions
+ with multiple string arguments.
This extraction function operates on the AST and is because of that unable
to extract any comments. For comment support you have to use the babel
@@ -554,7 +550,7 @@ def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS, babel_style=True
strings = []
for arg in node.args:
- if isinstance(arg, nodes.Const) and isinstance(arg.value, string_types):
+ if isinstance(arg, nodes.Const) and isinstance(arg.value, str):
strings.append(arg.value)
else:
strings.append(None)
@@ -578,7 +574,7 @@ def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS, babel_style=True
yield node.lineno, node.node.name, strings
-class _CommentFinder(object):
+class _CommentFinder:
"""Helper class to find comments in a token stream. Can only
find comments for gettext calls forwards. Once the comment
from line 4 is found, a comment for line 1 will not return a
diff --git a/src/jinja2/filters.py b/src/jinja2/filters.py
index 9741567..c257d4c 100644
--- a/src/jinja2/filters.py
+++ b/src/jinja2/filters.py
@@ -1,31 +1,25 @@
-# -*- coding: utf-8 -*-
"""Built-in template filters used with the ``|`` operator."""
import math
import random
import re
-import warnings
+from collections import abc
from collections import namedtuple
from itertools import chain
from itertools import groupby
from markupsafe import escape
from markupsafe import Markup
-from markupsafe import soft_unicode
+from markupsafe import soft_str
-from ._compat import abc
-from ._compat import imap
-from ._compat import iteritems
-from ._compat import string_types
-from ._compat import text_type
from .exceptions import FilterArgumentError
from .runtime import Undefined
from .utils import htmlsafe_json_dumps
from .utils import pformat
-from .utils import unicode_urlencode
+from .utils import url_quote
from .utils import urlize
-_word_re = re.compile(r"\w+", re.UNICODE)
-_word_beginning_split_re = re.compile(r"([-\s\(\{\[\<]+)", re.UNICODE)
+_word_re = re.compile(r"\w+")
+_word_beginning_split_re = re.compile(r"([-\s({\[<]+)")
def contextfilter(f):
@@ -58,7 +52,7 @@ def environmentfilter(f):
def ignore_case(value):
"""For use as a postprocessor for :func:`make_attrgetter`. Converts strings
to lowercase and returns other types as-is."""
- return value.lower() if isinstance(value, string_types) else value
+ return value.lower() if isinstance(value, str) else value
def make_attrgetter(environment, attribute, postprocess=None, default=None):
@@ -96,7 +90,7 @@ def make_multi_attrgetter(environment, attribute, postprocess=None):
Examples of attribute: "attr1,attr2", "attr1.inner1.0,attr2.inner2.0", etc.
"""
attribute_parts = (
- attribute.split(",") if isinstance(attribute, string_types) else [attribute]
+ attribute.split(",") if isinstance(attribute, str) else [attribute]
)
attribute = [
_prepare_attribute_parts(attribute_part) for attribute_part in attribute_parts
@@ -121,7 +115,7 @@ def make_multi_attrgetter(environment, attribute, postprocess=None):
def _prepare_attribute_parts(attr):
if attr is None:
return []
- elif isinstance(attr, string_types):
+ elif isinstance(attr, str):
return [int(x) if x.isdigit() else x for x in attr.split(".")]
else:
return [attr]
@@ -131,7 +125,7 @@ def do_forceescape(value):
"""Enforce HTML escaping. This will probably double escape variables."""
if hasattr(value, "__html__"):
value = value.__html__()
- return escape(text_type(value))
+ return escape(str(value))
def do_urlencode(value):
@@ -150,17 +144,16 @@ def do_urlencode(value):
.. versionadded:: 2.7
"""
- if isinstance(value, string_types) or not isinstance(value, abc.Iterable):
- return unicode_urlencode(value)
+ if isinstance(value, str) or not isinstance(value, abc.Iterable):
+ return url_quote(value)
if isinstance(value, dict):
- items = iteritems(value)
+ items = value.items()
else:
items = iter(value)
- return u"&".join(
- "%s=%s" % (unicode_urlencode(k, for_qs=True), unicode_urlencode(v, for_qs=True))
- for k, v in items
+ return "&".join(
+ f"{url_quote(k, for_qs=True)}={url_quote(v, for_qs=True)}" for k, v in items
)
@@ -183,7 +176,7 @@ def do_replace(eval_ctx, s, old, new, count=None):
if count is None:
count = -1
if not eval_ctx.autoescape:
- return text_type(s).replace(text_type(old), text_type(new), count)
+ return str(s).replace(str(old), str(new), count)
if (
hasattr(old, "__html__")
or hasattr(new, "__html__")
@@ -191,18 +184,18 @@ def do_replace(eval_ctx, s, old, new, count=None):
):
s = escape(s)
else:
- s = soft_unicode(s)
- return s.replace(soft_unicode(old), soft_unicode(new), count)
+ s = soft_str(s)
+ return s.replace(soft_str(old), soft_str(new), count)
def do_upper(s):
"""Convert a value to uppercase."""
- return soft_unicode(s).upper()
+ return soft_str(s).upper()
def do_lower(s):
"""Convert a value to lowercase."""
- return soft_unicode(s).lower()
+ return soft_str(s).lower()
@evalcontextfilter
@@ -229,13 +222,13 @@ def do_xmlattr(_eval_ctx, d, autospace=True):
As you can see it automatically prepends a space in front of the item
if the filter returned something unless the second parameter is false.
"""
- rv = u" ".join(
- u'%s="%s"' % (escape(key), escape(value))
- for key, value in iteritems(d)
+ rv = " ".join(
+ f'{escape(key)}="{escape(value)}"'
+ for key, value in d.items()
if value is not None and not isinstance(value, Undefined)
)
if autospace and rv:
- rv = u" " + rv
+ rv = " " + rv
if _eval_ctx.autoescape:
rv = Markup(rv)
return rv
@@ -245,7 +238,7 @@ def do_capitalize(s):
"""Capitalize a value. The first character will be uppercase, all others
lowercase.
"""
- return soft_unicode(s).capitalize()
+ return soft_str(s).capitalize()
def do_title(s):
@@ -255,7 +248,7 @@ def do_title(s):
return "".join(
[
item[0].upper() + item[1:].lower()
- for item in _word_beginning_split_re.split(soft_unicode(s))
+ for item in _word_beginning_split_re.split(soft_str(s))
if item
]
)
@@ -420,7 +413,7 @@ def do_max(environment, value, case_sensitive=False, attribute=None):
return _min_or_max(environment, value, max, case_sensitive, attribute)
-def do_default(value, default_value=u"", boolean=False):
+def do_default(value, default_value="", boolean=False):
"""If the value is undefined it will return the passed default value,
otherwise the value of the variable:
@@ -449,7 +442,7 @@ def do_default(value, default_value=u"", boolean=False):
@evalcontextfilter
-def do_join(eval_ctx, value, d=u"", attribute=None):
+def do_join(eval_ctx, value, d="", attribute=None):
"""Return a string which is the concatenation of the strings in the
sequence. The separator between elements is an empty string per
default, you can define it with the optional parameter:
@@ -472,11 +465,11 @@ def do_join(eval_ctx, value, d=u"", attribute=None):
The `attribute` parameter was added.
"""
if attribute is not None:
- value = imap(make_attrgetter(eval_ctx.environment, attribute), value)
+ value = map(make_attrgetter(eval_ctx.environment, attribute), value)
# no automatic escaping? joining is a lot easier then
if not eval_ctx.autoescape:
- return text_type(d).join(imap(text_type, value))
+ return str(d).join(map(str, value))
# if the delimiter doesn't have an html representation we check
# if any of the items has. If yes we do a coercion to Markup
@@ -487,20 +480,20 @@ def do_join(eval_ctx, value, d=u"", attribute=None):
if hasattr(item, "__html__"):
do_escape = True
else:
- value[idx] = text_type(item)
+ value[idx] = str(item)
if do_escape:
d = escape(d)
else:
- d = text_type(d)
+ d = str(d)
return d.join(value)
# no html involved, to normal joining
- return soft_unicode(d).join(imap(soft_unicode, value))
+ return soft_str(d).join(map(soft_str, value))
def do_center(value, width=80):
"""Centers the value in a field of a given width."""
- return text_type(value).center(width)
+ return str(value).center(width)
@environmentfilter
@@ -546,36 +539,32 @@ def do_filesizeformat(value, binary=False):
prefixes are used (Mebi, Gibi).
"""
bytes = float(value)
- base = binary and 1024 or 1000
+ base = 1024 if binary else 1000
prefixes = [
- (binary and "KiB" or "kB"),
- (binary and "MiB" or "MB"),
- (binary and "GiB" or "GB"),
- (binary and "TiB" or "TB"),
- (binary and "PiB" or "PB"),
- (binary and "EiB" or "EB"),
- (binary and "ZiB" or "ZB"),
- (binary and "YiB" or "YB"),
+ ("KiB" if binary else "kB"),
+ ("MiB" if binary else "MB"),
+ ("GiB" if binary else "GB"),
+ ("TiB" if binary else "TB"),
+ ("PiB" if binary else "PB"),
+ ("EiB" if binary else "EB"),
+ ("ZiB" if binary else "ZB"),
+ ("YiB" if binary else "YB"),
]
if bytes == 1:
return "1 Byte"
elif bytes < base:
- return "%d Bytes" % bytes
+ return f"{int(bytes)} Bytes"
else:
for i, prefix in enumerate(prefixes):
unit = base ** (i + 2)
if bytes < unit:
- return "%.1f %s" % ((base * bytes / unit), prefix)
- return "%.1f %s" % ((base * bytes / unit), prefix)
+ return f"{base * bytes / unit:.1f} {prefix}"
+ return f"{base * bytes / unit:.1f} {prefix}"
-def do_pprint(value, verbose=False):
- """Pretty print a variable. Useful for debugging.
-
- With Jinja 1.2 onwards you can pass it a parameter. If this parameter
- is truthy the output will be more verbose (this requires `pretty`)
- """
- return pformat(value, verbose=verbose)
+def do_pprint(value):
+ """Pretty print a variable. Useful for debugging."""
+ return pformat(value)
@evalcontextfilter
@@ -600,8 +589,8 @@ def do_urlize(
{{ mytext|urlize(40, target='_blank') }}
- .. versionchanged:: 2.8+
- The *target* parameter was added.
+ .. versionchanged:: 2.8
+ The ``target`` parameter was added.
"""
policies = eval_ctx.environment.policies
rel = set((rel or "").split() or [])
@@ -617,7 +606,7 @@ def do_urlize(
return rv
-def do_indent(s, width=4, first=False, blank=False, indentfirst=None):
+def do_indent(s, width=4, first=False, blank=False):
"""Return a copy of the string with each line indented by 4 spaces. The
first line and blank lines are not indented by default.
@@ -630,17 +619,8 @@ def do_indent(s, width=4, first=False, blank=False, indentfirst=None):
Rename the ``indentfirst`` argument to ``first``.
"""
- if indentfirst is not None:
- warnings.warn(
- "The 'indentfirst' argument is renamed to 'first' and will"
- " be removed in version 3.0.",
- DeprecationWarning,
- stacklevel=2,
- )
- first = indentfirst
-
- indention = u" " * width
- newline = u"\n"
+ indention = " " * width
+ newline = "\n"
if isinstance(s, Markup):
indention = Markup(indention)
@@ -692,8 +672,8 @@ def do_truncate(env, s, length=255, killwords=False, end="...", leeway=None):
"""
if leeway is None:
leeway = env.policies["truncate.leeway"]
- assert length >= len(end), "expected length >= %s, got %s" % (len(end), length)
- assert leeway >= 0, "expected leeway >= 0, got %s" % leeway
+ assert length >= len(end), f"expected length >= {len(end)}, got {length}"
+ assert leeway >= 0, f"expected leeway >= 0, got {leeway}"
if len(s) <= length + leeway:
return s
if killwords:
@@ -761,7 +741,7 @@ def do_wordwrap(
def do_wordcount(s):
"""Count the words in that string."""
- return len(_word_re.findall(soft_unicode(s)))
+ return len(_word_re.findall(soft_str(s)))
def do_int(value, default=0, base=10):
@@ -774,7 +754,7 @@ def do_int(value, default=0, base=10):
The base is ignored for decimal numbers and non-string values.
"""
try:
- if isinstance(value, string_types):
+ if isinstance(value, str):
return int(value, base)
return int(value)
except (TypeError, ValueError):
@@ -820,19 +800,19 @@ def do_format(value, *args, **kwargs):
raise FilterArgumentError(
"can't handle positional and keyword arguments at the same time"
)
- return soft_unicode(value) % (kwargs or args)
+ return soft_str(value) % (kwargs or args)
def do_trim(value, chars=None):
"""Strip leading and trailing characters, by default whitespace."""
- return soft_unicode(value).strip(chars)
+ return soft_str(value).strip(chars)
def do_striptags(value):
"""Strip SGML/XML tags and replace adjacent whitespace by one space."""
if hasattr(value, "__html__"):
value = value.__html__()
- return Markup(text_type(value)).striptags()
+ return Markup(str(value)).striptags()
def do_slice(value, slices, fill_with=None):
@@ -1005,7 +985,7 @@ def do_sum(environment, iterable, attribute=None, start=0):
attributes. Also the `start` parameter was moved on to the right.
"""
if attribute is not None:
- iterable = imap(make_attrgetter(environment, attribute), iterable)
+ iterable = map(make_attrgetter(environment, attribute), iterable)
return sum(iterable, start)
@@ -1025,14 +1005,14 @@ def do_mark_safe(value):
def do_mark_unsafe(value):
"""Mark a value as unsafe. This is the reverse operation for :func:`safe`."""
- return text_type(value)
+ return str(value)
def do_reverse(value):
"""Reverse the object or return an iterator that iterates over it the other
way round.
"""
- if isinstance(value, string_types):
+ if isinstance(value, str):
return value[::-1]
try:
return reversed(value)
@@ -1263,14 +1243,13 @@ def do_tojson(eval_ctx, value, indent=None):
def prepare_map(args, kwargs):
context = args[0]
seq = args[1]
- default = None
if len(args) == 2 and "attribute" in kwargs:
attribute = kwargs.pop("attribute")
default = kwargs.pop("default", None)
if kwargs:
raise FilterArgumentError(
- "Unexpected keyword argument %r" % next(iter(kwargs))
+ f"Unexpected keyword argument {next(iter(kwargs))!r}"
)
func = make_attrgetter(context.environment, attribute, default=default)
else:
@@ -1365,7 +1344,7 @@ FILTERS = {
"selectattr": do_selectattr,
"slice": do_slice,
"sort": do_sort,
- "string": soft_unicode,
+ "string": soft_str,
"striptags": do_striptags,
"sum": do_sum,
"title": do_title,
diff --git a/src/jinja2/idtracking.py b/src/jinja2/idtracking.py
index 9a0d838..78cad91 100644
--- a/src/jinja2/idtracking.py
+++ b/src/jinja2/idtracking.py
@@ -1,4 +1,3 @@
-from ._compat import iteritems
from .visitor import NodeVisitor
VAR_LOAD_PARAMETER = "param"
@@ -21,7 +20,7 @@ def symbols_for_node(node, parent_symbols=None):
return sym
-class Symbols(object):
+class Symbols:
def __init__(self, parent=None, level=None):
if level is None:
if parent is None:
@@ -39,7 +38,7 @@ class Symbols(object):
visitor.visit(node, **kwargs)
def _define_ref(self, name, load=None):
- ident = "l_%d_%s" % (self.level, name)
+ ident = f"l_{self.level}_{name}"
self.refs[name] = ident
if load is not None:
self.loads[ident] = load
@@ -61,8 +60,8 @@ class Symbols(object):
rv = self.find_ref(name)
if rv is None:
raise AssertionError(
- "Tried to resolve a name to a reference that "
- "was unknown to the frame (%r)" % name
+ "Tried to resolve a name to a reference that was"
+ f" unknown to the frame ({name!r})"
)
return rv
@@ -114,7 +113,7 @@ class Symbols(object):
self.loads.update(sym.loads)
self.stores.update(sym.stores)
- for name, branch_count in iteritems(stores):
+ for name, branch_count in stores.items():
if branch_count == len(branch_symbols):
continue
target = self.find_ref(name)
@@ -141,7 +140,7 @@ class Symbols(object):
rv = set()
node = self
while node is not None:
- for target, (instr, _) in iteritems(self.loads):
+ for target, (instr, _) in self.loads.items():
if instr == VAR_LOAD_PARAMETER:
rv.add(target)
node = node.parent
@@ -200,7 +199,7 @@ class RootVisitor(NodeVisitor):
def generic_visit(self, node, *args, **kwargs):
raise NotImplementedError(
- "Cannot find symbols for %r" % node.__class__.__name__
+ f"Cannot find symbols for {node.__class__.__name__!r}"
)
diff --git a/src/jinja2/lexer.py b/src/jinja2/lexer.py
index 5fa940d..fc44dbe 100644
--- a/src/jinja2/lexer.py
+++ b/src/jinja2/lexer.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
"""Implements a Jinja / Python combination lexer. The ``Lexer`` class
is used to do some preprocessing. It filters out invalid operators like
the bitshift operators we don't allow in templates. It separates
@@ -8,11 +7,9 @@ import re
from ast import literal_eval
from collections import deque
from operator import itemgetter
+from sys import intern
-from ._compat import implements_iterator
-from ._compat import intern
-from ._compat import iteritems
-from ._compat import text_type
+from ._identifier import pattern as name_re
from .exceptions import TemplateSyntaxError
from .utils import LRUCache
@@ -21,7 +18,7 @@ from .utils import LRUCache
_lexer_cache = LRUCache(50)
# static regular expressions
-whitespace_re = re.compile(r"\s+", re.U)
+whitespace_re = re.compile(r"\s+")
newline_re = re.compile(r"(\r\n|\r|\n)")
string_re = re.compile(
r"('([^'\\]*(?:\\.[^'\\]*)*)'" r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S
@@ -41,20 +38,6 @@ float_re = re.compile(
re.IGNORECASE | re.VERBOSE,
)
-try:
- # check if this Python supports Unicode identifiers
- compile("föö", "<unknown>", "eval")
-except SyntaxError:
- # Python 2, no Unicode support, use ASCII identifiers
- name_re = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*")
- check_ident = False
-else:
- # Unicode support, import generated re pattern and set flag to use
- # str.isidentifier to validate during lexing.
- from ._identifier import pattern as name_re
-
- check_ident = True
-
# internal the tokens and keep references to them
TOKEN_ADD = intern("add")
TOKEN_ASSIGN = intern("assign")
@@ -136,10 +119,10 @@ operators = {
";": TOKEN_SEMICOLON,
}
-reverse_operators = dict([(v, k) for k, v in iteritems(operators)])
+reverse_operators = {v: k for k, v in operators.items()}
assert len(operators) == len(reverse_operators), "operators dropped"
operator_re = re.compile(
- "(%s)" % "|".join(re.escape(x) for x in sorted(operators, key=lambda x: -len(x)))
+ f"({'|'.join(re.escape(x) for x in sorted(operators, key=lambda x: -len(x)))})"
)
ignored_tokens = frozenset(
@@ -243,7 +226,7 @@ def compile_rules(environment):
return [x[1:] for x in sorted(rules, reverse=True)]
-class Failure(object):
+class Failure:
"""Class that raises a `TemplateSyntaxError` if called.
Used by the `Lexer` to specify known errors.
"""
@@ -293,11 +276,10 @@ class Token(tuple):
return False
def __repr__(self):
- return "Token(%r, %r, %r)" % (self.lineno, self.type, self.value)
+ return f"Token({self.lineno!r}, {self.type!r}, {self.value!r})"
-@implements_iterator
-class TokenStreamIterator(object):
+class TokenStreamIterator:
"""The iterator for tokenstreams. Iterate over the stream
until the eof token is reached.
"""
@@ -317,8 +299,7 @@ class TokenStreamIterator(object):
return token
-@implements_iterator
-class TokenStream(object):
+class TokenStream:
"""A token stream is an iterable that yields :class:`Token`\\s. The
parser however does not iterate over it but calls :meth:`next` to go
one token ahead. The current active token is stored as :attr:`current`.
@@ -403,13 +384,13 @@ class TokenStream(object):
expr = describe_token_expr(expr)
if self.current.type is TOKEN_EOF:
raise TemplateSyntaxError(
- "unexpected end of template, expected %r." % expr,
+ f"unexpected end of template, expected {expr!r}.",
self.current.lineno,
self.name,
self.filename,
)
raise TemplateSyntaxError(
- "expected token %r, got %r" % (expr, describe_token(self.current)),
+ f"expected token {expr!r}, got {describe_token(self.current)!r}",
self.current.lineno,
self.name,
self.filename,
@@ -453,10 +434,10 @@ class OptionalLStrip(tuple):
# Even though it looks like a no-op, creating instances fails
# without this.
def __new__(cls, *members, **kwargs):
- return super(OptionalLStrip, cls).__new__(cls, members)
+ return super().__new__(cls, members)
-class Lexer(object):
+class Lexer:
"""Class that implements a lexer for a given environment. Automatically
created by the environment class, usually you don't have to do that.
@@ -489,8 +470,13 @@ class Lexer(object):
# is required.
root_tag_rules = compile_rules(environment)
+ block_start_re = e(environment.block_start_string)
+ block_end_re = e(environment.block_end_string)
+ comment_end_re = e(environment.comment_end_string)
+ variable_end_re = e(environment.variable_end_string)
+
# block suffix if trimming is enabled
- block_suffix_re = environment.trim_blocks and "\\n?" or ""
+ block_suffix_re = "\\n?" if environment.trim_blocks else ""
# If lstrip is enabled, it should not be applied if there is any
# non-whitespace between the newline and block.
@@ -499,28 +485,20 @@ class Lexer(object):
self.newline_sequence = environment.newline_sequence
self.keep_trailing_newline = environment.keep_trailing_newline
+ root_raw_re = (
+ fr"(?P<raw_begin>{block_start_re}(\-|\+|)\s*raw\s*"
+ fr"(?:\-{block_end_re}\s*|{block_end_re}))"
+ )
+ root_parts_re = "|".join(
+ [root_raw_re] + [fr"(?P<{n}>{r}(\-|\+|))" for n, r in root_tag_rules]
+ )
+
# global lexing rules
self.rules = {
"root": [
# directives
(
- c(
- "(.*?)(?:%s)"
- % "|".join(
- [
- r"(?P<raw_begin>%s(\-|\+|)\s*raw\s*(?:\-%s\s*|%s))"
- % (
- e(environment.block_start_string),
- e(environment.block_end_string),
- e(environment.block_end_string),
- )
- ]
- + [
- r"(?P<%s>%s(\-|\+|))" % (n, r)
- for n, r in root_tag_rules
- ]
- )
- ),
+ c(fr"(.*?)(?:{root_parts_re})"),
OptionalLStrip(TOKEN_DATA, "#bygroup"),
"#bygroup",
),
@@ -531,29 +509,18 @@ class Lexer(object):
TOKEN_COMMENT_BEGIN: [
(
c(
- r"(.*?)((?:\-%s\s*|%s)%s)"
- % (
- e(environment.comment_end_string),
- e(environment.comment_end_string),
- block_suffix_re,
- )
+ fr"(.*?)((?:\-{comment_end_re}\s*"
+ fr"|{comment_end_re}){block_suffix_re})"
),
(TOKEN_COMMENT, TOKEN_COMMENT_END),
"#pop",
),
- (c("(.)"), (Failure("Missing end of comment tag"),), None),
+ (c(r"(.)"), (Failure("Missing end of comment tag"),), None),
],
# blocks
TOKEN_BLOCK_BEGIN: [
(
- c(
- r"(?:\-%s\s*|%s)%s"
- % (
- e(environment.block_end_string),
- e(environment.block_end_string),
- block_suffix_re,
- )
- ),
+ c(fr"(?:\-{block_end_re}\s*|{block_end_re}){block_suffix_re}"),
TOKEN_BLOCK_END,
"#pop",
),
@@ -562,13 +529,7 @@ class Lexer(object):
# variables
TOKEN_VARIABLE_BEGIN: [
(
- c(
- r"\-%s\s*|%s"
- % (
- e(environment.variable_end_string),
- e(environment.variable_end_string),
- )
- ),
+ c(fr"\-{variable_end_re}\s*|{variable_end_re}"),
TOKEN_VARIABLE_END,
"#pop",
)
@@ -578,18 +539,13 @@ class Lexer(object):
TOKEN_RAW_BEGIN: [
(
c(
- r"(.*?)((?:%s(\-|\+|))\s*endraw\s*(?:\-%s\s*|%s%s))"
- % (
- e(environment.block_start_string),
- e(environment.block_end_string),
- e(environment.block_end_string),
- block_suffix_re,
- )
+ fr"(.*?)((?:{block_start_re}(\-|\+|))\s*endraw\s*"
+ fr"(?:\-{block_end_re}\s*|{block_end_re}{block_suffix_re}))"
),
OptionalLStrip(TOKEN_DATA, TOKEN_RAW_END),
"#pop",
),
- (c("(.)"), (Failure("Missing end of raw directive"),), None),
+ (c(r"(.)"), (Failure("Missing end of raw directive"),), None),
],
# line statements
TOKEN_LINESTATEMENT_BEGIN: [
@@ -607,7 +563,9 @@ class Lexer(object):
}
def _normalize_newlines(self, value):
- """Called for strings and template data to normalize it to unicode."""
+ """Replace all newlines with the configured sequence in strings
+ and template data.
+ """
return newline_re.sub(self.newline_sequence, value)
def tokenize(self, source, name=None, filename=None, state=None):
@@ -635,7 +593,7 @@ class Lexer(object):
token = value
elif token == TOKEN_NAME:
value = str(value)
- if check_ident and not value.isidentifier():
+ if not value.isidentifier():
raise TemplateSyntaxError(
"Invalid character in identifier", lineno, name, filename
)
@@ -663,13 +621,10 @@ class Lexer(object):
"""This method tokenizes the text and returns the tokens in a
generator. Use this method if you just want to tokenize a template.
"""
- source = text_type(source)
lines = source.splitlines()
if self.keep_trailing_newline and source:
- for newline in ("\r\n", "\r", "\n"):
- if source.endswith(newline):
- lines.append("")
- break
+ if source.endswith(("\r\n", "\r", "\n")):
+ lines.append("")
source = "\n".join(lines)
pos = 0
lineno = 1
@@ -745,16 +700,15 @@ class Lexer(object):
# yield for the current token the first named
# group that matched
elif token == "#bygroup":
- for key, value in iteritems(m.groupdict()):
+ for key, value in m.groupdict().items():
if value is not None:
yield lineno, key, value
lineno += value.count("\n")
break
else:
raise RuntimeError(
- "%r wanted to resolve "
- "the token dynamically"
- " but no group matched" % regex
+ f"{regex!r} wanted to resolve the token dynamically"
+ " but no group matched"
)
# normal group
else:
@@ -778,13 +732,12 @@ class Lexer(object):
elif data in ("}", ")", "]"):
if not balancing_stack:
raise TemplateSyntaxError(
- "unexpected '%s'" % data, lineno, name, filename
+ f"unexpected '{data}'", lineno, name, filename
)
expected_op = balancing_stack.pop()
if expected_op != data:
raise TemplateSyntaxError(
- "unexpected '%s', "
- "expected '%s'" % (data, expected_op),
+ f"unexpected '{data}', expected '{expected_op}'",
lineno,
name,
filename,
@@ -806,15 +759,14 @@ class Lexer(object):
stack.pop()
# resolve the new state by group checking
elif new_state == "#bygroup":
- for key, value in iteritems(m.groupdict()):
+ for key, value in m.groupdict().items():
if value is not None:
stack.append(key)
break
else:
raise RuntimeError(
- "%r wanted to resolve the "
- "new state dynamically but"
- " no group matched" % regex
+ f"{regex!r} wanted to resolve the new state dynamically"
+ f" but no group matched"
)
# direct state name given
else:
@@ -825,7 +777,7 @@ class Lexer(object):
# raise error
elif pos2 == pos:
raise RuntimeError(
- "%r yielded empty string without stack change" % regex
+ f"{regex!r} yielded empty string without stack change"
)
# publish new function and start again
pos = pos2
@@ -838,8 +790,5 @@ class Lexer(object):
return
# something went wrong
raise TemplateSyntaxError(
- "unexpected char %r at %d" % (source[pos], pos),
- lineno,
- name,
- filename,
+ f"unexpected char {source[pos]!r} at {pos}", lineno, name, filename
)
diff --git a/src/jinja2/loaders.py b/src/jinja2/loaders.py
index 457c4b5..6e546c0 100644
--- a/src/jinja2/loaders.py
+++ b/src/jinja2/loaders.py
@@ -1,18 +1,16 @@
-# -*- coding: utf-8 -*-
"""API and implementations for loading templates from different data
sources.
"""
+import importlib.util
import os
import sys
import weakref
+import zipimport
+from collections import abc
from hashlib import sha1
-from os import path
+from importlib import import_module
from types import ModuleType
-from ._compat import abc
-from ._compat import fspath
-from ._compat import iteritems
-from ._compat import string_types
from .exceptions import TemplateNotFound
from .utils import internalcode
from .utils import open_if_exists
@@ -25,9 +23,9 @@ def split_template_path(template):
pieces = []
for piece in template.split("/"):
if (
- path.sep in piece
- or (path.altsep and path.altsep in piece)
- or piece == path.pardir
+ os.path.sep in piece
+ or (os.path.altsep and os.path.altsep in piece)
+ or piece == os.path.pardir
):
raise TemplateNotFound(template)
elif piece and piece != ".":
@@ -35,7 +33,7 @@ def split_template_path(template):
return pieces
-class BaseLoader(object):
+class BaseLoader:
"""Baseclass for all loaders. Subclass this and override `get_source` to
implement a custom loading mechanism. The environment provides a
`get_template` method that calls the loader's `load` method to get the
@@ -75,9 +73,9 @@ class BaseLoader(object):
`TemplateNotFound` error if it can't locate the template.
The source part of the returned tuple must be the source of the
- template as unicode string or a ASCII bytestring. The filename should
- be the name of the file on the filesystem if it was loaded from there,
- otherwise `None`. The filename is used by python for the tracebacks
+ template as a string. The filename should be the name of the
+ file on the filesystem if it was loaded from there, otherwise
+ ``None``. The filename is used by Python for the tracebacks
if no loader extension is used.
The last item in the tuple is the `uptodate` function. If auto
@@ -88,7 +86,7 @@ class BaseLoader(object):
"""
if not self.has_source_access:
raise RuntimeError(
- "%s cannot provide access to the source" % self.__class__.__name__
+ f"{self.__class__.__name__} cannot provide access to the source"
)
raise TemplateNotFound(template)
@@ -161,22 +159,17 @@ class FileSystemLoader(BaseLoader):
"""
def __init__(self, searchpath, encoding="utf-8", followlinks=False):
- if not isinstance(searchpath, abc.Iterable) or isinstance(
- searchpath, string_types
- ):
+ if not isinstance(searchpath, abc.Iterable) or isinstance(searchpath, str):
searchpath = [searchpath]
- # In Python 3.5, os.path.join doesn't support Path. This can be
- # simplified to list(searchpath) when Python 3.5 is dropped.
- self.searchpath = [fspath(p) for p in searchpath]
-
+ self.searchpath = list(searchpath)
self.encoding = encoding
self.followlinks = followlinks
def get_source(self, environment, template):
pieces = split_template_path(template)
for searchpath in self.searchpath:
- filename = path.join(searchpath, *pieces)
+ filename = os.path.join(searchpath, *pieces)
f = open_if_exists(filename)
if f is None:
continue
@@ -185,11 +178,11 @@ class FileSystemLoader(BaseLoader):
finally:
f.close()
- mtime = path.getmtime(filename)
+ mtime = os.path.getmtime(filename)
def uptodate():
try:
- return path.getmtime(filename) == mtime
+ return os.path.getmtime(filename) == mtime
except OSError:
return False
@@ -215,82 +208,146 @@ class FileSystemLoader(BaseLoader):
class PackageLoader(BaseLoader):
- """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::
+ """Load templates from a directory in a Python package.
- loader = PackageLoader('mypackage', 'views')
+ :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.
- If the package path is not given, ``'templates'`` is assumed.
+ The following example looks up templates in the ``pages`` directory
+ within the ``project.ui`` package.
- 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.
+ .. 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:: 3.0
+ No longer uses ``setuptools`` as a dependency.
+
+ .. versionchanged:: 3.0
+ Limited PEP 420 namespace package support.
"""
def __init__(self, package_name, package_path="templates", encoding="utf-8"):
- from pkg_resources import DefaultProvider
- from pkg_resources import get_provider
- from pkg_resources import ResourceManager
+ if package_path == os.path.curdir:
+ package_path = ""
+ elif package_path[:2] == os.path.curdir + os.path.sep:
+ package_path = package_path[2:]
- provider = get_provider(package_name)
- self.encoding = encoding
- self.manager = ResourceManager()
- self.filesystem_bound = isinstance(provider, DefaultProvider)
- self.provider = provider
+ package_path = os.path.normpath(package_path).rstrip(os.path.sep)
self.package_path = package_path
+ self.package_name = 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)
+ spec = importlib.util.find_spec(package_name)
+ self._loader = loader = spec.loader
+ self._archive = None
+ self._template_root = None
+
+ if isinstance(loader, zipimport.zipimporter):
+ self._archive = loader.archive
+ pkgdir = next(iter(spec.submodule_search_locations))
+ self._template_root = os.path.join(pkgdir, package_path)
+ elif spec.submodule_search_locations:
+ # This will be one element for regular packages and multiple
+ # for namespace packages.
+ for root in spec.submodule_search_locations:
+ 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(
+ f"The {package_name!r} package was not installed in a"
+ " way that PackageLoader understands."
+ )
def get_source(self, environment, template):
- pieces = split_template_path(template)
- p = "/".join((self.package_path,) + tuple(pieces))
+ p = os.path.join(self._template_root, *split_template_path(template))
- if not self.provider.has_resource(p):
- raise TemplateNotFound(template)
+ if self._archive is None:
+ # Package is a directory.
+ if not os.path.isfile(p):
+ raise TemplateNotFound(template)
- filename = uptodate = None
+ with open(p, "rb") as f:
+ source = f.read()
- if self.filesystem_bound:
- filename = self.provider.get_resource_filename(self.manager, p)
- mtime = path.getmtime(filename)
+ mtime = os.path.getmtime(p)
- def uptodate():
- try:
- return path.getmtime(filename) == mtime
- except OSError:
- return False
+ def up_to_date():
+ return os.path.isfile(p) and os.path.getmtime(p) == mtime
- source = self.provider.get_resource_string(self.manager, p)
- return source.decode(self.encoding), filename, uptodate
+ else:
+ # Package is a zip file.
+ try:
+ source = self._loader.get_data(p)
+ except OSError:
+ raise TemplateNotFound(template)
- def list_templates(self):
- path = self.package_path
+ # 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
- if path[:2] == "./":
- path = path[2:]
- elif path == ".":
- path = ""
+ return source.decode(self.encoding), p, up_to_date
- offset = len(path)
+ def list_templates(self):
results = []
- def _walk(path):
- for filename in self.provider.resource_listdir(path):
- fullname = path + "/" + filename
+ 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)
- if self.provider.resource_isdir(fullname):
- _walk(fullname)
- else:
- results.append(fullname[offset:].lstrip("/"))
+ 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, "/"))
- _walk(path)
results.sort()
return results
class DictLoader(BaseLoader):
- """Loads a template from a python dict. It's passed a dict of unicode
- strings bound to template names. This loader is useful for unittesting:
+ """Loads a template from a Python dict mapping template names to
+ template source. This loader is useful for unittesting:
>>> loader = DictLoader({'index.html': 'source here'})
@@ -313,7 +370,7 @@ class DictLoader(BaseLoader):
class FunctionLoader(BaseLoader):
"""A loader that is passed a function which does the loading. The
function receives the name of the template and has to return either
- an unicode string with the template source, a tuple in the form ``(source,
+ a string with the template source, a tuple in the form ``(source,
filename, uptodatefunc)`` or `None` if the template does not exist.
>>> def load_template(name):
@@ -335,7 +392,7 @@ class FunctionLoader(BaseLoader):
rv = self.load_func(template)
if rv is None:
raise TemplateNotFound(template)
- elif isinstance(rv, string_types):
+ elif isinstance(rv, str):
return rv, None, None
return rv
@@ -388,7 +445,7 @@ class PrefixLoader(BaseLoader):
def list_templates(self):
result = []
- for prefix, loader in iteritems(self.mapping):
+ for prefix, loader in self.mapping.items():
for template in loader.list_templates():
result.append(prefix + self.delimiter + template)
return result
@@ -455,16 +512,16 @@ class ModuleLoader(BaseLoader):
has_source_access = False
def __init__(self, path):
- package_name = "_jinja2_module_templates_%x" % id(self)
+ package_name = f"_jinja2_module_templates_{id(self):x}"
# create a fake module that looks for the templates in the
# path given.
mod = _TemplateModule(package_name)
- if not isinstance(path, abc.Iterable) or isinstance(path, string_types):
+ if not isinstance(path, abc.Iterable) or isinstance(path, str):
path = [path]
- mod.__path__ = [fspath(p) for p in path]
+ mod.__path__ = [os.fspath(p) for p in path]
sys.modules[package_name] = weakref.proxy(
mod, lambda x: sys.modules.pop(package_name, None)
@@ -487,7 +544,7 @@ class ModuleLoader(BaseLoader):
@internalcode
def load(self, environment, name, globals=None):
key = self.get_template_key(name)
- module = "%s.%s" % (self.package_name, key)
+ module = f"{self.package_name}.{key}"
mod = getattr(self.module, module, None)
if mod is None:
try:
diff --git a/src/jinja2/meta.py b/src/jinja2/meta.py
index 3795aac..899e179 100644
--- a/src/jinja2/meta.py
+++ b/src/jinja2/meta.py
@@ -1,10 +1,7 @@
-# -*- coding: utf-8 -*-
"""Functions that expose information about templates that might be
interesting for introspection.
"""
from . import nodes
-from ._compat import iteritems
-from ._compat import string_types
from .compiler import CodeGenerator
@@ -21,7 +18,7 @@ class TrackingCodeGenerator(CodeGenerator):
def enter_frame(self, frame):
"""Remember all undeclared identifiers."""
CodeGenerator.enter_frame(self, frame)
- for _, (action, param) in iteritems(frame.symbols.loads):
+ for _, (action, param) in frame.symbols.loads.items():
if action == "resolve" and param not in self.environment.globals:
self.undeclared_identifiers.add(param)
@@ -35,7 +32,7 @@ def find_undeclared_variables(ast):
>>> from jinja2 import Environment, meta
>>> env = Environment()
>>> ast = env.parse('{% set foo = 42 %}{{ bar + foo }}')
- >>> meta.find_undeclared_variables(ast) == set(['bar'])
+ >>> meta.find_undeclared_variables(ast) == {'bar'}
True
.. admonition:: Implementation
@@ -75,7 +72,7 @@ def find_referenced_templates(ast):
# something const, only yield the strings and ignore
# non-string consts that really just make no sense
if isinstance(template_name, nodes.Const):
- if isinstance(template_name.value, string_types):
+ if isinstance(template_name.value, str):
yield template_name.value
# something dynamic in there
else:
@@ -85,7 +82,7 @@ def find_referenced_templates(ast):
yield None
continue
# constant is a basestring, direct template name
- if isinstance(node.template.value, string_types):
+ if isinstance(node.template.value, str):
yield node.template.value
# a tuple or list (latter *should* not happen) made of consts,
# yield the consts that are strings. We could warn here for
@@ -94,7 +91,7 @@ def find_referenced_templates(ast):
node.template.value, (tuple, list)
):
for template_name in node.template.value:
- if isinstance(template_name, string_types):
+ if isinstance(template_name, str):
yield template_name
# something else we don't care about, we could warn here
else:
diff --git a/src/jinja2/nativetypes.py b/src/jinja2/nativetypes.py
index 9866c96..e0ad94d 100644
--- a/src/jinja2/nativetypes.py
+++ b/src/jinja2/nativetypes.py
@@ -4,7 +4,6 @@ from itertools import chain
from itertools import islice
from . import nodes
-from ._compat import text_type
from .compiler import CodeGenerator
from .compiler import has_safe_repr
from .environment import Environment
@@ -33,7 +32,7 @@ def native_concat(nodes, preserve_quotes=True):
else:
if isinstance(nodes, types.GeneratorType):
nodes = chain(head, nodes)
- raw = u"".join([text_type(v) for v in nodes])
+ raw = "".join([str(v) for v in nodes])
try:
literal = literal_eval(raw)
@@ -45,14 +44,15 @@ def native_concat(nodes, preserve_quotes=True):
# Without this, "'{{ a }}', '{{ b }}'" results in "a, b", but should
# be ('a', 'b').
if preserve_quotes and isinstance(literal, str):
- return "{quote}{}{quote}".format(literal, quote=raw[0])
+ quote = raw[0]
+ return f"{quote}{literal}{quote}"
return literal
class NativeCodeGenerator(CodeGenerator):
"""A code generator which renders Python types by not adding
- ``to_string()`` around output nodes, and using :func:`native_concat`
+ ``str()`` around output nodes, and using :func:`native_concat`
to convert complex strings back to Python types if possible.
"""
diff --git a/src/jinja2/nodes.py b/src/jinja2/nodes.py
index 95bd614..d5133f7 100644
--- a/src/jinja2/nodes.py
+++ b/src/jinja2/nodes.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
"""AST nodes generated by the parser for the compiler. Also provides
some node tree helper functions used by the parser and compiler in order
to normalize nodes.
@@ -8,11 +7,6 @@ from collections import deque
from markupsafe import Markup
-from ._compat import izip
-from ._compat import PY2
-from ._compat import text_type
-from ._compat import with_metaclass
-
_binop_to_func = {
"*": operator.mul,
"/": operator.truediv,
@@ -23,7 +17,11 @@ _binop_to_func = {
"-": operator.sub,
}
-_uaop_to_func = {"not": operator.not_, "+": operator.pos, "-": operator.neg}
+_uaop_to_func = {
+ "not": operator.not_,
+ "+": operator.pos,
+ "-": operator.neg,
+}
_cmpop_to_func = {
"eq": operator.eq,
@@ -49,16 +47,16 @@ class NodeType(type):
def __new__(mcs, name, bases, d):
for attr in "fields", "attributes":
storage = []
- storage.extend(getattr(bases[0], attr, ()))
+ storage.extend(getattr(bases[0] if bases else object, attr, ()))
storage.extend(d.get(attr, ()))
- assert len(bases) == 1, "multiple inheritance not allowed"
+ assert len(bases) <= 1, "multiple inheritance not allowed"
assert len(storage) == len(set(storage)), "layout conflict"
d[attr] = tuple(storage)
d.setdefault("abstract", False)
return type.__new__(mcs, name, bases, d)
-class EvalContext(object):
+class EvalContext:
"""Holds evaluation time information. Custom attributes can be attached
to it in extensions.
"""
@@ -83,15 +81,14 @@ def get_eval_context(node, ctx):
if ctx is None:
if node.environment is None:
raise RuntimeError(
- "if no eval context is passed, the "
- "node must have an attached "
- "environment."
+ "if no eval context is passed, the node must have an"
+ " attached environment."
)
return EvalContext(node.environment)
return ctx
-class Node(with_metaclass(NodeType, object)):
+class Node(metaclass=NodeType):
"""Baseclass for all Jinja nodes. There are a number of nodes available
of different types. There are four major types:
@@ -118,21 +115,17 @@ class Node(with_metaclass(NodeType, object)):
if fields:
if len(fields) != len(self.fields):
if not self.fields:
- raise TypeError("%r takes 0 arguments" % self.__class__.__name__)
+ raise TypeError(f"{self.__class__.__name__!r} takes 0 arguments")
raise TypeError(
- "%r takes 0 or %d argument%s"
- % (
- self.__class__.__name__,
- len(self.fields),
- len(self.fields) != 1 and "s" or "",
- )
+ f"{self.__class__.__name__!r} takes 0 or {len(self.fields)}"
+ f" argument{'s' if len(self.fields) != 1 else ''}"
)
- for name, arg in izip(self.fields, fields):
+ for name, arg in zip(self.fields, fields):
setattr(self, name, arg)
for attr in self.attributes:
setattr(self, attr, attributes.pop(attr, None))
if attributes:
- raise TypeError("unknown attribute %r" % next(iter(attributes)))
+ raise TypeError(f"unknown attribute {next(iter(attributes))!r}")
def iter_fields(self, exclude=None, only=None):
"""This method iterates over all fields that are defined and yields
@@ -179,8 +172,7 @@ class Node(with_metaclass(NodeType, object)):
for child in self.iter_child_nodes():
if isinstance(child, node_type):
yield child
- for result in child.find_all(node_type):
- yield result
+ yield from child.find_all(node_type)
def set_ctx(self, ctx):
"""Reset the context of a node and all child nodes. Per default the
@@ -217,21 +209,17 @@ class Node(with_metaclass(NodeType, object)):
return self
def __eq__(self, other):
- return type(self) is type(other) and tuple(self.iter_fields()) == tuple(
- other.iter_fields()
- )
+ if type(self) is not type(other):
+ return NotImplemented
- def __ne__(self, other):
- return not self.__eq__(other)
+ return tuple(self.iter_fields()) == tuple(other.iter_fields())
- # Restore Python 2 hashing behavior on Python 3
- __hash__ = object.__hash__
+ def __hash__(self):
+ return hash(tuple(self.iter_fields()))
def __repr__(self):
- return "%s(%s)" % (
- self.__class__.__name__,
- ", ".join("%s=%r" % (arg, getattr(self, arg, None)) for arg in self.fields),
- )
+ args_str = ", ".join(f"{a}={getattr(self, a, None)!r}" for a in self.fields)
+ return f"{self.__class__.__name__}({args_str})"
def dump(self):
def _dump(node):
@@ -239,7 +227,7 @@ class Node(with_metaclass(NodeType, object)):
buf.append(repr(node))
return
- buf.append("nodes.%s(" % node.__class__.__name__)
+ buf.append(f"nodes.{node.__class__.__name__}(")
if not node.fields:
buf.append(")")
return
@@ -510,17 +498,7 @@ class Const(Literal):
fields = ("value",)
def as_const(self, eval_ctx=None):
- rv = self.value
- if (
- PY2
- and type(rv) is text_type
- and self.environment.policies["compiler.ascii_str"]
- ):
- try:
- rv = rv.encode("ascii")
- except UnicodeError:
- pass
- return rv
+ return self.value
@classmethod
def from_untrusted(cls, value, lineno=None, environment=None):
@@ -664,11 +642,6 @@ class Filter(Expr):
if eval_ctx.volatile or self.node is None:
raise Impossible()
- # we have to be careful here because we call filter_ below.
- # if this variable would be called filter, 2to3 would wrap the
- # call in a list because it is assuming we are talking about the
- # builtin filter function here which no longer returns a list in
- # 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) is True:
@@ -788,15 +761,15 @@ class Slice(Expr):
class Concat(Expr):
- """Concatenates the list of expressions provided after converting them to
- unicode.
+ """Concatenates the list of expressions provided after converting
+ them to strings.
"""
fields = ("nodes",)
def as_const(self, eval_ctx=None):
eval_ctx = get_eval_context(self, eval_ctx)
- return "".join(text_type(x.as_const(eval_ctx)) for x in self.nodes)
+ return "".join(str(x.as_const(eval_ctx)) for x in self.nodes)
class Compare(Expr):
@@ -831,15 +804,6 @@ class Operand(Helper):
fields = ("op", "expr")
-if __debug__:
- Operand.__doc__ += "\nThe following operators are available: " + ", ".join(
- sorted(
- "``%s``" % x
- for x in set(_binop_to_func) | set(_uaop_to_func) | set(_cmpop_to_func)
- )
- )
-
-
class Mul(BinExpr):
"""Multiplies the left with the right node."""
diff --git a/src/jinja2/optimizer.py b/src/jinja2/optimizer.py
index 7bc78c4..39d059f 100644
--- a/src/jinja2/optimizer.py
+++ b/src/jinja2/optimizer.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
"""The optimizer tries to constant fold expressions and modify the AST
in place so that it should be faster to evaluate.
@@ -24,7 +23,7 @@ class Optimizer(NodeTransformer):
self.environment = environment
def generic_visit(self, node, *args, **kwargs):
- node = super(Optimizer, self).generic_visit(node, *args, **kwargs)
+ node = super().generic_visit(node, *args, **kwargs)
# Do constant folding. Some other nodes besides Expr have
# as_const, but folding them causes errors later on.
diff --git a/src/jinja2/parser.py b/src/jinja2/parser.py
index d588106..eedea7a 100644
--- a/src/jinja2/parser.py
+++ b/src/jinja2/parser.py
@@ -1,7 +1,5 @@
-# -*- coding: utf-8 -*-
"""Parse tokens from the lexer into nodes for the compiler."""
from . import nodes
-from ._compat import imap
from .exceptions import TemplateAssertionError
from .exceptions import TemplateSyntaxError
from .lexer import describe_token
@@ -35,7 +33,7 @@ _math_nodes = {
}
-class Parser(object):
+class Parser:
"""This is the central parsing class Jinja uses. It's passed to
extensions and can be used to parse expressions or statements.
"""
@@ -66,10 +64,10 @@ class Parser(object):
def _fail_ut_eof(self, name, end_token_stack, lineno):
expected = []
for exprs in end_token_stack:
- expected.extend(imap(describe_token_expr, exprs))
+ expected.extend(map(describe_token_expr, exprs))
if end_token_stack:
currently_looking = " or ".join(
- "'%s'" % describe_token_expr(expr) for expr in end_token_stack[-1]
+ map(repr, map(describe_token_expr, end_token_stack[-1]))
)
else:
currently_looking = None
@@ -77,25 +75,23 @@ class Parser(object):
if name is None:
message = ["Unexpected end of template."]
else:
- message = ["Encountered unknown tag '%s'." % name]
+ message = [f"Encountered unknown tag {name!r}."]
if currently_looking:
if name is not None and name in expected:
message.append(
- "You probably made a nesting mistake. Jinja "
- "is expecting this tag, but currently looking "
- "for %s." % currently_looking
+ "You probably made a nesting mistake. Jinja is expecting this tag,"
+ f" but currently looking for {currently_looking}."
)
else:
message.append(
- "Jinja was looking for the following tags: "
- "%s." % currently_looking
+ f"Jinja was looking for the following tags: {currently_looking}."
)
if self._tag_stack:
message.append(
- "The innermost block that needs to be "
- "closed is '%s'." % self._tag_stack[-1]
+ "The innermost block that needs to be closed is"
+ f" {self._tag_stack[-1]!r}."
)
self.fail(" ".join(message), lineno)
@@ -126,7 +122,7 @@ class Parser(object):
"""Return a new free identifier as :class:`~jinja2.nodes.InternalName`."""
self._last_identifier += 1
rv = object.__new__(nodes.InternalName)
- nodes.Node.__init__(rv, "fi%d" % self._last_identifier, lineno=lineno)
+ nodes.Node.__init__(rv, f"fi{self._last_identifier}", lineno=lineno)
return rv
def parse_statement(self):
@@ -265,9 +261,8 @@ class Parser(object):
# raise a nicer error message in that case.
if self.stream.current.type == "sub":
self.fail(
- "Block names in Jinja have to be valid Python "
- "identifiers and may not contain hyphens, use an "
- "underscore instead."
+ "Block names in Jinja have to be valid Python identifiers and may not"
+ " contain hyphens, use an underscore instead."
)
node.body = self.parse_statements(("name:endblock",), drop_needle=True)
@@ -435,7 +430,7 @@ class Parser(object):
target.set_ctx("store")
if not target.can_assign():
self.fail(
- "can't assign to %r" % target.__class__.__name__.lower(), target.lineno
+ f"can't assign to {target.__class__.__name__.lower()!r}", target.lineno
)
return target
@@ -596,7 +591,7 @@ class Parser(object):
elif token.type == "lbrace":
node = self.parse_dict()
else:
- self.fail("unexpected '%s'" % describe_token(token), token.lineno)
+ self.fail(f"unexpected {describe_token(token)!r}", token.lineno)
return node
def parse_tuple(
@@ -658,8 +653,8 @@ class Parser(object):
# tuple.
if not explicit_parentheses:
self.fail(
- "Expected an expression, got '%s'"
- % describe_token(self.stream.current)
+ "Expected an expression,"
+ f" got {describe_token(self.stream.current)!r}"
)
return nodes.Tuple(args, "load", lineno=lineno)
diff --git a/src/jinja2/runtime.py b/src/jinja2/runtime.py
index 3ad7968..00f1f59 100644
--- a/src/jinja2/runtime.py
+++ b/src/jinja2/runtime.py
@@ -1,22 +1,13 @@
-# -*- coding: utf-8 -*-
"""The runtime functions and state used by compiled templates."""
import sys
+from collections import abc
from itertools import chain
from types import MethodType
from markupsafe import escape # noqa: F401
from markupsafe import Markup
-from markupsafe import soft_unicode
-
-from ._compat import abc
-from ._compat import imap
-from ._compat import implements_iterator
-from ._compat import implements_to_string
-from ._compat import iteritems
-from ._compat import PY2
-from ._compat import string_types
-from ._compat import text_type
-from ._compat import with_metaclass
+from markupsafe import soft_str
+
from .exceptions import TemplateNotFound # noqa: F401
from .exceptions import TemplateRuntimeError # noqa: F401
from .exceptions import UndefinedError
@@ -39,18 +30,13 @@ exported = [
"concat",
"escape",
"markup_join",
- "unicode_join",
- "to_string",
+ "str_join",
"identity",
"TemplateNotFound",
"Namespace",
"Undefined",
]
-#: the name of the function that is used to convert something into
-#: a string. We can just use the text type here.
-to_string = text_type
-
def identity(x):
"""Returns its argument. Useful for certain things in the
@@ -60,19 +46,31 @@ def identity(x):
def markup_join(seq):
- """Concatenation that escapes if necessary and converts to unicode."""
+ """Concatenation that escapes if necessary and converts to string."""
buf = []
- iterator = imap(soft_unicode, seq)
+ iterator = map(soft_str, seq)
for arg in iterator:
buf.append(arg)
if hasattr(arg, "__html__"):
- return Markup(u"").join(chain(buf, iterator))
+ return Markup("").join(chain(buf, iterator))
return concat(buf)
+def str_join(seq):
+ """Simple args to string conversion and concatenation."""
+ return concat(map(str, seq))
+
+
def unicode_join(seq):
- """Simple args to unicode conversion and concatenation."""
- return concat(imap(text_type, seq))
+ import warnings
+
+ warnings.warn(
+ "This template must be recompiled with at least Jinja 3.0, or"
+ " it will fail in 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return str_join(seq)
def new_context(
@@ -96,13 +94,13 @@ def new_context(
# we don't want to modify the dict passed
if shared:
parent = dict(parent)
- for key, value in iteritems(locals):
+ for key, value in locals.items():
if value is not missing:
parent[key] = value
return environment.context_class(environment, parent, template_name, blocks)
-class TemplateReference(object):
+class TemplateReference:
"""The `self` in templates."""
def __init__(self, context):
@@ -113,7 +111,7 @@ class TemplateReference(object):
return BlockReference(name, self.__context, blocks, 0)
def __repr__(self):
- return "<%s %r>" % (self.__class__.__name__, self.__context.name)
+ return f"<{self.__class__.__name__} {self.__context.name!r}>"
def _get_func(x):
@@ -155,7 +153,8 @@ def resolve_or_missing(context, key, missing=missing):
return missing
-class Context(with_metaclass(ContextMeta)):
+@abc.Mapping.register
+class Context(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
@@ -191,7 +190,7 @@ class Context(with_metaclass(ContextMeta)):
# create the initial mapping of blocks. Whenever template inheritance
# takes place the runtime will update this mapping with the new blocks
# from the template.
- self.blocks = dict((k, [v]) for k, v in iteritems(blocks))
+ self.blocks = {k: [v] for k, v in blocks.items()}
# In case we detect the fast resolve mode we can set up an alias
# here that bypasses the legacy code logic.
@@ -206,7 +205,7 @@ class Context(with_metaclass(ContextMeta)):
blocks[index]
except LookupError:
return self.environment.undefined(
- "there is no parent block called %r." % name, name="super"
+ f"there is no parent block called {name!r}.", name="super"
)
return BlockReference(name, self, blocks, index)
@@ -244,7 +243,7 @@ class Context(with_metaclass(ContextMeta)):
def get_exported(self):
"""Get a new dict with the exported variables."""
- return dict((k, self.vars[k]) for k in self.exported_vars)
+ return {k: self.vars[k] for k in self.exported_vars}
def get_all(self):
"""Return the complete context as dict including the exported
@@ -290,9 +289,8 @@ class Context(with_metaclass(ContextMeta)):
return __obj(*args, **kwargs)
except StopIteration:
return __self.environment.undefined(
- "value was undefined because "
- "a callable raised a "
- "StopIteration exception"
+ "value was undefined because a callable raised a"
+ " StopIteration exception"
)
def derived(self, locals=None):
@@ -304,7 +302,7 @@ class Context(with_metaclass(ContextMeta)):
self.environment, self.name, {}, self.get_all(), True, None, locals
)
context.eval_ctx = self.eval_ctx
- context.blocks.update((k, list(v)) for k, v in iteritems(self.blocks))
+ context.blocks.update((k, list(v)) for k, v in self.blocks.items())
return context
def _all(meth): # noqa: B902
@@ -318,12 +316,6 @@ class Context(with_metaclass(ContextMeta)):
keys = _all("keys")
values = _all("values")
items = _all("items")
-
- # not available on python 3
- if PY2:
- iterkeys = _all("iterkeys")
- itervalues = _all("itervalues")
- iteritems = _all("iteritems")
del _all
def __contains__(self, name):
@@ -339,17 +331,10 @@ class Context(with_metaclass(ContextMeta)):
return item
def __repr__(self):
- return "<%s %s of %r>" % (
- self.__class__.__name__,
- repr(self.get_all()),
- self.name,
- )
-
+ return f"<{self.__class__.__name__} {self.get_all()!r} of {self.name!r}>"
-abc.Mapping.register(Context)
-
-class BlockReference(object):
+class BlockReference:
"""One block on a template reference."""
def __init__(self, name, context, stack, depth):
@@ -363,7 +348,7 @@ class BlockReference(object):
"""Super the block."""
if self._depth + 1 >= len(self._stack):
return self._context.environment.undefined(
- "there is no parent block called %r." % self.name, name="super"
+ f"there is no parent block called {self.name!r}.", name="super"
)
return BlockReference(self.name, self._context, self._stack, self._depth + 1)
@@ -375,7 +360,6 @@ class BlockReference(object):
return rv
-@implements_iterator
class LoopContext:
"""A wrapper iterable for dynamic ``for`` loops, with information
about the loop and iteration.
@@ -564,10 +548,10 @@ class LoopContext:
return self._recurse(iterable, self._recurse, depth=self.depth)
def __repr__(self):
- return "<%s %d/%d>" % (self.__class__.__name__, self.index, self.length)
+ return f"<{self.__class__.__name__} {self.index}/{self.length}>"
-class Macro(object):
+class Macro:
"""Wraps a macro function."""
def __init__(
@@ -656,20 +640,18 @@ class Macro(object):
elif kwargs:
if "caller" in kwargs:
raise TypeError(
- "macro %r was invoked with two values for "
- "the special caller argument. This is "
- "most likely a bug." % self.name
+ f"macro {self.name!r} was invoked with two values for the special"
+ " caller argument. This is most likely a bug."
)
raise TypeError(
- "macro %r takes no keyword argument %r"
- % (self.name, next(iter(kwargs)))
+ f"macro {self.name!r} takes no keyword argument {next(iter(kwargs))!r}"
)
if self.catch_varargs:
arguments.append(args[self._argument_count :])
elif len(args) > self._argument_count:
raise TypeError(
- "macro %r takes not more than %d argument(s)"
- % (self.name, len(self.arguments))
+ f"macro {self.name!r} takes not more than"
+ f" {len(self.arguments)} argument(s)"
)
return self._invoke(arguments, autoescape)
@@ -682,14 +664,11 @@ class Macro(object):
return rv
def __repr__(self):
- return "<%s %s>" % (
- self.__class__.__name__,
- self.name is None and "anonymous" or repr(self.name),
- )
+ name = "anonymous" if self.name is None else repr(self.name)
+ return f"<{self.__class__.__name__} {name}>"
-@implements_to_string
-class Undefined(object):
+class Undefined:
"""The default undefined type. This undefined type can be printed and
iterated over, but every other access will raise an :exc:`UndefinedError`:
@@ -726,17 +705,17 @@ class Undefined(object):
return self._undefined_hint
if self._undefined_obj is missing:
- return "%r is undefined" % self._undefined_name
+ return f"{self._undefined_name!r} is undefined"
- if not isinstance(self._undefined_name, string_types):
- return "%s has no element %r" % (
- object_type_repr(self._undefined_obj),
- self._undefined_name,
+ if not isinstance(self._undefined_name, str):
+ return (
+ f"{object_type_repr(self._undefined_obj)} has no"
+ f" element {self._undefined_name!r}"
)
- return "%r has no attribute %r" % (
- object_type_repr(self._undefined_obj),
- self._undefined_name,
+ return (
+ f"{object_type_repr(self._undefined_obj)!r} has no"
+ f" attribute {self._undefined_name!r}"
)
@internalcode
@@ -752,51 +731,16 @@ class Undefined(object):
raise AttributeError(name)
return self._fail_with_undefined_error()
- __add__ = (
- __radd__
- ) = (
- __mul__
- ) = (
- __rmul__
- ) = (
- __div__
- ) = (
- __rdiv__
- ) = (
- __truediv__
- ) = (
- __rtruediv__
- ) = (
- __floordiv__
- ) = (
- __rfloordiv__
- ) = (
- __mod__
- ) = (
- __rmod__
- ) = (
- __pos__
- ) = (
- __neg__
- ) = (
- __call__
- ) = (
- __getitem__
- ) = (
- __lt__
- ) = (
- __le__
- ) = (
- __gt__
- ) = (
- __ge__
- ) = (
- __int__
- ) = (
- __float__
- ) = (
- __complex__
- ) = __pow__ = __rpow__ = __sub__ = __rsub__ = _fail_with_undefined_error
+ __add__ = __radd__ = __sub__ = __rsub__ = _fail_with_undefined_error
+ __mul__ = __rmul__ = __div__ = __rdiv__ = _fail_with_undefined_error
+ __truediv__ = __rtruediv__ = _fail_with_undefined_error
+ __floordiv__ = __rfloordiv__ = _fail_with_undefined_error
+ __mod__ = __rmod__ = _fail_with_undefined_error
+ __pos__ = __neg__ = _fail_with_undefined_error
+ __call__ = __getitem__ = _fail_with_undefined_error
+ __lt__ = __le__ = __gt__ = __ge__ = _fail_with_undefined_error
+ __int__ = __float__ = __complex__ = _fail_with_undefined_error
+ __pow__ = __rpow__ = _fail_with_undefined_error
def __eq__(self, other):
return type(self) is type(other)
@@ -808,7 +752,7 @@ class Undefined(object):
return id(type(self))
def __str__(self):
- return u""
+ return ""
def __len__(self):
return 0
@@ -817,11 +761,9 @@ class Undefined(object):
if 0:
yield None
- def __nonzero__(self):
+ def __bool__(self):
return False
- __bool__ = __nonzero__
-
def __repr__(self):
return "Undefined"
@@ -855,66 +797,31 @@ def make_logging_undefined(logger=None, base=None):
base = Undefined
def _log_message(undef):
- if undef._undefined_hint is None:
- if undef._undefined_obj is missing:
- hint = "%s is undefined" % undef._undefined_name
- elif not isinstance(undef._undefined_name, string_types):
- hint = "%s has no element %s" % (
- object_type_repr(undef._undefined_obj),
- undef._undefined_name,
- )
- else:
- hint = "%s has no attribute %s" % (
- object_type_repr(undef._undefined_obj),
- undef._undefined_name,
- )
- else:
- hint = undef._undefined_hint
- logger.warning("Template variable warning: %s", hint)
+ logger.warning("Template variable warning: %s", undef._undefined_message)
class LoggingUndefined(base):
def _fail_with_undefined_error(self, *args, **kwargs):
try:
- return base._fail_with_undefined_error(self, *args, **kwargs)
+ return super()._fail_with_undefined_error(*args, **kwargs)
except self._undefined_exception as e:
- logger.error("Template variable error: %s", str(e))
+ logger.error(f"Template variable error: %s", e)
raise e
def __str__(self):
- rv = base.__str__(self)
_log_message(self)
- return rv
+ return super().__str__()
def __iter__(self):
- rv = base.__iter__(self)
_log_message(self)
- return rv
-
- if PY2:
-
- def __nonzero__(self):
- rv = base.__nonzero__(self)
- _log_message(self)
- return rv
-
- def __unicode__(self):
- rv = base.__unicode__(self)
- _log_message(self)
- return rv
-
- else:
+ return super().__iter__()
- def __bool__(self):
- rv = base.__bool__(self)
- _log_message(self)
- return rv
+ def __bool__(self):
+ _log_message(self)
+ return super().__bool__()
return LoggingUndefined
-# No @implements_to_string decorator here because __str__
-# is not overwritten from Undefined in this class.
-# This would cause a recursion error in Python 2.
class ChainableUndefined(Undefined):
"""An undefined that is chainable, where both ``__getattr__`` and
``__getitem__`` return itself rather than raising an
@@ -942,7 +849,6 @@ class ChainableUndefined(Undefined):
__getitem__ = __getattr__
-@implements_to_string
class DebugUndefined(Undefined):
"""An undefined that returns the debug info when printed.
@@ -960,17 +866,21 @@ class DebugUndefined(Undefined):
__slots__ = ()
def __str__(self):
- if self._undefined_hint is None:
- if self._undefined_obj is missing:
- return u"{{ %s }}" % self._undefined_name
- return "{{ no such element: %s[%r] }}" % (
- object_type_repr(self._undefined_obj),
- self._undefined_name,
+ if self._undefined_hint:
+ message = f"undefined value printed: {self._undefined_hint}"
+
+ elif self._undefined_obj is missing:
+ message = self._undefined_name
+
+ else:
+ message = (
+ f"no such element: {object_type_repr(self._undefined_obj)}"
+ f"[{self._undefined_name!r}]"
)
- return u"{{ undefined value printed: %s }}" % self._undefined_hint
+
+ return f"{{{{ {message} }}}}"
-@implements_to_string
class StrictUndefined(Undefined):
"""An undefined that barks on print and iteration as well as boolean
tests and all kinds of comparisons. In other words: you can do nothing
@@ -992,17 +902,12 @@ class StrictUndefined(Undefined):
"""
__slots__ = ()
- __iter__ = (
- __str__
- ) = (
- __len__
- ) = (
- __nonzero__
- ) = __eq__ = __ne__ = __bool__ = __hash__ = Undefined._fail_with_undefined_error
+ __iter__ = __str__ = __len__ = Undefined._fail_with_undefined_error
+ __eq__ = __ne__ = __bool__ = __hash__ = Undefined._fail_with_undefined_error
-# remove remaining slots attributes, after the metaclass did the magic they
-# are unneeded and irritating as they contain wrong data for the subclasses.
+# Remove slots attributes, after the metaclass is applied they are
+# unneeded and contain wrong data for subclasses.
del (
Undefined.__slots__,
ChainableUndefined.__slots__,
diff --git a/src/jinja2/sandbox.py b/src/jinja2/sandbox.py
index cfd7993..deecf61 100644
--- a/src/jinja2/sandbox.py
+++ b/src/jinja2/sandbox.py
@@ -1,42 +1,27 @@
-# -*- coding: utf-8 -*-
"""A sandbox layer that ensures unsafe operations cannot be performed.
Useful when the template itself comes from an untrusted source.
"""
import operator
import types
-import warnings
+from _string import formatter_field_name_split
+from collections import abc
from collections import deque
from string import Formatter
from markupsafe import EscapeFormatter
from markupsafe import Markup
-from ._compat import abc
-from ._compat import PY2
-from ._compat import range_type
-from ._compat import string_types
from .environment import Environment
from .exceptions import SecurityError
#: maximum number of items a range may produce
MAX_RANGE = 100000
-#: attributes of function objects that are considered unsafe.
-if PY2:
- UNSAFE_FUNCTION_ATTRIBUTES = {
- "func_closure",
- "func_code",
- "func_dict",
- "func_defaults",
- "func_globals",
- }
-else:
- # On versions > python 2 the special attributes on functions are gone,
- # but they remain on methods and generators for whatever reason.
- UNSAFE_FUNCTION_ATTRIBUTES = set()
+#: Unsafe function attributes.
+UNSAFE_FUNCTION_ATTRIBUTES = set()
-#: unsafe method attributes. function attributes are unsafe for methods too
-UNSAFE_METHOD_ATTRIBUTES = {"im_class", "im_func", "im_self"}
+#: Unsafe method attributes. Function attributes are unsafe for methods too.
+UNSAFE_METHOD_ATTRIBUTES = set()
#: unsafe generator attributes.
UNSAFE_GENERATOR_ATTRIBUTES = {"gi_frame", "gi_code"}
@@ -47,41 +32,9 @@ UNSAFE_COROUTINE_ATTRIBUTES = {"cr_frame", "cr_code"}
#: unsafe attributes on async generators
UNSAFE_ASYNC_GENERATOR_ATTRIBUTES = {"ag_code", "ag_frame"}
-# make sure we don't warn in python 2.6 about stuff we don't care about
-warnings.filterwarnings(
- "ignore", "the sets module", DeprecationWarning, module=__name__
-)
-
-_mutable_set_types = (set,)
-_mutable_mapping_types = (dict,)
-_mutable_sequence_types = (list,)
-
-# on python 2.x we can register the user collection types
-try:
- from UserDict import UserDict, DictMixin
- from UserList import UserList
-
- _mutable_mapping_types += (UserDict, DictMixin)
- _mutable_set_types += (UserList,)
-except ImportError:
- pass
-
-# if sets is still available, register the mutable set from there as well
-try:
- from sets import Set
-
- _mutable_set_types += (Set,)
-except ImportError:
- pass
-
-#: register Python 2.6 abstract base classes
-_mutable_set_types += (abc.MutableSet,)
-_mutable_mapping_types += (abc.MutableMapping,)
-_mutable_sequence_types += (abc.MutableSequence,)
-
_mutable_spec = (
(
- _mutable_set_types,
+ abc.MutableSet,
frozenset(
[
"add",
@@ -96,11 +49,11 @@ _mutable_spec = (
),
),
(
- _mutable_mapping_types,
+ abc.MutableMapping,
frozenset(["clear", "pop", "popitem", "setdefault", "update"]),
),
(
- _mutable_sequence_types,
+ abc.MutableSequence,
frozenset(["append", "reverse", "insert", "sort", "extend", "remove"]),
),
(
@@ -159,7 +112,7 @@ def inspect_format_method(callable):
) or callable.__name__ not in ("format", "format_map"):
return None
obj = callable.__self__
- if isinstance(obj, string_types):
+ if isinstance(obj, str):
return obj
@@ -167,12 +120,12 @@ def safe_range(*args):
"""A range that can't generate ranges with a length of more than
MAX_RANGE items.
"""
- rng = range_type(*args)
+ rng = range(*args)
if len(rng) > MAX_RANGE:
raise OverflowError(
"Range too big. The sandbox blocks ranges larger than"
- " MAX_RANGE (%d)." % MAX_RANGE
+ f" MAX_RANGE ({MAX_RANGE})."
)
return rng
@@ -181,7 +134,7 @@ def safe_range(*args):
def unsafe(f):
"""Marks a function or method as unsafe.
- ::
+ .. code-block: python
@unsafe
def delete(self):
@@ -230,10 +183,8 @@ def is_internal_attribute(obj, attr):
def modifies_known_mutable(obj, attr):
"""This function checks if an attribute on a builtin mutable object
- (list, dict, set or deque) would modify it if called. It also supports
- the "user"-versions of the objects (`sets.Set`, `UserDict.*` etc.) and
- with Python 2.6 onwards the abstract base classes `MutableSet`,
- `MutableMapping`, and `MutableSequence`.
+ (list, dict, set or deque) or the corresponding ABCs would modify it
+ if called.
>>> modifies_known_mutable({}, "clear")
True
@@ -244,8 +195,7 @@ def modifies_known_mutable(obj, attr):
>>> modifies_known_mutable([], "index")
False
- If called with an unsupported object (such as unicode) `False` is
- returned.
+ If called with an unsupported object, ``False`` is returned.
>>> modifies_known_mutable("foo", "upper")
False
@@ -383,7 +333,7 @@ class SandboxedEnvironment(Environment):
try:
return obj[argument]
except (TypeError, LookupError):
- if isinstance(argument, string_types):
+ if isinstance(argument, str):
try:
attr = str(argument)
except Exception:
@@ -419,8 +369,8 @@ class SandboxedEnvironment(Environment):
def unsafe_undefined(self, obj, attribute):
"""Return an undefined object for unsafe attributes."""
return self.undefined(
- "access to attribute %r of %r "
- "object is unsafe." % (attribute, obj.__class__.__name__),
+ f"access to attribute {attribute!r} of"
+ f" {obj.__class__.__name__!r} object is unsafe.",
name=attribute,
obj=obj,
exc=SecurityError,
@@ -438,8 +388,8 @@ class SandboxedEnvironment(Environment):
if format_func is not None and format_func.__name__ == "format_map":
if len(args) != 1 or kwargs:
raise TypeError(
- "format_map() takes exactly one argument %d given"
- % (len(args) + (kwargs is not None))
+ "format_map() takes exactly one argument"
+ f" {len(args) + (kwargs is not None)} given"
)
kwargs = args[0]
@@ -458,7 +408,7 @@ class SandboxedEnvironment(Environment):
# the double prefixes are to avoid double keyword argument
# errors when proxying the call.
if not __self.is_safe_callable(__obj):
- raise SecurityError("%r is not safely callable" % (__obj,))
+ raise SecurityError(f"{__obj!r} is not safely callable")
return __context.call(__obj, *args, **kwargs)
@@ -474,16 +424,7 @@ class ImmutableSandboxedEnvironment(SandboxedEnvironment):
return not modifies_known_mutable(obj, attr)
-# This really is not a public API apparently.
-try:
- from _string import formatter_field_name_split
-except ImportError:
-
- def formatter_field_name_split(field_name):
- return field_name._formatter_field_name_split()
-
-
-class SandboxedFormatterMixin(object):
+class SandboxedFormatterMixin:
def __init__(self, env):
self._env = env
diff --git a/src/jinja2/tests.py b/src/jinja2/tests.py
index fabd4ce..bc76326 100644
--- a/src/jinja2/tests.py
+++ b/src/jinja2/tests.py
@@ -1,13 +1,9 @@
-# -*- coding: utf-8 -*-
"""Built-in template tests used with the ``is`` operator."""
-import decimal
import operator
import re
+from collections import abc
+from numbers import Number
-from ._compat import abc
-from ._compat import integer_types
-from ._compat import string_types
-from ._compat import text_type
from .runtime import Undefined
number_re = re.compile(r"^-?\d+(\.\d+)?$")
@@ -87,7 +83,7 @@ def test_integer(value):
.. versionadded:: 2.11
"""
- return isinstance(value, integer_types) and value is not True and value is not False
+ return isinstance(value, int) and value is not True and value is not False
# NOTE: The existing 'number' test matches booleans and integers
@@ -101,17 +97,17 @@ def test_float(value):
def test_lower(value):
"""Return true if the variable is lowercased."""
- return text_type(value).islower()
+ return str(value).islower()
def test_upper(value):
"""Return true if the variable is uppercased."""
- return text_type(value).isupper()
+ return str(value).isupper()
def test_string(value):
"""Return true if the object is a string."""
- return isinstance(value, string_types)
+ return isinstance(value, str)
def test_mapping(value):
@@ -124,7 +120,7 @@ def test_mapping(value):
def test_number(value):
"""Return true if the variable is a number."""
- return isinstance(value, integer_types + (float, complex, decimal.Decimal))
+ return isinstance(value, Number)
def test_sequence(value):
diff --git a/src/jinja2/utils.py b/src/jinja2/utils.py
index b422ba9..8ee0295 100644
--- a/src/jinja2/utils.py
+++ b/src/jinja2/utils.py
@@ -1,28 +1,21 @@
-# -*- coding: utf-8 -*-
import json
import os
import re
-import warnings
+from collections import abc
from collections import deque
from random import choice
from random import randrange
from threading import Lock
+from urllib.parse import quote_from_bytes
from markupsafe import escape
from markupsafe import Markup
-from ._compat import abc
-from ._compat import string_types
-from ._compat import text_type
-from ._compat import url_quote
-
_word_split_re = re.compile(r"(\s+)")
+_lead_pattern = "|".join(map(re.escape, ("(", "<", "&lt;")))
+_trail_pattern = "|".join(map(re.escape, (".", ",", ")", ">", "\n", "&gt;")))
_punctuation_re = re.compile(
- "^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$"
- % (
- "|".join(map(re.escape, ("(", "<", "&lt;"))),
- "|".join(map(re.escape, (".", ",", ")", ">", "\n", "&gt;"))),
- )
+ fr"^(?P<lead>(?:{_lead_pattern})*)(?P<middle>.*?)(?P<trail>(?:{_trail_pattern})*)$"
)
_simple_email_re = re.compile(r"^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$")
_striptags_re = re.compile(r"(<!--.*?-->|<[^>]*>)")
@@ -36,7 +29,7 @@ missing = type("MissingType", (), {"__repr__": lambda x: "missing"})()
# internal code
internal_code = set()
-concat = u"".join
+concat = "".join
_slash_escape = "\\/" not in json.dumps("/")
@@ -168,27 +161,18 @@ def object_type_repr(obj):
cls = type(obj)
- # __builtin__ in 2.x, builtins in 3.x
- if cls.__module__ in ("__builtin__", "builtins"):
- name = cls.__name__
- else:
- name = cls.__module__ + "." + cls.__name__
+ if cls.__module__ == "builtins":
+ return f"{cls.__name__} object"
- return "%s object" % name
+ return f"{cls.__module__}.{cls.__name__} object"
-def pformat(obj, verbose=False):
- """Prettyprint an object. Either use the `pretty` library or the
- builtin `pprint`.
+def pformat(obj):
+ """Format an object using :func:`pprint.pformat`.
"""
- try:
- from pretty import pretty
-
- return pretty(obj, verbose=verbose)
- except ImportError:
- from pprint import pformat
+ from pprint import pformat
- return pformat(obj)
+ return pformat(obj)
def urlize(text, trim_url_limit=None, rel=None, target=None):
@@ -205,14 +189,16 @@ def urlize(text, trim_url_limit=None, rel=None, target=None):
If target is not None, a target attribute will be added to the link.
"""
- trim_url = (
- lambda x, limit=trim_url_limit: limit is not None
- and (x[:limit] + (len(x) >= limit and "..." or ""))
- or x
- )
- words = _word_split_re.split(text_type(escape(text)))
- rel_attr = rel and ' rel="%s"' % text_type(escape(rel)) or ""
- target_attr = target and ' target="%s"' % escape(target) or ""
+
+ def trim_url(x, limit=trim_url_limit):
+ if limit is not None:
+ return x[:limit] + ("..." if len(x) >= limit else "")
+
+ return x
+
+ words = _word_split_re.split(str(escape(text)))
+ rel_attr = f' rel="{escape(rel)}"' if rel else ""
+ target_attr = f' target="{escape(target)}"' if target else ""
for i, word in enumerate(words):
match = _punctuation_re.match(word)
@@ -230,18 +216,13 @@ def urlize(text, trim_url_limit=None, rel=None, target=None):
or middle.endswith(".com")
)
):
- middle = '<a href="http://%s"%s%s>%s</a>' % (
- middle,
- rel_attr,
- target_attr,
- trim_url(middle),
+ middle = (
+ f'<a href="http://{middle}"{rel_attr}{target_attr}>'
+ f"{trim_url(middle)}</a>"
)
if middle.startswith("http://") or middle.startswith("https://"):
- middle = '<a href="%s"%s%s>%s</a>' % (
- middle,
- rel_attr,
- target_attr,
- trim_url(middle),
+ middle = (
+ f'<a href="{middle}"{rel_attr}{target_attr}>{trim_url(middle)}</a>'
)
if (
"@" in middle
@@ -249,10 +230,10 @@ def urlize(text, trim_url_limit=None, rel=None, target=None):
and ":" not in middle
and _simple_email_re.match(middle)
):
- middle = '<a href="mailto:%s">%s</a>' % (middle, middle)
+ middle = f'<a href="mailto:{middle}">{middle}</a>'
if lead + middle + trail != word:
words[i] = lead + middle + trail
- return u"".join(words)
+ return "".join(words)
def generate_lorem_ipsum(n=5, html=True, min=20, max=100):
@@ -292,7 +273,7 @@ def generate_lorem_ipsum(n=5, html=True, min=20, max=100):
p.append(word)
# ensure that the paragraph ends with a dot.
- p = u" ".join(p)
+ p = " ".join(p)
if p.endswith(","):
p = p[:-1] + "."
elif not p.endswith("."):
@@ -300,11 +281,11 @@ def generate_lorem_ipsum(n=5, html=True, min=20, max=100):
result.append(p)
if not html:
- return u"\n\n".join(result)
- return Markup(u"\n".join(u"<p>%s</p>" % escape(x) for x in result))
+ return "\n\n".join(result)
+ return Markup("\n".join(f"<p>{escape(x)}</p>" for x in result))
-def unicode_urlencode(obj, charset="utf-8", for_qs=False):
+def url_quote(obj, charset="utf-8", for_qs=False):
"""Quote a string for use in a URL using the given charset.
This function is misnamed, it is a wrapper around
@@ -315,17 +296,14 @@ def unicode_urlencode(obj, charset="utf-8", for_qs=False):
:param charset: Encode text to bytes using this charset.
:param for_qs: Quote "/" and use "+" for spaces.
"""
- if not isinstance(obj, string_types):
- obj = text_type(obj)
+ if not isinstance(obj, bytes):
+ if not isinstance(obj, str):
+ obj = str(obj)
- if isinstance(obj, text_type):
obj = obj.encode(charset)
safe = b"" if for_qs else b"/"
- rv = url_quote(obj, safe)
-
- if not isinstance(rv, text_type):
- rv = rv.decode("utf-8")
+ rv = quote_from_bytes(obj, safe)
if for_qs:
rv = rv.replace("%20", "+")
@@ -333,7 +311,20 @@ def unicode_urlencode(obj, charset="utf-8", for_qs=False):
return rv
-class LRUCache(object):
+def unicode_urlencode(obj, charset="utf-8", for_qs=False):
+ import warnings
+
+ warnings.warn(
+ "'unicode_urlencode' has been renamed to 'url_quote'. The old"
+ " name will be removed in version 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return url_quote(obj, charset=charset, for_qs=for_qs)
+
+
+@abc.MutableMapping.register
+class LRUCache:
"""A simple LRU Cache implementation."""
# this is fast for small capacities (something below 1000) but doesn't
@@ -410,7 +401,7 @@ class LRUCache(object):
return len(self._mapping)
def __repr__(self):
- return "<%s %r>" % (self.__class__.__name__, self._mapping)
+ return f"<{self.__class__.__name__} {self._mapping!r}>"
def __getitem__(self, key):
"""Get an item from the cache. Moves the item up so that it has the
@@ -469,56 +460,14 @@ class LRUCache(object):
result.reverse()
return result
- def iteritems(self):
- """Iterate over all items."""
- warnings.warn(
- "'iteritems()' will be removed in version 3.0. Use"
- " 'iter(cache.items())' instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- return iter(self.items())
-
def values(self):
"""Return a list of all values."""
return [x[1] for x in self.items()]
- def itervalue(self):
- """Iterate over all values."""
- warnings.warn(
- "'itervalue()' will be removed in version 3.0. Use"
- " 'iter(cache.values())' instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- return iter(self.values())
-
- def itervalues(self):
- """Iterate over all values."""
- warnings.warn(
- "'itervalues()' will be removed in version 3.0. Use"
- " 'iter(cache.values())' instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- return iter(self.values())
-
def keys(self):
"""Return a list of all keys ordered by most recent usage."""
return list(self)
- def iterkeys(self):
- """Iterate over all keys in the cache dict, ordered by
- the most recent usage.
- """
- warnings.warn(
- "'iterkeys()' will be removed in version 3.0. Use"
- " 'iter(cache.keys())' instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- return iter(self)
-
def __iter__(self):
return reversed(tuple(self._queue))
@@ -531,9 +480,6 @@ class LRUCache(object):
__copy__ = copy
-abc.MutableMapping.register(LRUCache)
-
-
def select_autoescape(
enabled_extensions=("html", "htm", "xml"),
disabled_extensions=(),
@@ -574,8 +520,8 @@ def select_autoescape(
.. versionadded:: 2.9
"""
- enabled_patterns = tuple("." + x.lstrip(".").lower() for x in enabled_extensions)
- disabled_patterns = tuple("." + x.lstrip(".").lower() for x in disabled_extensions)
+ enabled_patterns = tuple(f".{x.lstrip('.').lower()}" for x in enabled_extensions)
+ disabled_patterns = tuple(f".{x.lstrip('.').lower()}" for x in disabled_extensions)
def autoescape(template_name):
if template_name is None:
@@ -612,15 +558,15 @@ def htmlsafe_json_dumps(obj, dumper=None, **kwargs):
dumper = json.dumps
rv = (
dumper(obj, **kwargs)
- .replace(u"<", u"\\u003c")
- .replace(u">", u"\\u003e")
- .replace(u"&", u"\\u0026")
- .replace(u"'", u"\\u0027")
+ .replace("<", "\\u003c")
+ .replace(">", "\\u003e")
+ .replace("&", "\\u0026")
+ .replace("'", "\\u0027")
)
return Markup(rv)
-class Cycler(object):
+class Cycler:
"""Cycle through values by yield them one at a time, then restarting
once the end is reached. Available as ``cycler`` in templates.
@@ -674,21 +620,21 @@ class Cycler(object):
__next__ = next
-class Joiner(object):
+class Joiner:
"""A joining helper for templates."""
- def __init__(self, sep=u", "):
+ def __init__(self, sep=", "):
self.sep = sep
self.used = False
def __call__(self):
if not self.used:
self.used = True
- return u""
+ return ""
return self.sep
-class Namespace(object):
+class Namespace:
"""A namespace object that can hold arbitrary attributes. It may be
initialized from a dictionary or with keyword arguments."""
@@ -709,7 +655,7 @@ class Namespace(object):
self.__attrs[name] = value
def __repr__(self):
- return "<Namespace %r>" % self.__attrs
+ return f"<Namespace {self.__attrs!r}>"
# does this python version support async for in and async generators?
@@ -718,15 +664,3 @@ try:
have_async_gen = True
except SyntaxError:
have_async_gen = False
-
-
-def soft_unicode(s):
- from markupsafe import soft_unicode
-
- warnings.warn(
- "'jinja2.utils.soft_unicode' will be removed in version 3.0."
- " Use 'markupsafe.soft_unicode' instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- return soft_unicode(s)
diff --git a/src/jinja2/visitor.py b/src/jinja2/visitor.py
index d1365bf..590fa9e 100644
--- a/src/jinja2/visitor.py
+++ b/src/jinja2/visitor.py
@@ -1,11 +1,10 @@
-# -*- coding: utf-8 -*-
"""API for traversing the AST nodes. Implemented by the compiler and
meta introspection.
"""
from .nodes import Node
-class NodeVisitor(object):
+class NodeVisitor:
"""Walks the abstract syntax tree and call visitor functions for every
node found. The visitor functions may return values which will be
forwarded by the `visit` method.
@@ -22,8 +21,7 @@ class NodeVisitor(object):
exists for this node. In that case the generic visit function is
used instead.
"""
- method = "visit_" + node.__class__.__name__
- return getattr(self, method, None)
+ return getattr(self, f"visit_{node.__class__.__name__}", None)
def visit(self, node, *args, **kwargs):
"""Visit a node."""
diff --git a/tests/conftest.py b/tests/conftest.py
index 23088a3..ce30d8b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
import os
import pytest
diff --git a/tests/res/package.zip b/tests/res/package.zip
new file mode 100644
index 0000000..d4c9ce9
--- /dev/null
+++ b/tests/res/package.zip
Binary files differ
diff --git a/tests/test_api.py b/tests/test_api.py
index 7a1cae8..2679e8f 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
import os
import shutil
import tempfile
@@ -25,12 +24,11 @@ from jinja2.utils import environmentfunction
from jinja2.utils import evalcontextfunction
-class TestExtendedAPI(object):
+class TestExtendedAPI:
def test_item_and_attribute(self, env):
from jinja2.sandbox import SandboxedEnvironment
for env in Environment(), SandboxedEnvironment():
- # the |list is necessary for python3
tmpl = env.from_string("{{ foo.items()|list }}")
assert tmpl.render(foo={"items": 42}) == "[('items', 42)]"
tmpl = env.from_string('{{ foo|attr("items")()|list }}')
@@ -161,11 +159,11 @@ class TestExtendedAPI(object):
t.render(total=MAX_RANGE + 1)
-class TestMeta(object):
+class TestMeta:
def test_find_undeclared_variables(self, env):
ast = env.parse("{% set foo = 42 %}{{ bar + foo }}")
x = meta.find_undeclared_variables(ast)
- assert x == set(["bar"])
+ assert x == {"bar"}
ast = env.parse(
"{% set foo = 42 %}{{ bar + foo }}"
@@ -174,11 +172,11 @@ class TestMeta(object):
"{% endfor %}"
)
x = meta.find_undeclared_variables(ast)
- assert x == set(["bar", "seq", "muh"])
+ assert x == {"bar", "seq", "muh"}
ast = env.parse("{% for x in range(5) %}{{ x }}{% endfor %}{{ foo }}")
x = meta.find_undeclared_variables(ast)
- assert x == set(["foo"])
+ assert x == {"foo"}
def test_find_refererenced_templates(self, env):
ast = env.parse('{% extends "layout.html" %}{% include helper %}')
@@ -214,7 +212,7 @@ class TestMeta(object):
assert list(i) == ["foo.html", "bar.html", None]
-class TestStreaming(object):
+class TestStreaming:
def test_basic_streaming(self, env):
t = env.from_string(
"<ul>{% for item in seq %}<li>{{ loop.index }} - {{ item }}</li>"
@@ -231,8 +229,8 @@ class TestStreaming(object):
)
stream = tmpl.stream(seq=list(range(3)))
stream.enable_buffering(size=3)
- assert next(stream) == u"<ul><li>1"
- assert next(stream) == u" - 0</li>"
+ assert next(stream) == "<ul><li>1"
+ assert next(stream) == " - 0</li>"
def test_streaming_behavior(self, env):
tmpl = env.from_string("")
@@ -246,7 +244,7 @@ class TestStreaming(object):
def test_dump_stream(self, env):
tmp = tempfile.mkdtemp()
try:
- tmpl = env.from_string(u"\u2713")
+ tmpl = env.from_string("\u2713")
stream = tmpl.stream()
stream.dump(os.path.join(tmp, "dump.txt"), "utf-8")
with open(os.path.join(tmp, "dump.txt"), "rb") as f:
@@ -255,7 +253,7 @@ class TestStreaming(object):
shutil.rmtree(tmp)
-class TestUndefined(object):
+class TestUndefined:
def test_stopiteration_is_undefined(self):
def test():
raise StopIteration()
@@ -274,7 +272,7 @@ class TestUndefined(object):
# 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):
+ class Error:
@property
def __class__(self):
raise AttributeError()
@@ -287,7 +285,7 @@ class TestUndefined(object):
def test_logging_undefined(self):
_messages = []
- class DebugLogger(object):
+ class DebugLogger:
def warning(self, msg, *args):
_messages.append("W:" + msg % args)
@@ -296,23 +294,23 @@ class TestUndefined(object):
logging_undefined = make_logging_undefined(DebugLogger())
env = Environment(undefined=logging_undefined)
- assert env.from_string("{{ missing }}").render() == u""
+ assert env.from_string("{{ missing }}").render() == ""
pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render)
assert env.from_string("{{ missing|list }}").render() == "[]"
assert env.from_string("{{ missing is not defined }}").render() == "True"
assert env.from_string("{{ foo.missing }}").render(foo=42) == ""
assert env.from_string("{{ not missing }}").render() == "True"
assert _messages == [
- "W:Template variable warning: missing is undefined",
+ "W:Template variable warning: 'missing' is undefined",
"E:Template variable error: 'missing' is undefined",
- "W:Template variable warning: missing is undefined",
- "W:Template variable warning: int object has no attribute missing",
- "W:Template variable warning: missing is undefined",
+ "W:Template variable warning: 'missing' is undefined",
+ "W:Template variable warning: 'int object' has no attribute 'missing'",
+ "W:Template variable warning: 'missing' is undefined",
]
def test_default_undefined(self):
env = Environment(undefined=Undefined)
- assert env.from_string("{{ missing }}").render() == u""
+ assert env.from_string("{{ missing }}").render() == ""
pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render)
assert env.from_string("{{ missing|list }}").render() == "[]"
assert env.from_string("{{ missing is not defined }}").render() == "True"
@@ -330,7 +328,7 @@ class TestUndefined(object):
def test_chainable_undefined(self):
env = Environment(undefined=ChainableUndefined)
# The following tests are copied from test_default_undefined
- assert env.from_string("{{ missing }}").render() == u""
+ assert env.from_string("{{ missing }}").render() == ""
assert env.from_string("{{ missing|list }}").render() == "[]"
assert env.from_string("{{ missing is not defined }}").render() == "True"
assert env.from_string("{{ foo.missing }}").render(foo=42) == ""
@@ -340,19 +338,17 @@ class TestUndefined(object):
getattr(ChainableUndefined, "__slots__") # noqa: B009
# The following tests ensure subclass functionality works as expected
- assert env.from_string('{{ missing.bar["baz"] }}').render() == u""
- assert (
- env.from_string('{{ foo.bar["baz"]._undefined_name }}').render() == u"foo"
- )
+ assert env.from_string('{{ missing.bar["baz"] }}').render() == ""
+ assert env.from_string('{{ foo.bar["baz"]._undefined_name }}').render() == "foo"
assert (
env.from_string('{{ foo.bar["baz"]._undefined_name }}').render(foo=42)
- == u"bar"
+ == "bar"
)
assert (
env.from_string('{{ foo.bar["baz"]._undefined_name }}').render(
foo={"bar": 42}
)
- == u"baz"
+ == "baz"
)
def test_debug_undefined(self):
@@ -363,13 +359,13 @@ class TestUndefined(object):
assert env.from_string("{{ missing is not defined }}").render() == "True"
assert (
env.from_string("{{ foo.missing }}").render(foo=42)
- == u"{{ no such element: int object['missing'] }}"
+ == "{{ no such element: int object['missing'] }}"
)
assert env.from_string("{{ not missing }}").render() == "True"
undefined_hint = "this is testing undefined hint of DebugUndefined"
assert (
str(DebugUndefined(hint=undefined_hint))
- == u"{{ undefined value printed: %s }}" % undefined_hint
+ == f"{{{{ undefined value printed: {undefined_hint} }}}}"
)
with pytest.raises(AttributeError):
getattr(DebugUndefined, "__slots__") # noqa: B009
@@ -407,7 +403,7 @@ class TestUndefined(object):
Undefined(obj=42, name="upper")()
-class TestLowLevel(object):
+class TestLowLevel:
def test_custom_code_generator(self):
class CustomCodeGenerator(CodeGenerator):
def visit_Const(self, node, frame=None):
@@ -415,7 +411,7 @@ class TestLowLevel(object):
if node.value == "foo":
self.write(repr("bar"))
else:
- super(CustomCodeGenerator, self).visit_Const(node, frame)
+ super().visit_Const(node, frame)
class CustomEnvironment(Environment):
code_generator_class = CustomCodeGenerator
diff --git a/tests/test_async.py b/tests/test_async.py
index 2b9974e..bfdcdb2 100644
--- a/tests/test_async.py
+++ b/tests/test_async.py
@@ -130,7 +130,7 @@ def test_env_async():
return env
-class TestAsyncImports(object):
+class TestAsyncImports:
def test_context_imports(self, test_env_async):
t = test_env_async.from_string('{% import "module" as m %}{{ m.test() }}')
assert t.render(foo=42) == "[|23]"
@@ -179,7 +179,7 @@ class TestAsyncImports(object):
assert not hasattr(m, "notthere")
-class TestAsyncIncludes(object):
+class TestAsyncIncludes:
def test_context_include(self, test_env_async):
t = test_env_async.from_string('{% include "header" %}')
assert t.render(foo=42) == "[42|23]"
@@ -276,7 +276,7 @@ class TestAsyncIncludes(object):
assert t.render().strip() == "(FOO)"
-class TestAsyncForLoop(object):
+class TestAsyncForLoop:
def test_simple(self, test_env_async):
tmpl = test_env_async.from_string("{% for item in seq %}{{ item }}{% endfor %}")
assert tmpl.render(seq=list(range(10))) == "0123456789"
@@ -340,8 +340,7 @@ class TestAsyncForLoop(object):
def test_varlen(self, test_env_async):
def inner():
- for item in range(5):
- yield item
+ yield from range(5)
tmpl = test_env_async.from_string(
"{% for item in iter %}{{ item }}{% endfor %}"
diff --git a/tests/test_bytecode_cache.py b/tests/test_bytecode_cache.py
index c7882b1..5b9eb0f 100644
--- a/tests/test_bytecode_cache.py
+++ b/tests/test_bytecode_cache.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
import pytest
from jinja2 import Environment
@@ -14,14 +13,14 @@ def env(package_loader, tmp_path):
return Environment(loader=package_loader, bytecode_cache=bytecode_cache)
-class TestByteCodeCache(object):
+class TestByteCodeCache:
def test_simple(self, env):
tmpl = env.get_template("test.html")
assert tmpl.render().strip() == "BAR"
pytest.raises(TemplateNotFound, env.get_template, "missing.html")
-class MockMemcached(object):
+class MockMemcached:
class Error(Exception):
pass
@@ -44,7 +43,7 @@ class MockMemcached(object):
raise self.Error()
-class TestMemcachedBytecodeCache(object):
+class TestMemcachedBytecodeCache:
def test_dump_load(self):
memcached = MockMemcached()
m = MemcachedBytecodeCache(memcached)
diff --git a/tests/test_core_tags.py b/tests/test_core_tags.py
index 1bd96c4..4bb95e0 100644
--- a/tests/test_core_tags.py
+++ b/tests/test_core_tags.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
import pytest
from jinja2 import DictLoader
@@ -13,7 +12,7 @@ def env_trim():
return Environment(trim_blocks=True)
-class TestForLoop(object):
+class TestForLoop:
def test_simple(self, env):
tmpl = env.from_string("{% for item in seq %}{{ item }}{% endfor %}")
assert tmpl.render(seq=list(range(10))) == "0123456789"
@@ -101,12 +100,8 @@ class TestForLoop(object):
assert not output
def test_varlen(self, env):
- def inner():
- for item in range(5):
- yield item
-
tmpl = env.from_string("{% for item in iter %}{{ item }}{% endfor %}")
- output = tmpl.render(iter=inner())
+ output = tmpl.render(iter=range(5))
assert output == "01234"
def test_noniter(self, env):
@@ -303,7 +298,7 @@ class TestForLoop(object):
assert tmpl.render(x=0, seq=[1, 2, 3]) == "919293"
-class TestIfCondition(object):
+class TestIfCondition:
def test_simple(self, env):
tmpl = env.from_string("""{% if true %}...{% endif %}""")
assert tmpl.render() == "..."
@@ -316,10 +311,8 @@ class TestIfCondition(object):
assert tmpl.render() == "..."
def test_elif_deep(self, env):
- elifs = "\n".join("{{% elif a == {0} %}}{0}".format(i) for i in range(1, 1000))
- tmpl = env.from_string(
- "{{% if a == 0 %}}0{0}{{% else %}}x{{% endif %}}".format(elifs)
- )
+ elifs = "\n".join(f"{{% elif a == {i} %}}{i}" for i in range(1, 1000))
+ tmpl = env.from_string(f"{{% if a == 0 %}}0{elifs}{{% else %}}x{{% endif %}}")
for x in (0, 10, 999):
assert tmpl.render(a=x).strip() == str(x)
assert tmpl.render(a=1000).strip() == "x"
@@ -345,7 +338,7 @@ class TestIfCondition(object):
assert tmpl.render() == "1"
-class TestMacros(object):
+class TestMacros:
def test_simple(self, env_trim):
tmpl = env_trim.from_string(
"""\
@@ -469,7 +462,7 @@ class TestMacros(object):
assert tmpl.module.m(1, x=7) == "1|7|7"
-class TestSet(object):
+class TestSet:
def test_normal(self, env_trim):
tmpl = env_trim.from_string("{% set foo = 1 %}{{ foo }}")
assert tmpl.render() == "1"
@@ -478,7 +471,7 @@ class TestSet(object):
def test_block(self, env_trim):
tmpl = env_trim.from_string("{% set foo %}42{% endset %}{{ foo }}")
assert tmpl.render() == "42"
- assert tmpl.module.foo == u"42"
+ assert tmpl.module.foo == "42"
def test_block_escaping(self):
env = Environment(autoescape=True)
@@ -557,7 +550,7 @@ class TestSet(object):
"{% set foo | trim | length | string %} 42 {% endset %}{{ foo }}"
)
assert tmpl.render() == "2"
- assert tmpl.module.foo == u"2"
+ assert tmpl.module.foo == "2"
def test_block_filtered_set(self, env_trim):
def _myfilter(val, arg):
@@ -573,10 +566,10 @@ class TestSet(object):
"{{ foo }}"
)
assert tmpl.render() == "11"
- assert tmpl.module.foo == u"11"
+ assert tmpl.module.foo == "11"
-class TestWith(object):
+class TestWith:
def test_with(self, env):
tmpl = env.from_string(
"""\
diff --git a/tests/test_debug.py b/tests/test_debug.py
index 284b9e9..0aec78a 100644
--- a/tests/test_debug.py
+++ b/tests/test_debug.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
import pickle
import re
from traceback import format_exception
@@ -17,17 +16,16 @@ def fs_env(filesystem_loader):
return Environment(loader=filesystem_loader)
-class TestDebug(object):
+class TestDebug:
def assert_traceback_matches(self, callback, expected_tb):
with pytest.raises(Exception) as exc_info:
callback()
tb = format_exception(exc_info.type, exc_info.value, exc_info.tb)
m = re.search(expected_tb.strip(), "".join(tb))
- assert m is not None, "Traceback did not match:\n\n%s\nexpected:\n%s" % (
- "".join(tb),
- expected_tb,
- )
+ assert (
+ m is not None
+ ), "Traceback did not match:\n\n{''.join(tb)}\nexpected:\n{expected_tb}"
def test_runtime_error(self, fs_env):
def test():
diff --git a/tests/test_ext.py b/tests/test_ext.py
index 8e4b411..94d20be 100644
--- a/tests/test_ext.py
+++ b/tests/test_ext.py
@@ -1,5 +1,5 @@
-# -*- coding: utf-8 -*-
import re
+from io import BytesIO
import pytest
@@ -7,9 +7,6 @@ from jinja2 import contextfunction
from jinja2 import DictLoader
from jinja2 import Environment
from jinja2 import nodes
-from jinja2._compat import BytesIO
-from jinja2._compat import itervalues
-from jinja2._compat import text_type
from jinja2.exceptions import TemplateAssertionError
from jinja2.ext import Extension
from jinja2.lexer import count_newlines
@@ -54,14 +51,14 @@ newstyle_i18n_templates = {
languages = {
"de": {
- "missing": u"fehlend",
- "watch out": u"pass auf",
- "One user online": u"Ein Benutzer online",
- "%(user_count)s users online": u"%(user_count)s Benutzer online",
- "User: %(num)s": u"Benutzer: %(num)s",
- "User: %(count)s": u"Benutzer: %(count)s",
- "%(num)s apple": u"%(num)s Apfel",
- "%(num)s apples": u"%(num)s Äpfel",
+ "missing": "fehlend",
+ "watch out": "pass auf",
+ "One user online": "Ein Benutzer online",
+ "%(user_count)s users online": "%(user_count)s Benutzer online",
+ "User: %(num)s": "Benutzer: %(num)s",
+ "User: %(count)s": "Benutzer: %(count)s",
+ "%(num)s apple": "%(num)s Apfel",
+ "%(num)s apples": "%(num)s Äpfel",
}
}
@@ -97,7 +94,7 @@ newstyle_i18n_env.install_gettext_callables(gettext, ngettext, newstyle=True)
class ExampleExtension(Extension):
- tags = set(["test"])
+ tags = {"test"}
ext_attr = 42
context_reference_node_cls = nodes.ContextReference
@@ -117,12 +114,9 @@ class ExampleExtension(Extension):
).set_lineno(next(parser.stream).lineno)
def _dump(self, sandboxed, ext_attr, imported_object, context):
- return "%s|%s|%s|%s|%s" % (
- sandboxed,
- ext_attr,
- imported_object,
- context.blocks,
- context.get("test_var"),
+ return (
+ f"{sandboxed}|{ext_attr}|{imported_object}|{context.blocks}"
+ f"|{context.get('test_var')}"
)
@@ -139,8 +133,7 @@ class StreamFilterExtension(Extension):
def filter_stream(self, stream):
for token in stream:
if token.type == "data":
- for t in self.interpolate(token):
- yield t
+ yield from self.interpolate(token)
else:
yield token
@@ -167,7 +160,7 @@ class StreamFilterExtension(Extension):
yield Token(lineno, "data", token.value[pos:])
-class TestExtensions(object):
+class TestExtensions:
def test_extend_late(self):
env = Environment()
env.add_extension("jinja2.ext.autoescape")
@@ -230,7 +223,7 @@ class TestExtensions(object):
original = Environment(extensions=[ExampleExtension])
overlay = original.overlay()
for env in original, overlay:
- for ext in itervalues(env.extensions):
+ for ext in env.extensions.values():
assert ext.environment is env
def test_preprocessor_extension(self):
@@ -263,10 +256,10 @@ class TestExtensions(object):
out = t.render()
for value in ("context", "cycler", "filters", "abs", "tests", "!="):
- assert "'{}'".format(value) in out
+ assert f"'{value}'" in out
-class TestInternationalization(object):
+class TestInternationalization:
def test_trans(self):
tmpl = i18n_env.get_template("child.html")
assert tmpl.render(LANGUAGE="de") == "<title>fehlend</title>pass auf"
@@ -340,87 +333,79 @@ class TestInternationalization(object):
from jinja2.ext import babel_extract
source = BytesIO(
+ b"""
+ {{ gettext('Hello World') }}
+ {% trans %}Hello World{% endtrans %}
+ {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
"""
- {{ gettext('Hello World') }}
- {% trans %}Hello World{% endtrans %}
- {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
- """.encode(
- "ascii"
- )
- ) # make python 3 happy
+ )
assert list(babel_extract(source, ("gettext", "ngettext", "_"), [], {})) == [
- (2, "gettext", u"Hello World", []),
- (3, "gettext", u"Hello World", []),
- (4, "ngettext", (u"%(users)s user", u"%(users)s users", None), []),
+ (2, "gettext", "Hello World", []),
+ (3, "gettext", "Hello World", []),
+ (4, "ngettext", ("%(users)s user", "%(users)s users", None), []),
]
def test_extract_trimmed(self):
from jinja2.ext import babel_extract
source = BytesIO(
+ b"""
+ {{ gettext(' Hello \n World') }}
+ {% trans trimmed %} Hello \n World{% endtrans %}
+ {% trans trimmed %}{{ users }} \n user
+ {%- pluralize %}{{ users }} \n users{% endtrans %}
"""
- {{ gettext(' Hello \n World') }}
- {% trans trimmed %} Hello \n World{% endtrans %}
- {% trans trimmed %}{{ users }} \n user
- {%- pluralize %}{{ users }} \n users{% endtrans %}
- """.encode(
- "ascii"
- )
- ) # make python 3 happy
+ )
assert list(babel_extract(source, ("gettext", "ngettext", "_"), [], {})) == [
- (2, "gettext", u" Hello \n World", []),
- (4, "gettext", u"Hello World", []),
- (6, "ngettext", (u"%(users)s user", u"%(users)s users", None), []),
+ (2, "gettext", " Hello \n World", []),
+ (4, "gettext", "Hello World", []),
+ (6, "ngettext", ("%(users)s user", "%(users)s users", None), []),
]
def test_extract_trimmed_option(self):
from jinja2.ext import babel_extract
source = BytesIO(
+ b"""
+ {{ gettext(' Hello \n World') }}
+ {% trans %} Hello \n World{% endtrans %}
+ {% trans %}{{ users }} \n user
+ {%- pluralize %}{{ users }} \n users{% endtrans %}
"""
- {{ gettext(' Hello \n World') }}
- {% trans %} Hello \n World{% endtrans %}
- {% trans %}{{ users }} \n user
- {%- pluralize %}{{ users }} \n users{% endtrans %}
- """.encode(
- "ascii"
- )
- ) # make python 3 happy
+ )
opts = {"trimmed": "true"}
assert list(babel_extract(source, ("gettext", "ngettext", "_"), [], opts)) == [
- (2, "gettext", u" Hello \n World", []),
- (4, "gettext", u"Hello World", []),
- (6, "ngettext", (u"%(users)s user", u"%(users)s users", None), []),
+ (2, "gettext", " Hello \n World", []),
+ (4, "gettext", "Hello World", []),
+ (6, "ngettext", ("%(users)s user", "%(users)s users", None), []),
]
def test_comment_extract(self):
from jinja2.ext import babel_extract
source = BytesIO(
+ b"""
+ {# trans first #}
+ {{ gettext('Hello World') }}
+ {% trans %}Hello World{% endtrans %}{# trans second #}
+ {#: third #}
+ {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
"""
- {# trans first #}
- {{ gettext('Hello World') }}
- {% trans %}Hello World{% endtrans %}{# trans second #}
- {#: third #}
- {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
- """.encode(
- "utf-8"
- )
- ) # make python 3 happy
+ )
assert list(
babel_extract(source, ("gettext", "ngettext", "_"), ["trans", ":"], {})
) == [
- (3, "gettext", u"Hello World", ["first"]),
- (4, "gettext", u"Hello World", ["second"]),
- (6, "ngettext", (u"%(users)s user", u"%(users)s users", None), ["third"]),
+ (3, "gettext", "Hello World", ["first"]),
+ (4, "gettext", "Hello World", ["second"]),
+ (6, "ngettext", ("%(users)s user", "%(users)s users", None), ["third"]),
]
-class TestScope(object):
+class TestScope:
def test_basic_scope_behavior(self):
# This is what the old with statement compiled down to
class ScopeExt(Extension):
- tags = set(["scope"])
+ tags = {"scope"}
def parse(self, parser):
node = nodes.Scope(lineno=next(parser.stream).lineno)
@@ -449,7 +434,7 @@ class TestScope(object):
assert tmpl.render(b=3, e=4) == "1|2|2|4|5"
-class TestNewstyleInternationalization(object):
+class TestNewstyleInternationalization:
def test_trans(self):
tmpl = newstyle_i18n_env.get_template("child.html")
assert tmpl.render(LANGUAGE="de") == "<title>fehlend</title>pass auf"
@@ -478,12 +463,12 @@ class TestNewstyleInternationalization(object):
def test_newstyle_plural(self):
tmpl = newstyle_i18n_env.get_template("ngettext.html")
assert tmpl.render(LANGUAGE="de", apples=1) == "1 Apfel"
- assert tmpl.render(LANGUAGE="de", apples=5) == u"5 Äpfel"
+ assert tmpl.render(LANGUAGE="de", apples=5) == "5 Äpfel"
def test_autoescape_support(self):
env = Environment(extensions=["jinja2.ext.autoescape", "jinja2.ext.i18n"])
env.install_gettext_callables(
- lambda x: u"<strong>Wert: %(name)s</strong>",
+ lambda x: "<strong>Wert: %(name)s</strong>",
lambda s, p, n: s,
newstyle=True,
)
@@ -504,7 +489,7 @@ class TestNewstyleInternationalization(object):
def test_num_used_twice(self):
tmpl = newstyle_i18n_env.get_template("ngettext_long.html")
- assert tmpl.render(apples=5, LANGUAGE="de") == u"5 Äpfel"
+ assert tmpl.render(apples=5, LANGUAGE="de") == "5 Äpfel"
def test_num_called_num(self):
source = newstyle_i18n_env.compile(
@@ -519,7 +504,7 @@ class TestNewstyleInternationalization(object):
# would work) for better performance. This only works on the
# newstyle gettext of course
assert (
- re.search(r"u?'\%\(num\)s apple', u?'\%\(num\)s " r"apples', 3", source)
+ re.search(r"u?'%\(num\)s apple', u?'%\(num\)s apples', 3", source)
is not None
)
@@ -540,7 +525,7 @@ class TestNewstyleInternationalization(object):
assert t.render() == "%(foo)s"
-class TestAutoEscape(object):
+class TestAutoEscape:
def test_scoped_setting(self):
env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=True)
tmpl = env.from_string(
@@ -553,9 +538,9 @@ class TestAutoEscape(object):
"""
)
assert tmpl.render().split() == [
- u"&lt;HelloWorld&gt;",
- u"<HelloWorld>",
- u"&lt;HelloWorld&gt;",
+ "&lt;HelloWorld&gt;",
+ "<HelloWorld>",
+ "&lt;HelloWorld&gt;",
]
env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=False)
@@ -569,9 +554,9 @@ class TestAutoEscape(object):
"""
)
assert tmpl.render().split() == [
- u"<HelloWorld>",
- u"&lt;HelloWorld&gt;",
- u"<HelloWorld>",
+ "<HelloWorld>",
+ "&lt;HelloWorld&gt;",
+ "<HelloWorld>",
]
def test_nonvolatile(self):
@@ -614,7 +599,7 @@ class TestAutoEscape(object):
"""
tmpl = env.from_string(tmplsource)
assert tmpl.render(val=True).split()[0] == "Markup"
- assert tmpl.render(val=False).split()[0] == text_type.__name__
+ assert tmpl.render(val=False).split()[0] == "str"
# looking at the source we should see <testing> there in raw
# (and then escaped as well)
@@ -628,7 +613,7 @@ class TestAutoEscape(object):
def test_overlay_scopes(self):
class MagicScopeExtension(Extension):
- tags = set(["overlay"])
+ tags = {"overlay"}
def parse(self, parser):
node = nodes.OverlayScope(lineno=next(parser.stream).lineno)
diff --git a/tests/test_features.py b/tests/test_features.py
index 34b6f20..4f36458 100644
--- a/tests/test_features.py
+++ b/tests/test_features.py
@@ -1,42 +1,14 @@
-import sys
-
import pytest
-from jinja2 import contextfilter
-from jinja2 import Environment
from jinja2 import Template
-from jinja2._compat import text_type
-@pytest.mark.skipif(sys.version_info < (3, 5), reason="Requires 3.5 or later")
+# Python < 3.7
def test_generator_stop():
- class X(object):
+ class X:
def __getattr__(self, name):
raise StopIteration()
t = Template("a{{ bad.bar() }}b")
with pytest.raises(RuntimeError):
t.render(bad=X())
-
-
-@pytest.mark.skipif(sys.version_info[0] > 2, reason="Feature only supported on 2.x")
-def test_ascii_str():
- @contextfilter
- def assert_func(context, value):
- assert type(value) is context["expected_type"]
-
- env = Environment()
- env.filters["assert"] = assert_func
-
- env.policies["compiler.ascii_str"] = False
- t = env.from_string('{{ "foo"|assert }}')
- t.render(expected_type=text_type)
-
- env.policies["compiler.ascii_str"] = True
- t = env.from_string('{{ "foo"|assert }}')
- t.render(expected_type=str)
-
- for val in True, False:
- env.policies["compiler.ascii_str"] = val
- t = env.from_string(u'{{ "\N{SNOWMAN}"|assert }}')
- t.render(expected_type=text_type)
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 388c346..8087a24 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
import random
from collections import namedtuple
@@ -8,30 +7,26 @@ 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
-@implements_to_string
-class Magic(object):
+class Magic:
def __init__(self, value):
self.value = value
def __str__(self):
- return text_type(self.value)
+ return str(self.value)
-@implements_to_string
-class Magic2(object):
+class Magic2:
def __init__(self, value1, value2):
self.value1 = value1
self.value2 = value2
def __str__(self):
- return u"(%s,%s)" % (text_type(self.value1), text_type(self.value2))
+ return f"({self.value1},{self.value2})"
-class TestFilter(object):
+class TestFilter:
def test_filter_calling(self, env):
rv = env.call_filter("sum", [1, 2, 3])
assert rv == 6
@@ -61,7 +56,7 @@ class TestFilter(object):
),
)
def test_dictsort(self, env, args, expect):
- t = env.from_string("{{{{ foo|dictsort({args}) }}}}".format(args=args))
+ t = env.from_string(f"{{{{ foo|dictsort({args}) }}}}")
out = t.render(foo={"aa": 0, "b": 1, "c": 2, "AB": 3})
assert out == expect
@@ -144,18 +139,18 @@ class TestFilter(object):
assert out == "0"
@pytest.mark.parametrize(
- ("value", "expect"), (("42", "42.0"), ("abc", "0.0"), ("32.32", "32.32"),)
+ ("value", "expect"), (("42", "42.0"), ("abc", "0.0"), ("32.32", "32.32"))
)
def test_float(self, env, value, expect):
- t = env.from_string("{{ '%s'|float }}" % value)
- assert t.render() == expect
+ t = env.from_string("{{ value|float }}")
+ assert t.render(value=value) == expect
def test_float_default(self, env):
t = env.from_string("{{ value|float(default=1.0) }}")
assert t.render(value="abc") == "1.0"
def test_format(self, env):
- tmpl = env.from_string("""{{ "%s|%s"|format("a", "b") }}""")
+ tmpl = env.from_string("{{ '%s|%s'|format('a', 'b') }}")
out = tmpl.render()
assert out == "a|b"
@@ -198,23 +193,23 @@ class TestFilter(object):
),
)
def test_int(self, env, value, expect):
- t = env.from_string("{{ '%s'|int }}" % value)
- assert t.render() == expect
+ t = env.from_string("{{ value|int }}")
+ assert t.render(value=value) == expect
@pytest.mark.parametrize(
("value", "base", "expect"),
(("0x4d32", 16, "19762"), ("011", 8, "9"), ("0x33Z", 16, "0"),),
)
def test_int_base(self, env, value, base, expect):
- t = env.from_string("{{ '%s'|int(base=%d) }}" % (value, base))
- assert t.render() == expect
+ t = env.from_string("{{ value|int(base=base) }}")
+ assert t.render(value=value, base=base) == expect
def test_int_default(self, env):
t = env.from_string("{{ value|int(default=1) }}")
assert t.render(value="abc") == "1"
def test_int_special_method(self, env):
- class IntIsh(object):
+ class IntIsh:
def __int__(self):
return 42
@@ -282,7 +277,7 @@ class TestFilter(object):
def test_string(self, env):
x = [1, 2, 3, 4, 5]
tmpl = env.from_string("""{{ obj|string }}""")
- assert tmpl.render(obj=x) == text_type(x)
+ assert tmpl.render(obj=x) == str(x)
def test_title(self, env):
tmpl = env.from_string("""{{ "foo bar"|title }}""")
@@ -323,20 +318,19 @@ class TestFilter(object):
"{{ smalldata|truncate(15) }}"
)
out = tmpl.render(data="foobar baz bar" * 1000, smalldata="foobar baz bar")
- msg = "Current output: %s" % out
- assert out == "foobar baz b>>>|foobar baz>>>|foobar baz bar", msg
+ assert out == "foobar baz b>>>|foobar baz>>>|foobar baz bar"
def test_truncate_very_short(self, env):
tmpl = env.from_string(
'{{ "foo bar baz"|truncate(9) }}|{{ "foo bar baz"|truncate(9, true) }}'
)
out = tmpl.render()
- assert out == "foo bar baz|foo bar baz", out
+ assert out == "foo bar baz|foo bar baz"
def test_truncate_end_length(self, env):
tmpl = env.from_string('{{ "Joel is a slug"|truncate(7, true) }}')
out = tmpl.render()
- assert out == "Joel...", "Current output: %s" % out
+ assert out == "Joel..."
def test_upper(self, env):
tmpl = env.from_string('{{ "foo"|upper }}')
@@ -590,7 +584,7 @@ class TestFilter(object):
def test_forceescape(self, env):
tmpl = env.from_string("{{ x|forceescape }}")
- assert tmpl.render(x=Markup("<div />")) == u"&lt;div /&gt;"
+ assert tmpl.render(x=Markup("<div />")) == "&lt;div /&gt;"
def test_safe(self, env):
env = Environment(autoescape=True)
@@ -603,10 +597,10 @@ class TestFilter(object):
("value", "expect"),
[
("Hello, world!", "Hello%2C%20world%21"),
- (u"Hello, world\u203d", "Hello%2C%20world%E2%80%BD"),
+ ("Hello, world\u203d", "Hello%2C%20world%E2%80%BD"),
({"f": 1}, "f=1"),
([("f", 1), ("z", 2)], "f=1&amp;z=2"),
- ({u"\u203d": 1}, "%E2%80%BD=1"),
+ ({"\u203d": 1}, "%E2%80%BD=1"),
({0: 1}, "0=1"),
([("a b/c", "a b/c")], "a+b%2Fc=a+b%2Fc"),
("a b/c", "a%20b/c"),
diff --git a/tests/test_idtracking.py b/tests/test_idtracking.py
index 4c79ee6..8a88467 100644
--- a/tests/test_idtracking.py
+++ b/tests/test_idtracking.py
@@ -38,7 +38,7 @@ def test_basics():
def test_complex():
title_block = nodes.Block(
- "title", [nodes.Output([nodes.TemplateData(u"Page Title")])], False
+ "title", [nodes.Output([nodes.TemplateData("Page Title")])], False
)
render_title_macro = nodes.Macro(
@@ -48,11 +48,11 @@ def test_complex():
[
nodes.Output(
[
- nodes.TemplateData(u'\n <div class="title">\n <h1>'),
+ nodes.TemplateData('\n <div class="title">\n <h1>'),
nodes.Name("title", "load"),
- nodes.TemplateData(u"</h1>\n <p>"),
+ nodes.TemplateData("</h1>\n <p>"),
nodes.Name("subtitle", "load"),
- nodes.TemplateData(u"</p>\n "),
+ nodes.TemplateData("</p>\n "),
]
),
nodes.Assign(
@@ -60,9 +60,9 @@ def test_complex():
),
nodes.Output(
[
- nodes.TemplateData(u"\n <p>"),
+ nodes.TemplateData("\n <p>"),
nodes.Name("subtitle", "load"),
- nodes.TemplateData(u"</p>\n </div>\n"),
+ nodes.TemplateData("</p>\n </div>\n"),
nodes.If(
nodes.Name("something", "load"),
[
@@ -104,13 +104,13 @@ def test_complex():
[
nodes.Output(
[
- nodes.TemplateData(u"\n <li>"),
+ nodes.TemplateData("\n <li>"),
nodes.Name("item", "load"),
- nodes.TemplateData(u"</li>\n <span>"),
+ nodes.TemplateData("</li>\n <span>"),
]
),
nodes.Include(nodes.Const("helper.html"), True, False),
- nodes.Output([nodes.TemplateData(u"</span>\n ")]),
+ nodes.Output([nodes.TemplateData("</span>\n ")]),
],
[],
None,
@@ -122,7 +122,7 @@ def test_complex():
[
nodes.Output(
[
- nodes.TemplateData(u"\n "),
+ nodes.TemplateData("\n "),
nodes.Call(
nodes.Name("render_title", "load"),
[nodes.Name("item", "load")],
@@ -130,11 +130,11 @@ def test_complex():
None,
None,
),
- nodes.TemplateData(u"\n <ul>\n "),
+ nodes.TemplateData("\n <ul>\n "),
]
),
for_loop,
- nodes.Output([nodes.TemplateData(u"\n </ul>\n")]),
+ nodes.Output([nodes.TemplateData("\n </ul>\n")]),
],
False,
)
@@ -155,7 +155,7 @@ def test_complex():
assert tmpl_sym.loads == {
"l_0_render_title": ("undefined", None),
}
- assert tmpl_sym.stores == set(["render_title"])
+ assert tmpl_sym.stores == {"render_title"}
assert tmpl_sym.dump_stores() == {
"render_title": "l_0_render_title",
}
@@ -173,7 +173,7 @@ def test_complex():
"l_1_title": ("param", None),
"l_1_title_upper": ("resolve", "title_upper"),
}
- assert macro_sym.stores == set(["title", "title_upper", "subtitle"])
+ assert macro_sym.stores == {"title", "title_upper", "subtitle"}
assert macro_sym.find_ref("render_title") == "l_0_render_title"
assert macro_sym.dump_stores() == {
"title": "l_1_title",
@@ -193,7 +193,7 @@ def test_complex():
"l_0_seq": ("resolve", "seq"),
"l_0_render_title": ("resolve", "render_title"),
}
- assert body_sym.stores == set([])
+ assert body_sym.stores == set()
for_sym = symbols_for_node(for_loop, body_sym)
assert for_sym.refs == {
@@ -202,7 +202,7 @@ def test_complex():
assert for_sym.loads == {
"l_1_item": ("param", None),
}
- assert for_sym.stores == set(["item"])
+ assert for_sym.stores == {"item"}
assert for_sym.dump_stores() == {
"item": "l_1_item",
}
@@ -222,7 +222,7 @@ def test_if_branching_stores():
sym = symbols_for_node(tmpl)
assert sym.refs == {"variable": "l_0_variable", "expression": "l_0_expression"}
- assert sym.stores == set(["variable"])
+ assert sym.stores == {"variable"}
assert sym.loads == {
"l_0_variable": ("resolve", "variable"),
"l_0_expression": ("resolve", "expression"),
@@ -247,7 +247,7 @@ def test_if_branching_stores_undefined():
sym = symbols_for_node(tmpl)
assert sym.refs == {"variable": "l_0_variable", "expression": "l_0_expression"}
- assert sym.stores == set(["variable"])
+ assert sym.stores == {"variable"}
assert sym.loads == {
"l_0_variable": ("undefined", None),
"l_0_expression": ("resolve", "expression"),
@@ -281,7 +281,7 @@ def test_if_branching_multi_scope():
tmpl_sym = symbols_for_node(tmpl)
for_sym = symbols_for_node(for_loop, tmpl_sym)
- assert for_sym.stores == set(["item", "x"])
+ assert for_sym.stores == {"item", "x"}
assert for_sym.loads == {
"l_1_x": ("alias", "l_0_x"),
"l_1_item": ("param", None),
diff --git a/tests/test_imports.py b/tests/test_imports.py
index fad2eda..7a2bd94 100644
--- a/tests/test_imports.py
+++ b/tests/test_imports.py
@@ -1,11 +1,11 @@
-# -*- coding: utf-8 -*-
import pytest
-from jinja2 import DictLoader
-from jinja2 import Environment
+from jinja2.environment import Environment
from jinja2.exceptions import TemplateNotFound
from jinja2.exceptions import TemplatesNotFound
from jinja2.exceptions import TemplateSyntaxError
+from jinja2.exceptions import UndefinedError
+from jinja2.loaders import DictLoader
@pytest.fixture
@@ -23,7 +23,7 @@ def test_env():
return env
-class TestImports(object):
+class TestImports:
def test_context_imports(self, test_env):
t = test_env.from_string('{% import "module" as m %}{{ m.test() }}')
assert t.render(foo=42) == "[|23]"
@@ -92,8 +92,14 @@ class TestImports(object):
assert m.variable == 42
assert not hasattr(m, "notthere")
+ def test_not_exported(self, test_env):
+ t = test_env.from_string("{% from 'module' import nothing %}{{ nothing() }}")
-class TestIncludes(object):
+ with pytest.raises(UndefinedError, match="does not export the requested name"):
+ t.render()
+
+
+class TestIncludes:
def test_context_include(self, test_env):
t = test_env.from_string('{% include "header" %}')
assert t.render(foo=42) == "[42|23]"
diff --git a/tests/test_inheritance.py b/tests/test_inheritance.py
index e513d2e..b95c47d 100644
--- a/tests/test_inheritance.py
+++ b/tests/test_inheritance.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
import pytest
from jinja2 import DictLoader
@@ -74,7 +73,7 @@ def env():
)
-class TestInheritance(object):
+class TestInheritance:
def test_layout(self, env):
tmpl = env.get_template("layout")
assert tmpl.render() == (
@@ -157,7 +156,7 @@ class TestInheritance(object):
)
tmpl = env.get_template("child")
for m in range(1, 3):
- assert tmpl.render(master="master%d" % m) == "MASTER%dCHILD" % m
+ assert tmpl.render(master=f"master{m}") == f"MASTER{m}CHILD"
def test_multi_inheritance(self, env):
env = Environment(
@@ -232,7 +231,7 @@ class TestInheritance(object):
assert rv == ["43", "44", "45"]
-class TestBugFix(object):
+class TestBugFix:
def test_fixed_macro_scoping_bug(self, env):
assert (
Environment(
@@ -274,7 +273,7 @@ class TestBugFix(object):
.get_template("test.html")
.render()
.split()
- == [u"outer_box", u"my_macro"]
+ == ["outer_box", "my_macro"]
)
def test_double_extends(self, env):
diff --git a/tests/test_lexnparse.py b/tests/test_lexnparse.py
index 3355791..6057416 100644
--- a/tests/test_lexnparse.py
+++ b/tests/test_lexnparse.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
import pytest
from jinja2 import Environment
@@ -6,9 +5,6 @@ from jinja2 import nodes
from jinja2 import Template
from jinja2 import TemplateSyntaxError
from jinja2 import UndefinedError
-from jinja2._compat import iteritems
-from jinja2._compat import PY2
-from jinja2._compat import text_type
from jinja2.lexer import Token
from jinja2.lexer import TOKEN_BLOCK_BEGIN
from jinja2.lexer import TOKEN_BLOCK_END
@@ -16,18 +12,7 @@ from jinja2.lexer import TOKEN_EOF
from jinja2.lexer import TokenStream
-# how does a string look like in jinja syntax?
-if PY2:
-
- def jinja_string_repr(string):
- return repr(string)[1:]
-
-
-else:
- jinja_string_repr = repr
-
-
-class TestTokenStream(object):
+class TestTokenStream:
test_tokens = [
Token(1, TOKEN_BLOCK_BEGIN, ""),
Token(2, TOKEN_BLOCK_END, ""),
@@ -55,7 +40,7 @@ class TestTokenStream(object):
]
-class TestLexer(object):
+class TestLexer:
def test_raw1(self, env):
tmpl = env.from_string(
"{% raw %}foo{% endraw %}|"
@@ -106,24 +91,24 @@ class TestLexer(object):
)
def test_string_escapes(self, env):
- for char in u"\0", u"\u2668", u"\xe4", u"\t", u"\r", u"\n":
- tmpl = env.from_string("{{ %s }}" % jinja_string_repr(char))
+ for char in "\0", "\u2668", "\xe4", "\t", "\r", "\n":
+ tmpl = env.from_string(f"{{{{ {char!r} }}}}")
assert tmpl.render() == char
- assert env.from_string('{{ "\N{HOT SPRINGS}" }}').render() == u"\u2668"
+ assert env.from_string('{{ "\N{HOT SPRINGS}" }}').render() == "\u2668"
def test_bytefallback(self, env):
from pprint import pformat
- tmpl = env.from_string(u"""{{ 'foo'|pprint }}|{{ 'bär'|pprint }}""")
- assert tmpl.render() == pformat("foo") + "|" + pformat(u"bär")
+ tmpl = env.from_string("""{{ 'foo'|pprint }}|{{ 'bär'|pprint }}""")
+ assert tmpl.render() == pformat("foo") + "|" + pformat("bär")
def test_operators(self, env):
from jinja2.lexer import operators
- for test, expect in iteritems(operators):
+ for test, expect in operators.items():
if test in "([{}])":
continue
- stream = env.lexer.tokenize("{{ %s }}" % test)
+ stream = env.lexer.tokenize(f"{{{{ {test} }}}}")
next(stream)
assert stream.current.type == expect
@@ -149,30 +134,30 @@ class TestLexer(object):
assert result == expect, (keep, template, result, expect)
@pytest.mark.parametrize(
- "name,valid2,valid3",
- (
- (u"foo", True, True),
- (u"föö", False, True),
- (u"き", False, True),
- (u"_", True, True),
- (u"1a", False, False), # invalid ascii start
- (u"a-", False, False), # invalid ascii continue
- (u"🐍", False, False), # invalid unicode start
- (u"a🐍", False, False), # invalid unicode continue
+ ("name", "valid"),
+ [
+ ("foo", True),
+ ("föö", True),
+ ("き", True),
+ ("_", True),
+ ("1a", False), # invalid ascii start
+ ("a-", False), # invalid ascii continue
+ ("\U0001f40da", False), # invalid unicode start
+ ("a🐍\U0001f40d", False), # invalid unicode continue
# start characters not matched by \w
- (u"\u1885", False, True),
- (u"\u1886", False, True),
- (u"\u2118", False, True),
- (u"\u212e", False, True),
+ ("\u1885", True),
+ ("\u1886", True),
+ ("\u2118", True),
+ ("\u212e", True),
# continue character not matched by \w
- (u"\xb7", False, False),
- (u"a\xb7", False, True),
- ),
+ ("\xb7", False),
+ ("a\xb7", True),
+ ],
)
- def test_name(self, env, name, valid2, valid3):
- t = u"{{ " + name + u" }}"
+ def test_name(self, env, name, valid):
+ t = "{{ " + name + " }}"
- if (valid2 and PY2) or (valid3 and not PY2):
+ if valid:
# valid for version being tested, shouldn't raise
env.from_string(t)
else:
@@ -197,7 +182,7 @@ class TestLexer(object):
break
-class TestParser(object):
+class TestParser:
def test_php_syntax(self, env):
env = Environment("<?", "?>", "<?=", "?>", "<!--", "-->")
tmpl = env.from_string(
@@ -330,7 +315,7 @@ and bar comment #}
assert_error("{% unknown_tag %}", "Encountered unknown tag 'unknown_tag'.")
-class TestSyntax(object):
+class TestSyntax:
def test_call(self, env):
env = Environment()
env.globals["foo"] = lambda a, b, c, e, g: a + b + c + e + g
@@ -381,7 +366,7 @@ class TestSyntax(object):
],
)
def test_compare(self, env, a, op, b):
- t = env.from_string("{{ %d %s %d }}" % (a, op, b))
+ t = env.from_string(f"{{{{ {a} {op} {b} }}}}")
assert t.render() == "True"
def test_compare_parens(self, env):
@@ -409,7 +394,7 @@ class TestSyntax(object):
@pytest.mark.parametrize("value", ("[]", "{}", "()"))
def test_collection_literal(self, env, value):
- t = env.from_string("{{ %s }}" % value)
+ t = env.from_string(f"{{{{ {value} }}}}")
assert t.render() == value
@pytest.mark.parametrize(
@@ -430,7 +415,7 @@ class TestSyntax(object):
),
)
def test_numeric_literal(self, env, value, expect):
- t = env.from_string("{{ %s }}" % value)
+ t = env.from_string(f"{{{{ {value} }}}}")
assert t.render() == expect
def test_bool(self, env):
@@ -484,11 +469,10 @@ class TestSyntax(object):
]
for should_fail, sig in tests:
if should_fail:
- pytest.raises(
- TemplateSyntaxError, env.from_string, "{{ foo(%s) }}" % sig
- )
+ with pytest.raises(TemplateSyntaxError):
+ env.from_string(f"{{{{ foo({sig}) }}}}")
else:
- env.from_string("foo(%s)" % sig)
+ env.from_string(f"foo({sig})")
def test_tuple_expr(self, env):
for tmpl in [
@@ -515,11 +499,11 @@ class TestSyntax(object):
def test_constant_casing(self, env):
for const in True, False, None:
+ const = str(const)
tmpl = env.from_string(
- "{{ %s }}|{{ %s }}|{{ %s }}"
- % (str(const), str(const).lower(), str(const).upper())
+ f"{{{{ {const} }}}}|{{{{ {const.lower()} }}}}|{{{{ {const.upper()} }}}}"
)
- assert tmpl.render() == "%s|%s|" % (const, const)
+ assert tmpl.render() == f"{const}|{const}|"
def test_test_chaining(self, env):
pytest.raises(
@@ -538,15 +522,15 @@ class TestSyntax(object):
def test_operator_precedence(self, env):
tmpl = env.from_string("""{{ 2 * 3 + 4 % 2 + 1 - 2 }}""")
- assert tmpl.render() == text_type(2 * 3 + 4 % 2 + 1 - 2)
+ assert tmpl.render() == "5"
def test_implicit_subscribed_tuple(self, env):
- class Foo(object):
+ class Foo:
def __getitem__(self, x):
return x
t = env.from_string("{{ foo[1, 2] }}")
- assert t.render(foo=Foo()) == u"(1, 2)"
+ assert t.render(foo=Foo()) == "(1, 2)"
def test_raw2(self, env):
tmpl = env.from_string("{% raw %}{{ FOO }} and {% BAR %}{% endraw %}")
@@ -585,7 +569,7 @@ class TestSyntax(object):
assert tmpl.render(foo={"bar": 42}) == "42"
-class TestLstripBlocks(object):
+class TestLstripBlocks:
def test_lstrip(self, env):
env = Environment(lstrip_blocks=True, trim_blocks=False)
tmpl = env.from_string(""" {% if True %}\n {% endif %}""")
@@ -705,7 +689,7 @@ hello
${item} ## the rest of the stuff
<% endfor %>"""
)
- assert tmpl.render(seq=range(5)) == "".join("%s\n" % x for x in range(5))
+ assert tmpl.render(seq=range(5)) == "".join(f"{x}\n" for x in range(5))
def test_lstrip_angle_bracket_compact(self, env):
env = Environment(
@@ -727,7 +711,7 @@ ${item} ## the rest of the stuff
${item} ## the rest of the stuff
<%endfor%>"""
)
- assert tmpl.render(seq=range(5)) == "".join("%s\n" % x for x in range(5))
+ assert tmpl.render(seq=range(5)) == "".join(f"{x}\n" for x in range(5))
def test_php_syntax_with_manual(self, env):
env = Environment(
@@ -753,9 +737,7 @@ ${item} ## the rest of the stuff
<?= item ?>
<? endfor ?>"""
)
- assert tmpl.render(seq=range(5)) == "".join(
- " %s\n" % x for x in range(5)
- )
+ assert tmpl.render(seq=range(5)) == "".join(f" {x}\n" for x in range(5))
def test_php_syntax_compact(self, env):
env = Environment(
@@ -768,19 +750,12 @@ ${item} ## the rest of the stuff
<?=item?>
<?endfor?>"""
)
- assert tmpl.render(seq=range(5)) == "".join(
- " %s\n" % x for x in range(5)
- )
+ assert tmpl.render(seq=range(5)) == "".join(f" {x}\n" for x in range(5))
def test_erb_syntax(self, env):
env = Environment(
"<%", "%>", "<%=", "%>", "<%#", "%>", lstrip_blocks=True, trim_blocks=True
)
- # env.from_string('')
- # for n,r in env.lexer.rules.iteritems():
- # print n
- # print env.lexer.rules['root'][0][0].pattern
- # print "'%s'" % tmpl.render(seq=range(5))
tmpl = env.from_string(
"""\
<%# I'm a comment, I'm not interesting %>
@@ -789,7 +764,7 @@ ${item} ## the rest of the stuff
<% endfor %>
"""
)
- assert tmpl.render(seq=range(5)) == "".join(" %s\n" % x for x in range(5))
+ assert tmpl.render(seq=range(5)) == "".join(f" {x}\n" for x in range(5))
def test_erb_syntax_with_manual(self, env):
env = Environment(
diff --git a/tests/test_loader.py b/tests/test_loader.py
index f10f756..8ca1289 100644
--- a/tests/test_loader.py
+++ b/tests/test_loader.py
@@ -1,5 +1,8 @@
-# -*- coding: utf-8 -*-
+import importlib.abc
+import importlib.machinery
+import importlib.util
import os
+import platform
import shutil
import sys
import tempfile
@@ -10,13 +13,12 @@ import pytest
from jinja2 import Environment
from jinja2 import loaders
-from jinja2._compat import PY2
-from jinja2._compat import PYPY
+from jinja2 import PackageLoader
from jinja2.exceptions import TemplateNotFound
from jinja2.loaders import split_template_path
-class TestLoaders(object):
+class TestLoaders:
def test_dict_loader(self, dict_loader):
env = Environment(loader=dict_loader)
tmpl = env.get_template("justdict.html")
@@ -66,7 +68,7 @@ class TestLoaders(object):
class TestLoader(loaders.BaseLoader):
def get_source(self, environment, template):
- return u"foo", None, lambda: not changed
+ return "foo", None, lambda: not changed
env = Environment(loader=TestLoader(), cache_size=-1)
tmpl = env.get_template("template")
@@ -115,7 +117,7 @@ class TestLoaders(object):
pytest.raises(TemplateNotFound, split_template_path, "../foo")
-class TestFileSystemLoader(object):
+class TestFileSystemLoader:
searchpath = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "res", "templates"
)
@@ -134,25 +136,19 @@ class TestFileSystemLoader(object):
env = Environment(loader=filesystem_loader)
self._test_common(env)
- @pytest.mark.skipif(PY2, reason="pathlib is not available in Python 2")
def test_searchpath_as_pathlib(self):
import pathlib
searchpath = pathlib.Path(self.searchpath)
-
filesystem_loader = loaders.FileSystemLoader(searchpath)
-
env = Environment(loader=filesystem_loader)
self._test_common(env)
- @pytest.mark.skipif(PY2, reason="pathlib is not available in Python 2")
def test_searchpath_as_list_including_pathlib(self):
import pathlib
searchpath = pathlib.Path(self.searchpath)
-
filesystem_loader = loaders.FileSystemLoader(["/tmp/templates", searchpath])
-
env = Environment(loader=filesystem_loader)
self._test_common(env)
@@ -171,8 +167,8 @@ class TestFileSystemLoader(object):
@pytest.mark.parametrize(
("encoding", "expect"),
[
- ("utf-8", u"文字化け"),
- ("iso-8859-1", u"æ\x96\x87\xe5\xad\x97\xe5\x8c\x96\xe3\x81\x91"),
+ ("utf-8", "文字化け"),
+ ("iso-8859-1", "æ\x96\x87\xe5\xad\x97\xe5\x8c\x96\xe3\x81\x91"),
],
)
def test_uses_specified_encoding(self, encoding, expect):
@@ -182,10 +178,10 @@ class TestFileSystemLoader(object):
assert t.render() == expect
-class TestModuleLoader(object):
+class TestModuleLoader:
archive = None
- def compile_down(self, prefix_loader, zip="deflated", py_compile=False):
+ def compile_down(self, prefix_loader, zip="deflated"):
log = []
self.reg_env = Environment(loader=prefix_loader)
if zip is not None:
@@ -193,9 +189,7 @@ class TestModuleLoader(object):
os.close(fd)
else:
self.archive = tempfile.mkdtemp()
- self.reg_env.compile_templates(
- self.archive, zip=zip, log_function=log.append, py_compile=py_compile
- )
+ self.reg_env.compile_templates(self.archive, zip=zip, log_function=log.append)
self.mod_env = Environment(loader=loaders.ModuleLoader(self.archive))
return "".join(log)
@@ -261,17 +255,6 @@ class TestModuleLoader(object):
assert name not in sys.modules
- # This test only makes sense on non-pypy python 2
- @pytest.mark.skipif(
- not (PY2 and not PYPY), reason="This test only makes sense on non-pypy python 2"
- )
- def test_byte_compilation(self, prefix_loader):
- log = self.compile_down(prefix_loader, py_compile=True)
- assert 'Byte-compiled "a/test.html"' in log
- self.mod_env.get_template("a/test.html")
- mod = self.mod_env.loader.module.tmpl_3c4ddf650c1a73df961a6d3d2ce2752f1b8fd490
- assert mod.__file__.endswith(".pyc")
-
def test_choice_loader(self, prefix_loader):
self.compile_down(prefix_loader)
self.mod_env.loader = loaders.ChoiceLoader(
@@ -295,7 +278,6 @@ class TestModuleLoader(object):
tmpl2 = self.mod_env.get_template("DICT/test.html")
assert tmpl2.render() == "DICT_TEMPLATE"
- @pytest.mark.skipif(PY2, reason="pathlib is not available in Python 2")
def test_path_as_pathlib(self, prefix_loader):
self.compile_down(prefix_loader)
@@ -308,7 +290,6 @@ class TestModuleLoader(object):
self._test_common()
- @pytest.mark.skipif(PY2, reason="pathlib is not available in Python 2")
def test_supports_pathlib_in_list_of_paths(self, prefix_loader):
self.compile_down(prefix_loader)
@@ -320,3 +301,83 @@ 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(
+ platform.python_implementation() == "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"]
+
+
+def test_pep_451_import_hook():
+ class ImportHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
+ def find_spec(self, name, path=None, target=None):
+ if name != "res":
+ return None
+
+ spec = importlib.machinery.PathFinder.find_spec(name)
+ return importlib.util.spec_from_file_location(
+ name,
+ spec.origin,
+ loader=self,
+ submodule_search_locations=spec.submodule_search_locations,
+ )
+
+ def create_module(self, spec):
+ return None # default behaviour is fine
+
+ def exec_module(self, module):
+ return None # we need this to satisfy the interface, it's wrong
+
+ # ensure we restore `sys.meta_path` after putting in our loader
+ before = sys.meta_path[:]
+
+ try:
+ sys.meta_path.insert(0, ImportHook())
+ package_loader = PackageLoader("res")
+ assert "test.html" in package_loader.list_templates()
+ finally:
+ sys.meta_path[:] = before
diff --git a/tests/test_nativetypes.py b/tests/test_nativetypes.py
index 77d378d..947168c 100644
--- a/tests/test_nativetypes.py
+++ b/tests/test_nativetypes.py
@@ -1,6 +1,5 @@
import pytest
-from jinja2._compat import text_type
from jinja2.exceptions import UndefinedError
from jinja2.nativetypes import NativeEnvironment
from jinja2.nativetypes import NativeTemplate
@@ -53,7 +52,7 @@ def test_multi_expression_add(env):
def test_loops(env):
t = env.from_string("{% for x in value %}{{ x }}{% endfor %}")
result = t.render(value=["a", "b", "c", "d"])
- assert isinstance(result, text_type)
+ assert isinstance(result, str)
assert result == "abcd"
@@ -111,7 +110,7 @@ def test_constant_dunder_to_string(env):
def test_string_literal_var(env):
t = env.from_string("[{{ 'all' }}]")
result = t.render()
- assert isinstance(result, text_type)
+ assert isinstance(result, str)
assert result == "[all]"
diff --git a/tests/test_regression.py b/tests/test_regression.py
index c5a0d68..65ace87 100644
--- a/tests/test_regression.py
+++ b/tests/test_regression.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-import sys
-
import pytest
from jinja2 import DictLoader
@@ -10,10 +7,9 @@ from jinja2 import Template
from jinja2 import TemplateAssertionError
from jinja2 import TemplateNotFound
from jinja2 import TemplateSyntaxError
-from jinja2._compat import text_type
-class TestCorner(object):
+class TestCorner:
def test_assigned_scoping(self, env):
t = env.from_string(
"""
@@ -84,7 +80,7 @@ class TestCorner(object):
assert t.render(wrapper=23) == "[1][2][3][4]23"
-class TestBug(object):
+class TestBug:
def test_keyword_folding(self, env):
env = Environment()
env.filters["testing"] = lambda value, some: value + some
@@ -132,7 +128,7 @@ class TestBug(object):
"""
)
- assert tmpl.render().split() == [text_type(x) for x in range(1, 11)] * 5
+ assert tmpl.render().split() == [str(x) for x in range(1, 11)] * 5
def test_weird_inline_comment(self, env):
env = Environment(line_statement_prefix="%")
@@ -193,7 +189,7 @@ class TestBug(object):
"""
)
rv = t.render(foo=[1]).strip()
- assert rv == u"1"
+ assert rv == "1"
def test_call_with_args(self, env):
t = Template(
@@ -227,13 +223,13 @@ class TestBug(object):
]
).splitlines()
] == [
- u"<ul><li><p>apo</p><dl>",
- u"<dl>Realname</dl>",
- u"<dd>something else</dd>",
- u"<dl>Description</dl>",
- u"<dd>test</dd>",
- u"</dl>",
- u"</li></ul>",
+ "<ul><li><p>apo</p><dl>",
+ "<dl>Realname</dl>",
+ "<dd>something else</dd>",
+ "<dl>Description</dl>",
+ "<dd>test</dd>",
+ "</dl>",
+ "</li></ul>",
]
def test_empty_if_condition_fails(self, env):
@@ -295,7 +291,7 @@ class TestBug(object):
def test_contextfunction_callable_classes(self, env):
from jinja2.utils import contextfunction
- class CallableClass(object):
+ class CallableClass:
@contextfunction
def __call__(self, ctx):
return ctx.resolve("hello")
@@ -306,13 +302,6 @@ class TestBug(object):
assert output == expected
- @pytest.mark.skipif(sys.version_info[0] > 2, reason="This only works on 2.x")
- def test_old_style_attribute(self, env):
- class Foo:
- x = 42
-
- assert env.getitem(Foo(), "x") == 42
-
def test_block_set_with_extends(self):
env = Environment(
loader=DictLoader({"main": "{% block body %}[{{ x }}]{% endblock %}"})
@@ -603,7 +592,7 @@ class TestBug(object):
def resolve(self, name):
if name == "foo":
return 42
- return super(MyContext, self).resolve(name)
+ return super().resolve(name)
x = MyContext(env, parent={"bar": 23}, name="foo", blocks={})
assert x._legacy_resolve_mode
diff --git a/tests/test_runtime.py b/tests/test_runtime.py
index 5e4686c..db95899 100644
--- a/tests/test_runtime.py
+++ b/tests/test_runtime.py
@@ -62,7 +62,7 @@ def test_mock_not_contextfunction():
as a ``contextfunction``.
"""
- class Calc(object):
+ class Calc:
def __getattr__(self, item):
return object()
diff --git a/tests/test_security.py b/tests/test_security.py
index 7e8974c..44ac47a 100644
--- a/tests/test_security.py
+++ b/tests/test_security.py
@@ -1,10 +1,7 @@
-# -*- coding: utf-8 -*-
import pytest
from jinja2 import Environment
from jinja2 import escape
-from jinja2 import Markup
-from jinja2._compat import text_type
from jinja2.exceptions import SecurityError
from jinja2.exceptions import TemplateRuntimeError
from jinja2.exceptions import TemplateSyntaxError
@@ -14,7 +11,7 @@ from jinja2.sandbox import SandboxedEnvironment
from jinja2.sandbox import unsafe
-class PrivateStuff(object):
+class PrivateStuff:
def bar(self):
return 23
@@ -26,7 +23,7 @@ class PrivateStuff(object):
return "PrivateStuff"
-class PublicStuff(object):
+class PublicStuff:
def bar(self):
return 23
@@ -37,7 +34,7 @@ class PublicStuff(object):
return "PublicStuff"
-class TestSandbox(object):
+class TestSandbox:
def test_unsafe(self, env):
env = SandboxedEnvironment()
pytest.raises(
@@ -76,44 +73,6 @@ class TestSandbox(object):
"{% for foo, bar.baz in seq %}...{% endfor %}",
)
- def test_markup_operations(self, env):
- # adding two strings should escape the unsafe one
- unsafe = '<script type="application/x-some-script">alert("foo");</script>'
- safe = Markup("<em>username</em>")
- assert unsafe + safe == text_type(escape(unsafe)) + text_type(safe)
-
- # string interpolations are safe to use too
- assert Markup("<em>%s</em>") % "<bad user>" == "<em>&lt;bad user&gt;</em>"
- assert (
- Markup("<em>%(username)s</em>") % {"username": "<bad user>"}
- == "<em>&lt;bad user&gt;</em>"
- )
-
- # an escaped object is markup too
- assert type(Markup("foo") + "bar") is Markup
-
- # and it implements __html__ by returning itself
- x = Markup("foo")
- assert x.__html__() is x
-
- # it also knows how to treat __html__ objects
- class Foo(object):
- def __html__(self):
- return "<em>awesome</em>"
-
- def __unicode__(self):
- return "awesome"
-
- assert Markup(Foo()) == "<em>awesome</em>"
- assert (
- Markup("<strong>%s</strong>") % Foo() == "<strong><em>awesome</em></strong>"
- )
-
- # escaping and unescaping
- assert escape("\"<>&'") == "&#34;&lt;&gt;&amp;&#39;"
- assert Markup("<em>Foo &amp; Bar</em>").striptags() == "Foo & Bar"
- assert Markup("&lt;test&gt;").unescape() == "<test>"
-
def test_template_data(self, env):
env = Environment(autoescape=True)
t = env.from_string(
@@ -123,7 +82,7 @@ class TestSandbox(object):
)
escaped_out = "<p>Hello &lt;blink&gt;foo&lt;/blink&gt;!</p>"
assert t.render() == escaped_out
- assert text_type(t.module) == escaped_out
+ assert str(t.module) == escaped_out
assert escape(t.module) == escaped_out
assert t.module.say_hello("<blink>foo</blink>") == escaped_out
assert (
@@ -144,10 +103,10 @@ class TestSandbox(object):
for expr, ctx, rv in ("1 + 2", {}, "3"), ("a + 2", {"a": 2}, "4"):
env = SandboxedEnvironment()
env.binop_table["+"] = disable_op
- t = env.from_string("{{ %s }}" % expr)
+ t = env.from_string(f"{{{{ {expr} }}}}")
assert t.render(ctx) == rv
env.intercepted_binops = frozenset(["+"])
- t = env.from_string("{{ %s }}" % expr)
+ t = env.from_string(f"{{{{ {expr} }}}}")
with pytest.raises(TemplateRuntimeError):
t.render(ctx)
@@ -158,15 +117,15 @@ class TestSandbox(object):
for expr, ctx, rv in ("-1", {}, "-1"), ("-a", {"a": 2}, "-2"):
env = SandboxedEnvironment()
env.unop_table["-"] = disable_op
- t = env.from_string("{{ %s }}" % expr)
+ t = env.from_string(f"{{{{ {expr} }}}}")
assert t.render(ctx) == rv
env.intercepted_unops = frozenset(["-"])
- t = env.from_string("{{ %s }}" % expr)
+ t = env.from_string(f"{{{{ {expr} }}}}")
with pytest.raises(TemplateRuntimeError):
t.render(ctx)
-class TestStringFormat(object):
+class TestStringFormat:
def test_basic_format_safety(self):
env = SandboxedEnvironment()
t = env.from_string('{{ "a{0.__class__}b".format(42) }}')
@@ -188,10 +147,7 @@ class TestStringFormat(object):
assert t.render() == "a42b&lt;foo&gt;"
-@pytest.mark.skipif(
- not hasattr(str, "format_map"), reason="requires str.format_map method"
-)
-class TestStringFormatMap(object):
+class TestStringFormatMap:
def test_basic_format_safety(self):
env = SandboxedEnvironment()
t = env.from_string('{{ "a{x.__class__}b".format_map({"x":42}) }}')
diff --git a/tests/test_tests.py b/tests/test_tests.py
index b903e3b..d363653 100644
--- a/tests/test_tests.py
+++ b/tests/test_tests.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
import pytest
from jinja2 import Environment
@@ -9,7 +8,7 @@ class MyDict(dict):
pass
-class TestTestsCase(object):
+class TestTestsCase:
def test_defined(self, env):
tmpl = env.from_string("{{ missing is defined }}|{{ true is defined }}")
assert tmpl.render() == "False|True"
@@ -110,7 +109,7 @@ class TestTestsCase(object):
),
)
def test_types(self, env, op, expect):
- t = env.from_string("{{{{ {op} }}}}".format(op=op))
+ t = env.from_string(f"{{{{ {op} }}}}")
assert t.render(mydict=MyDict(), complex=complex(1, 2)) == str(expect)
def test_upper(self, env):
@@ -151,7 +150,7 @@ class TestTestsCase(object):
),
)
def test_compare_aliases(self, env, op, expect):
- t = env.from_string("{{{{ 2 is {op} }}}}".format(op=op))
+ t = env.from_string(f"{{{{ 2 is {op} }}}}")
assert t.render() == str(expect)
def test_sameas(self, env):
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b379a97..3e24166 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
import pickle
import random
from collections import deque
@@ -7,8 +6,6 @@ from copy import copy as shallow_copy
import pytest
from markupsafe import Markup
-from jinja2._compat import range_type
-from jinja2._compat import string_types
from jinja2.utils import consume
from jinja2.utils import generate_lorem_ipsum
from jinja2.utils import LRUCache
@@ -18,7 +15,7 @@ from jinja2.utils import select_autoescape
from jinja2.utils import urlize
-class TestLRUCache(object):
+class TestLRUCache:
def test_simple(self):
d = LRUCache(3)
d["a"] = 1
@@ -29,13 +26,6 @@ class TestLRUCache(object):
assert len(d) == 3
assert "a" in d and "c" in d and "d" in d and "b" not in d
- def test_itervalue_deprecated(self):
- cache = LRUCache(3)
- cache["a"] = 1
- cache["b"] = 2
- with pytest.deprecated_call():
- cache.itervalue()
-
def test_itervalues(self):
cache = LRUCache(3)
cache["b"] = 1
@@ -87,7 +77,7 @@ class TestLRUCache(object):
d["b"] = 2
d["c"] = 3
# Sort the strings - mapping is unordered
- assert sorted(repr(d)) == sorted(u"<LRUCache {'a': 1, 'b': 2, 'c': 3}>")
+ assert sorted(repr(d)) == sorted("<LRUCache {'a': 1, 'b': 2, 'c': 3}>")
def test_items(self):
"""Test various items, keys, values and iterators of LRUCache."""
@@ -118,9 +108,9 @@ class TestLRUCache(object):
assert len(d) == 2
-class TestHelpers(object):
+class TestHelpers:
def test_object_type_repr(self):
- class X(object):
+ class X:
pass
assert object_type_repr(42) == "int object"
@@ -146,7 +136,7 @@ class TestHelpers(object):
assert not func("FOO.TXT")
-class TestEscapeUrlizeTarget(object):
+class TestEscapeUrlizeTarget:
def test_escape_urlize_target(self):
url = "http://example.org"
target = "<script>"
@@ -157,39 +147,39 @@ class TestEscapeUrlizeTarget(object):
)
-class TestLoremIpsum(object):
+class TestLoremIpsum:
def test_lorem_ipsum_markup(self):
"""Test that output of lorem_ipsum is Markup by default."""
assert isinstance(generate_lorem_ipsum(), Markup)
def test_lorem_ipsum_html(self):
"""Test that output of lorem_ipsum is a string_type when not html."""
- assert isinstance(generate_lorem_ipsum(html=False), string_types)
+ assert isinstance(generate_lorem_ipsum(html=False), str)
def test_lorem_ipsum_n(self):
"""Test that the n (number of lines) works as expected."""
- assert generate_lorem_ipsum(n=0, html=False) == u""
- for n in range_type(1, 50):
+ assert generate_lorem_ipsum(n=0, html=False) == ""
+ for n in range(1, 50):
assert generate_lorem_ipsum(n=n, html=False).count("\n") == (n - 1) * 2
def test_lorem_ipsum_min(self):
"""Test that at least min words are in the output of each line"""
- for _ in range_type(5):
+ for _ in range(5):
m = random.randrange(20, 99)
- for _ in range_type(10):
+ for _ in range(10):
assert generate_lorem_ipsum(n=1, min=m, html=False).count(" ") >= m - 1
def test_lorem_ipsum_max(self):
"""Test that at least max words are in the output of each line"""
- for _ in range_type(5):
+ for _ in range(5):
m = random.randrange(21, 100)
- for _ in range_type(10):
+ for _ in range(10):
assert generate_lorem_ipsum(n=1, max=m, html=False).count(" ") < m - 1
def test_missing():
"""Test the repr of missing."""
- assert repr(missing) == u"missing"
+ assert repr(missing) == "missing"
def test_consume():
diff --git a/tox.ini b/tox.ini
index 679ebeb..736b6e7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
envlist =
- py{38,37,36,35,27,py3,py}
+ py{38,37,36,py3}
style
docs
skip_missing_interpreters = true