Merge branch '2.11.x'
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 @@ vmImage: ubuntu-latest python.version: '3.8' TOXENV: py - hasTestResults: 'true' strategy: matrix: @@ -16,22 +15,16 @@ 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 9b8b24e..57de4ae 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` + + Version 2.11.2 -------------- @@ -9,7 +22,7 @@ :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 @@ 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 @@ -------------------- 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 @@ .. 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 @@ .. 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 @@ 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 @@ .. 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ .. 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 @@ 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 @@ 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 @@ {% 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 @@ 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 @@ :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 @@ 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. -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. +It includes: -Prerequisites -------------- +- 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 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. +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 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`` - -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/ + $ pip install Jinja2 -MarkupSafe Dependency -~~~~~~~~~~~~~~~~~~~~~ +Dependencies +~~~~~~~~~~~~ -As of version 2.7 Jinja depends on the `MarkupSafe`_ module. If you install -Jinja via ``pip`` it will be installed automatically for you. +These will be installed automatically when installing Jinja. + +- `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. +Optional Dependencies +~~~~~~~~~~~~~~~~~~~~~ -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: +These distributions will not be installed automatically. ->>> from jinja2 import Template ->>> template = Template('Hello {{ name }}!') ->>> template.render(name='John Doe') -u'Hello John Doe!' +- `Babel`_ provides translation support in templates. -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. - -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 @@ 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 @@ 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 @@ {% 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 @@ {{ 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["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/ext/Vim/jinja.vim b/ext/Vim/jinja.vim index e2a5bbf..63f0d99 100644 --- a/ext/Vim/jinja.vim +++ b/ext/Vim/jinja.vim
@@ -82,9 +82,6 @@ " Jinja comments syn region jinjaComment matchgroup=jinjaCommentDelim start="{#" end="#}" containedin=ALLBUT,jinjaTagBlock,jinjaVarBlock,jinjaString,jinjaComment -" help support folding for some setups -setlocal commentstring={#%s#} -setlocal comments=s:{#,e:#} " Block start keywords. A bit tricker. We only highlight at the start of a " tag block and only if the name is not followed by a comma or equals sign
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 @@ out.append(a) out.append(b) else: - out.append("{}-{}".format(a, b)) + out.append(f"{a}-{b}") return "".join(out) @@ -70,7 +66,7 @@ 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 @@ "Issue tracker": "https://github.com/pallets/jinja/issues", }, license="BSD-3-Clause", - author="Armin Ronacher", - author_email="[email protected]", maintainer="Pallets", maintainer_email="[email protected]", description="A very fast and expressive template engine.", - long_description=readme, classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", @@ -33,15 +26,6 @@ "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 @@ 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 1229ba4..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 is_undefined from .utils import select_autoescape -__version__ = "2.11.2" +__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 @@ @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 @@ "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 @@ 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 @@ """ 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 io import BytesIO -from ._compat import BytesIO -from ._compat import marshal_dump -from ._compat import marshal_load -from ._compat import pickle -from ._compat import text_type 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 @@ ) -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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ - `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 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 @@ "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 @@ """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: + + if type(value) in {bool, int, float, complex, range, str, Markup}: 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 - 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 @@ return visitor.undeclared -class MacroRef(object): +class MacroRef: def __init__(self, node): self.node = node self.accesses_caller = False @@ -131,12 +96,12 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ # 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ # 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ #: 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 @@ # 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 @@ ``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 @@ 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 @@ 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 @@ 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 @@ # `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 @@ 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 @@ 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 @@ 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 @@ 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 @@ @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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ for attr in ( "argcount", "posonlyargcount", # Python 3.8 - "kwonlyargcount", # Python 3 + "kwonlyargcount", "nlocals", "stacksize", "flags", @@ -215,7 +208,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 @@ # 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 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 @@ """ 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 @@ != 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ """ 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 @@ 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 @@ 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 @@ 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 @@ """ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ """ 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 @@ 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 @@ .. 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 @@ 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 @@ 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 @@ """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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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): + super().__init__(message) - 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) - - @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 @@ 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 @@ 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 @@ 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 @@ 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 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 .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 @@ 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 @@ ) 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ 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 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 @@ * ``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 @@ 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 @@ 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 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 @@ 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 _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 @@ """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 @@ .. 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 @@ 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 @@ ): 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 @@ 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 @@ """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 @@ 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 @@ 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 @@ @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 @@ 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 @@ 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 @@ 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 @@ {{ 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 @@ 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 @@ 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 @@ """ 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_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 @@ 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 @@ 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 @@ 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_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 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 @@ "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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 552356a..0a99207 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 @@ 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 @@ _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 @@ 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 @@ ";": 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ # 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 @@ 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 @@ 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 @@ # 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 @@ 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 @@ } 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 @@ 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 @@ """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 @@ -746,16 +701,15 @@ # 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: @@ -779,13 +733,12 @@ 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, @@ -809,15 +762,14 @@ 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: @@ -828,7 +780,7 @@ # 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 @@ -841,8 +793,5 @@ 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..09f678d 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 @@ 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 @@ 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 @@ -57,8 +55,8 @@ if not exists(path): raise TemplateNotFound(template) mtime = getmtime(path) - with file(path) as f: - source = f.read().decode('utf-8') + with open(path) as f: + source = f.read() return source, path, lambda: mtime == getmtime(path) """ @@ -75,9 +73,9 @@ `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 @@ """ 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 @@ """ 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 @@ 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 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) + + # 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 + + return source.decode(self.encoding), p, up_to_date def list_templates(self): - path = self.package_path - - if path[:2] == "./": - path = path[2:] - elif path == ".": - path = "" - - offset = len(path) 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) - if self.provider.resource_isdir(fullname): - _walk(fullname) - else: - results.append(fullname[offset:].lstrip("/")) + 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." + ) - _walk(path) + # Package is a zip file. + prefix = ( + self._template_root[len(self._archive) :].lstrip(os.path.sep) + + os.path.sep + ) + offset = len(prefix) + + 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, "/")) + 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 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 @@ 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 @@ 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 @@ 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 @@ @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 @@ 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 @@ >>> 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 @@ # 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 @@ 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 @@ 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 a9ead4e..5ecf72b 100644 --- a/src/jinja2/nativetypes.py +++ b/src/jinja2/nativetypes.py
@@ -3,7 +3,6 @@ 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 @@ -27,7 +26,7 @@ if len(head) == 1: raw = head[0] else: - raw = u"".join([text_type(v) for v in chain(head, nodes)]) + raw = "".join([str(v) for v in chain(head, nodes)]) try: return literal_eval(raw) @@ -37,7 +36,7 @@ class NativeCodeGenerator(CodeGenerator): """A code generator which renders Python types by not adding - ``to_string()`` around output nodes. + ``str()`` around output nodes. """ @staticmethod @@ -45,7 +44,7 @@ return value def _output_const_repr(self, group): - return repr(u"".join([text_type(v) for v in group])) + return repr("".join([str(v) for v in group])) def _output_child_to_const(self, node, frame, finalize): const = node.as_const(frame.eval_ctx)
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 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 @@ "-": 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 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 @@ 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 @@ 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 @@ } -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 @@ 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 @@ 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 @@ """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 @@ # 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 @@ 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 @@ 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 @@ # 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 markupsafe import soft_str -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 .exceptions import TemplateNotFound # noqa: F401 from .exceptions import TemplateRuntimeError # noqa: F401 from .exceptions import UndefinedError @@ -39,18 +30,13 @@ "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 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 @@ # 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 @@ 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 @@ return missing -class Context(with_metaclass(ContextMeta)): [email protected] +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 @@ # 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ """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 @@ return rv -@implements_iterator class LoopContext: """A wrapper iterable for dynamic ``for`` loops, with information about the loop and iteration. @@ -564,10 +548,10 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ return id(type(self)) def __str__(self): - return u"" + return "" def __len__(self): return 0 @@ -817,11 +761,9 @@ if 0: yield None - def __nonzero__(self): + def __bool__(self): return False - __bool__ = __nonzero__ - def __repr__(self): return "Undefined" @@ -855,66 +797,31 @@ 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 + return super().__iter__() - 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: - - 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 @@ __getitem__ = __getattr__ -@implements_to_string class DebugUndefined(Undefined): """An undefined that returns the debug info when printed. @@ -960,17 +866,21 @@ __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 @@ """ __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 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_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 @@ ) 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 @@ """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 unsafe(f): """Marks a function or method as unsafe. - :: + .. code-block: python @unsafe def delete(self): @@ -230,10 +183,8 @@ 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 @@ >>> 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ 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 @@ .. 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_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_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, ("(", "<", "<"))) +_trail_pattern = "|".join(map(re.escape, (".", ",", ")", ">", "\n", ">"))) _punctuation_re = re.compile( - "^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$" - % ( - "|".join(map(re.escape, ("(", "<", "<"))), - "|".join(map(re.escape, (".", ",", ")", ">", "\n", ">"))), - ) + 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 @@ # internal code internal_code = set() -concat = u"".join +concat = "".join _slash_escape = "\\/" not in json.dumps("/") @@ -168,27 +161,18 @@ 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 + from pprint import pformat - return pretty(obj, verbose=verbose) - except ImportError: - 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ :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 @@ 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) + + [email protected] +class LRUCache: """A simple LRU Cache implementation.""" # this is fast for small capacities (something below 1000) but doesn't @@ -410,7 +401,7 @@ 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 @@ 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 @@ __copy__ = copy -abc.MutableMapping.register(LRUCache) - - def select_autoescape( enabled_extensions=("html", "htm", "xml"), disabled_extensions=(), @@ -574,8 +520,8 @@ .. 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 @@ 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 @@ __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 @@ 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 @@ 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 @@ 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 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 @@ 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 @@ "{% 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 @@ 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 @@ ) 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 @@ 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 @@ shutil.rmtree(tmp) -class TestUndefined(object): +class TestUndefined: def test_stopiteration_is_undefined(self): def test(): raise StopIteration() @@ -274,7 +272,7 @@ # 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ assert tmpl.render() == "1" -class TestMacros(object): +class TestMacros: def test_simple(self, env_trim): tmpl = env_trim.from_string( """\ @@ -469,7 +462,7 @@ 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 @@ 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 @@ "{% 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 @@ "{{ 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 @@ 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 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 @@ 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 @@ class ExampleExtension(Extension): - tags = set(["test"]) + tags = {"test"} ext_attr = 42 context_reference_node_cls = nodes.ContextReference @@ -117,12 +114,9 @@ ).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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ 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 @@ """ ) assert tmpl.render().split() == [ - u"<HelloWorld>", - u"<HelloWorld>", - u"<HelloWorld>", + "<HelloWorld>", + "<HelloWorld>", + "<HelloWorld>", ] env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=False) @@ -569,9 +554,9 @@ """ ) assert tmpl.render().split() == [ - u"<HelloWorld>", - u"<HelloWorld>", - u"<HelloWorld>", + "<HelloWorld>", + "<HelloWorld>", + "<HelloWorld>", ] def test_nonvolatile(self): @@ -614,7 +599,7 @@ """ 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 @@ 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 [email protected](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()) - - [email protected](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 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 @@ ), ) 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 @@ 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 @@ ), ) 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 @@ 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 @@ "{{ 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 @@ def test_forceescape(self, env): tmpl = env.from_string("{{ x|forceescape }}") - assert tmpl.render(x=Markup("<div />")) == u"<div />" + assert tmpl.render(x=Markup("<div />")) == "<div />" def test_safe(self, env): env = Environment(autoescape=True) @@ -603,10 +597,10 @@ ("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&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_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 @@ [ 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 @@ ), 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 @@ [ 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 @@ [ nodes.Output( [ - nodes.TemplateData(u"\n "), + nodes.TemplateData("\n "), nodes.Call( nodes.Name("render_title", "load"), [nodes.Name("item", "load")], @@ -130,11 +130,11 @@ 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 @@ 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 @@ "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 @@ "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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ ) -class TestInheritance(object): +class TestInheritance: def test_layout(self, env): tmpl = env.get_template("layout") assert tmpl.render() == ( @@ -157,7 +156,7 @@ ) 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 @@ assert rv == ["43", "44", "45"] -class TestBugFix(object): +class TestBugFix: def test_fixed_macro_scoping_bug(self, env): assert ( Environment( @@ -274,7 +273,7 @@ .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 83ae75e..c0257cf 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 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 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 TestLexer(object): +class TestLexer: def test_raw1(self, env): tmpl = env.from_string( "{% raw %}foo{% endraw %}|" @@ -106,24 +91,24 @@ ) 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 @@ 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 @@ break -class TestParser(object): +class TestParser: def test_php_syntax(self, env): env = Environment("<?", "?>", "<?=", "?>", "<!--", "-->") tmpl = env.from_string( @@ -330,7 +315,7 @@ 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 @@ ], ) 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 @@ @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 @@ ), ) 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 @@ ] 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 @@ 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 @@ 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 @@ 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 @@ ${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 <%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_blocks_outside_with_new_line(self): env = Environment(lstrip_blocks=True, trim_blocks=False) @@ -845,9 +829,7 @@ <?= 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( @@ -860,19 +842,12 @@ <?=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 %> @@ -881,7 +856,7 @@ <% 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 @@ 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 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 @@ 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 @@ 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 @@ @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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ self.mod_env = Environment(loader=mod_loader) self._test_common() + + [email protected]() +def package_dir_loader(monkeypatch): + monkeypatch.syspath_prepend(os.path.dirname(__file__)) + return PackageLoader("res") + + [email protected]( + ("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 + + [email protected]() +def package_zip_loader(monkeypatch): + monkeypatch.syspath_prepend( + os.path.join(os.path.dirname(__file__), "res", "package.zip") + ) + return PackageLoader("t_pack") + + [email protected]( + ("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 + + [email protected]( + 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 76871ab..2258181 100644 --- a/tests/test_nativetypes.py +++ b/tests/test_nativetypes.py
@@ -1,6 +1,7 @@ +import math + 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 +54,7 @@ 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 +112,7 @@ 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]" @@ -140,7 +141,7 @@ assert isinstance(result, float) # If intermediate eval happened, 0.000 would render 0.0, then 7 # would be appended, resulting in 0.07. - assert result < 0.007 # TODO use math.isclose in Python 3 + assert math.isclose(result, 0.0007) def test_spontaneous_env():
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 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 @@ 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 @@ """ ) - 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 @@ """ ) 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 @@ ] ).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 @@ 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 @@ 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 @@ 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 @@ 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 unsafe -class PrivateStuff(object): +class PrivateStuff: def bar(self): return 23 @@ -26,7 +23,7 @@ return "PrivateStuff" -class PublicStuff(object): +class PublicStuff: def bar(self): return 23 @@ -37,7 +34,7 @@ return "PublicStuff" -class TestSandbox(object): +class TestSandbox: def test_unsafe(self, env): env = SandboxedEnvironment() pytest.raises( @@ -76,44 +73,6 @@ "{% 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><bad user></em>" - assert ( - Markup("<em>%(username)s</em>") % {"username": "<bad user>"} - == "<em><bad user></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("\"<>&'") == ""<>&'" - assert Markup("<em>Foo & Bar</em>").striptags() == "Foo & Bar" - assert Markup("<test>").unescape() == "<test>" - def test_template_data(self, env): env = Environment(autoescape=True) t = env.from_string( @@ -123,7 +82,7 @@ ) escaped_out = "<p>Hello <blink>foo</blink>!</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 @@ 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 @@ 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 @@ assert t.render() == "a42b<foo>" [email protected]( - 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 @@ 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 @@ ), ) 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 @@ ), ) 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 @@ 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 urlize -class TestLRUCache(object): +class TestLRUCache: def test_simple(self): d = LRUCache(3) d["a"] = 1 @@ -29,13 +26,6 @@ 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 @@ 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 @@ 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 @@ 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 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