Compare commits
139 Commits
antlr-gram
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ef70112a1 | ||
|
|
284501eb0f | ||
|
|
7eb5758063 | ||
|
|
0514dce509 | ||
|
|
ede7905d30 | ||
|
|
77092a882a | ||
|
|
0b08e13cac | ||
|
|
8ee8f90d67 | ||
|
|
10304827e4 | ||
|
|
574565b1ef | ||
|
|
0547cd6c58 | ||
|
|
9c11dd65ff | ||
|
|
9e49736ae0 | ||
|
|
b7ce542db1 | ||
|
|
dfe82ade3d | ||
|
|
9508c57faa | ||
|
|
49c48a0b31 | ||
|
|
ece7c271f3 | ||
|
|
0cc6ff9051 | ||
|
|
05f5d74849 | ||
|
|
a3dce7bb64 | ||
|
|
51dbd8977e | ||
|
|
220e67ae99 | ||
|
|
01d0bab939 | ||
|
|
15206881c0 | ||
|
|
90457bbf33 | ||
|
|
065334d1ee | ||
|
|
033c20015c | ||
|
|
bc68d4efa9 | ||
|
|
247de5e0c5 | ||
|
|
6aeab5d1da | ||
|
|
ab8218c7a1 | ||
|
|
b4ffc8ff29 | ||
|
|
877f6e51be | ||
|
|
8d58859265 | ||
|
|
eda8fe86fd | ||
|
|
c8fdce1e03 | ||
|
|
66587ce989 | ||
|
|
fbc3a696c7 | ||
|
|
b8f4831d41 | ||
|
|
ee832194cd | ||
|
|
1d55cddbb2 | ||
|
|
8a8eafc6b9 | ||
|
|
d6998ab74e | ||
|
|
e7cb37de59 | ||
|
|
ae68c961dc | ||
|
|
028f61da7b | ||
|
|
d05bd3858c | ||
|
|
7a41ddb915 | ||
|
|
0cd6948192 | ||
|
|
106d61cba5 | ||
|
|
ded9915fc5 | ||
|
|
53c75915c9 | ||
|
|
aaa083d265 | ||
|
|
f54fa113d3 | ||
|
|
58a358f092 | ||
|
|
227edfd372 | ||
|
|
ed5f76206a | ||
|
|
b4b28ec01c | ||
|
|
e45bc745a7 | ||
|
|
767b236176 | ||
|
|
56a724644b | ||
|
|
48b0687e05 | ||
|
|
91a972f580 | ||
|
|
0871c71d01 | ||
|
|
91e3521173 | ||
|
|
b512058270 | ||
|
|
1dc04bccf9 | ||
|
|
7232b82462 | ||
|
|
ba8847a466 | ||
|
|
d4fb0e8c40 | ||
|
|
39d9ffff1f | ||
|
|
4936e4d482 | ||
|
|
3ef3ba885b | ||
|
|
2eb4542cba | ||
|
|
20be10e566 | ||
|
|
76af7110ea | ||
|
|
a4abbfd753 | ||
|
|
d3a0b1a4ab | ||
|
|
791dd3b041 | ||
|
|
955d7daf3d | ||
|
|
13ce60bad8 | ||
|
|
0c0a3d02d1 | ||
|
|
3d0a7d7b0f | ||
|
|
786d12b529 | ||
|
|
c667d56de3 | ||
|
|
a12789e7f9 | ||
|
|
75f0fbf6cb | ||
|
|
13c42b3aab | ||
|
|
64a6bd1b66 | ||
|
|
619d8eef41 | ||
|
|
7d023e5a86 | ||
|
|
420082efa5 | ||
|
|
8a90b760a8 | ||
|
|
058e059662 | ||
|
|
9c3622c1af | ||
|
|
da6729990f | ||
|
|
4e7850ce1b | ||
|
|
9849db5215 | ||
|
|
f502aac8dc | ||
|
|
d680a95932 | ||
|
|
ada0a9a6fc | ||
|
|
ee6c734e9b | ||
|
|
d3d0910d8a | ||
|
|
1e383959f7 | ||
|
|
af054f3e48 | ||
|
|
a9a0197e3c | ||
|
|
eb0df049de | ||
|
|
180816e571 | ||
|
|
4c49d2322c | ||
|
|
cd74006a9b | ||
|
|
9949b49808 | ||
|
|
3e5b5b2794 | ||
|
|
7f0fc0ad2c | ||
|
|
896a1d59b7 | ||
|
|
3adf44dde2 | ||
|
|
65b27afb61 | ||
|
|
a59744f50e | ||
|
|
b490da6b23 | ||
|
|
295b284b7c | ||
|
|
68d75132c4 | ||
|
|
0464cf88a0 | ||
|
|
293c8abe93 | ||
|
|
ae8b5354e0 | ||
|
|
f8323cf404 | ||
|
|
d7225e65f3 | ||
|
|
4e04e110e7 | ||
|
|
0087c5fe00 | ||
|
|
e72c1825d4 | ||
|
|
64b54f2189 | ||
|
|
64e6151474 | ||
|
|
9c6c319899 | ||
|
|
4b6dac1b6b | ||
|
|
951868f355 | ||
|
|
afb577b313 | ||
|
|
71e374d895 | ||
|
|
1470c17f9f | ||
|
|
8a8e2bc4d7 | ||
|
|
679af7f816 |
@ -1,7 +1,17 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
python3 -m venv --upgrade-deps .venv
|
||||
. .venv/bin/activate
|
||||
pip install -r requirements/dev.txt
|
||||
pip install -e .
|
||||
|
||||
# Install uv if not already installed
|
||||
if ! command -v uv &> /dev/null; then
|
||||
echo "Installing uv..."
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
fi
|
||||
|
||||
# Create venv using uv and install dependencies
|
||||
echo "Creating virtual environment and installing dependencies..."
|
||||
uv sync
|
||||
|
||||
# Install pre-commit hooks
|
||||
echo "Installing pre-commit hooks..."
|
||||
pre-commit install --install-hooks
|
||||
|
||||
18
.github/dependabot.yml
vendored
18
.github/dependabot.yml
vendored
@ -1,18 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- '*'
|
||||
- package-ecosystem: pip
|
||||
directory: /requirements/
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
python-requirements:
|
||||
patterns:
|
||||
- '*'
|
||||
1
.github/workflows/lock.yaml
vendored
1
.github/workflows/lock.yaml
vendored
@ -10,6 +10,7 @@ on:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
concurrency:
|
||||
group: lock
|
||||
jobs:
|
||||
|
||||
25
.github/workflows/pre-commit.yaml
vendored
Normal file
25
.github/workflows/pre-commit.yaml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: pre-commit
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main, stable]
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
with:
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
id: setup-python
|
||||
with:
|
||||
python-version-file: pyproject.toml
|
||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit|${{ hashFiles('pyproject.toml', '.pre-commit-config.yaml') }}
|
||||
- run: uv run --locked --group pre-commit pre-commit run --show-diff-on-failure --color=always --all-files
|
||||
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
||||
if: ${{ !cancelled() }}
|
||||
58
.github/workflows/publish.yaml
vendored
58
.github/workflows/publish.yaml
vendored
@ -1,61 +1,39 @@
|
||||
name: Publish
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
tags: ['*']
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
hash: ${{ steps.hash.outputs.hash }}
|
||||
steps:
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
with:
|
||||
python-version: '3.x'
|
||||
cache: pip
|
||||
cache-dependency-path: requirements*/*.txt
|
||||
- run: pip install -r requirements/build.txt
|
||||
# Use the commit date instead of the current date during the build.
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version-file: pyproject.toml
|
||||
- run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
|
||||
- run: python -m build
|
||||
# Generate hashes used for provenance.
|
||||
- name: generate hash
|
||||
id: hash
|
||||
run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
- run: uv build
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
path: ./dist
|
||||
provenance:
|
||||
needs: [build]
|
||||
permissions:
|
||||
actions: read
|
||||
id-token: write
|
||||
contents: write
|
||||
# Can't pin with hash due to how this workflow works.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
|
||||
with:
|
||||
base64-subjects: ${{ needs.build.outputs.hash }}
|
||||
create-release:
|
||||
# Upload the sdist, wheels, and provenance to a GitHub release. They remain
|
||||
# available as build artifacts for a while as well.
|
||||
needs: [provenance]
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
- name: create release
|
||||
run: >
|
||||
gh release create --draft --repo ${{ github.repository }}
|
||||
${{ github.ref_name }}
|
||||
*.intoto.jsonl/* artifact/*
|
||||
${{ github.ref_name }} artifact/*
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
publish-pypi:
|
||||
needs: [provenance]
|
||||
# Wait for approval before attempting to upload to PyPI. This allows reviewing the
|
||||
# files in the draft release.
|
||||
needs: [build]
|
||||
environment:
|
||||
name: publish
|
||||
url: https://pypi.org/project/Jinja2/${{ github.ref_name }}
|
||||
@ -63,11 +41,7 @@ jobs:
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
- uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
packages-dir: artifact/
|
||||
- uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
- uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
with:
|
||||
packages-dir: artifact/
|
||||
|
||||
55
.github/workflows/tests.yaml
vendored
55
.github/workflows/tests.yaml
vendored
@ -1,18 +1,10 @@
|
||||
name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- '*.x'
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '*.rst'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '*.rst'
|
||||
paths-ignore: ['docs/**', 'README.md']
|
||||
push:
|
||||
branches: [main, stable]
|
||||
paths-ignore: ['docs/**', 'README.md']
|
||||
jobs:
|
||||
tests:
|
||||
name: ${{ matrix.name || matrix.python }}
|
||||
@ -22,37 +14,36 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- {python: '3.13'}
|
||||
- {name: Windows, python: '3.13', os: windows-latest}
|
||||
- {name: Mac, python: '3.13', os: macos-latest}
|
||||
- {python: '3.12'}
|
||||
- {name: Windows, python: '3.12', os: windows-latest}
|
||||
- {name: Mac, python: '3.12', os: macos-latest}
|
||||
- {python: '3.11'}
|
||||
- {python: '3.10'}
|
||||
- {python: '3.9'}
|
||||
- {python: '3.8'}
|
||||
- {name: PyPy, python: 'pypy-3.10', tox: pypy310}
|
||||
- {name: PyPy, python: 'pypy-3.11', tox: pypy3.11}
|
||||
steps:
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
with:
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
allow-prereleases: true
|
||||
cache: pip
|
||||
cache-dependency-path: requirements*/*.txt
|
||||
- run: pip install tox
|
||||
- run: tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }}
|
||||
- run: uv run --locked tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }}
|
||||
typing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
with:
|
||||
python-version: '3.x'
|
||||
cache: pip
|
||||
cache-dependency-path: requirements*/*.txt
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version-file: pyproject.toml
|
||||
- name: cache mypy
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ./.mypy_cache
|
||||
key: mypy|${{ hashFiles('pyproject.toml') }}
|
||||
- run: pip install tox
|
||||
- run: tox run -e typing
|
||||
- run: uv run --locked tox run -e typing
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,13 +1,8 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
.venv*/
|
||||
venv*/
|
||||
__pycache__/
|
||||
dist/
|
||||
.coverage*
|
||||
htmlcov/
|
||||
.tox/
|
||||
docs/_build/
|
||||
|
||||
.antlr/
|
||||
*.input.txt
|
||||
@ -1,13 +1,15 @@
|
||||
ci:
|
||||
autoupdate_schedule: monthly
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.4.4
|
||||
rev: 76e47323a83cd9795e4ff9a1de1c0d2eef610f17 # frozen: v0.11.11
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||
rev: 648bdbfd6bb1a82f132ecc2c666e0d1b2e4b0d94 # frozen: 0.7.8
|
||||
hooks:
|
||||
- id: uv-lock
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: debug-statements
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
version: 2
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
os: ubuntu-24.04
|
||||
tools:
|
||||
python: '3.12'
|
||||
python:
|
||||
install:
|
||||
- requirements: requirements/docs.txt
|
||||
- method: pip
|
||||
path: .
|
||||
sphinx:
|
||||
builder: dirhtml
|
||||
fail_on_warning: true
|
||||
python: '3.13'
|
||||
commands:
|
||||
- asdf plugin add uv
|
||||
- asdf install uv latest
|
||||
- asdf global uv latest
|
||||
- uv run --group docs sphinx-build -W -b dirhtml docs $READTHEDOCS_OUTPUT/html
|
||||
|
||||
53
CHANGES.rst
53
CHANGES.rst
@ -5,17 +5,38 @@ Version 3.2.0
|
||||
|
||||
Unreleased
|
||||
|
||||
- Drop support for Python 3.7.
|
||||
- Drop support for Python 3.7, 3.8, and 3.9.
|
||||
- Update minimum MarkupSafe version to >= 3.0.
|
||||
- Update minimum Babel version to >= 2.17.
|
||||
- Deprecate the ``__version__`` attribute. Use feature detection or
|
||||
``importlib.metadata.version("jinja2")`` instead.
|
||||
- Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``.
|
||||
:pr:`1793`
|
||||
- Use ``flit_core`` instead of ``setuptools`` as build backend.
|
||||
|
||||
|
||||
Version 3.1.6
|
||||
-------------
|
||||
|
||||
Released 2025-03-05
|
||||
|
||||
- The ``|attr`` filter does not bypass the environment's attribute lookup,
|
||||
allowing the sandbox to apply its checks. :ghsa:`cpwx-vrp4-4pq7`
|
||||
|
||||
|
||||
Version 3.1.5
|
||||
-------------
|
||||
|
||||
Unreleased
|
||||
Released 2024-12-21
|
||||
|
||||
- The sandboxed environment handles indirect calls to ``str.format``, such as
|
||||
by passing a stored reference to a filter that calls its argument.
|
||||
:ghsa:`q2x7-8rv6-6q7h`
|
||||
- Escape template name before formatting it into error messages, to avoid
|
||||
issues with names that contain f-string syntax.
|
||||
:issue:`1792`, :ghsa:`gmj6-6f8f-6699`
|
||||
- Sandbox does not allow ``clear`` and ``pop`` on known mutable sequence
|
||||
types. :issue:`2032`
|
||||
- Calling sync ``render`` for an async template uses ``asyncio.run``.
|
||||
:pr:`1952`
|
||||
- Avoid unclosed ``auto_aiter`` warnings. :pr:`1960`
|
||||
@ -25,6 +46,32 @@ Unreleased
|
||||
``Template.generate_async``. :pr:`1960`
|
||||
- Avoid leaving async generators unclosed in blocks, includes and extends.
|
||||
:pr:`1960`
|
||||
- The runtime uses the correct ``concat`` function for the current environment
|
||||
when calling block references. :issue:`1701`
|
||||
- Make ``|unique`` async-aware, allowing it to be used after another
|
||||
async-aware filter. :issue:`1781`
|
||||
- ``|int`` filter handles ``OverflowError`` from scientific notation.
|
||||
:issue:`1921`
|
||||
- Make compiling deterministic for tuple unpacking in a ``{% set ... %}``
|
||||
call. :issue:`2021`
|
||||
- Fix dunder protocol (`copy`/`pickle`/etc) interaction with ``Undefined``
|
||||
objects. :issue:`2025`
|
||||
- Fix `copy`/`pickle` support for the internal ``missing`` object.
|
||||
:issue:`2027`
|
||||
- ``Environment.overlay(enable_async)`` is applied correctly. :pr:`2061`
|
||||
- The error message from ``FileSystemLoader`` includes the paths that were
|
||||
searched. :issue:`1661`
|
||||
- ``PackageLoader`` shows a clearer error message when the package does not
|
||||
contain the templates directory. :issue:`1705`
|
||||
- Improve annotations for methods returning copies. :pr:`1880`
|
||||
- ``urlize`` does not add ``mailto:`` to values like `@a@b`. :pr:`1870`
|
||||
- Tests decorated with `@pass_context`` can be used with the ``|select``
|
||||
filter. :issue:`1624`
|
||||
- Using ``set`` for multiple assignment (``a, b = 1, 2``) does not fail when the
|
||||
target is a namespace attribute. :issue:`1413`
|
||||
- Using ``set`` in all branches of ``{% if %}{% elif %}{% else %}`` blocks
|
||||
does not cause the variable to be considered initially undefined.
|
||||
:issue:`1253`
|
||||
|
||||
|
||||
Version 3.1.4
|
||||
@ -1012,7 +1059,7 @@ Released 2008-07-17, codename Jinjavitus
|
||||
evaluates to ``false``.
|
||||
- Improved error reporting for undefined values by providing a
|
||||
position.
|
||||
- ``filesizeformat`` filter uses decimal prefixes now per default and
|
||||
- ``filesizeformat`` filter uses decimal prefixes now by default and
|
||||
can be set to binary mode with the second parameter.
|
||||
- Fixed bug in finalizer
|
||||
|
||||
|
||||
216
CONTRIBUTING.rst
216
CONTRIBUTING.rst
@ -1,216 +0,0 @@
|
||||
How to contribute to Jinja
|
||||
==========================
|
||||
|
||||
Thank you for considering contributing to Jinja!
|
||||
|
||||
|
||||
Support questions
|
||||
-----------------
|
||||
|
||||
Please don't use the issue tracker for this. The issue tracker is a
|
||||
tool to address bugs and feature requests in Jinja itself. Use one of
|
||||
the following resources for questions about using Jinja or issues with
|
||||
your own code:
|
||||
|
||||
- The ``#get-help`` channel on our Discord chat:
|
||||
https://discord.gg/pallets
|
||||
- The mailing list flask@python.org for long term discussion or larger
|
||||
issues.
|
||||
- Ask on `Stack Overflow`_. Search with Google first using:
|
||||
``site:stackoverflow.com jinja {search term, exception message, etc.}``
|
||||
|
||||
.. _Stack Overflow: https://stackoverflow.com/questions/tagged/jinja?tab=Frequent
|
||||
|
||||
|
||||
Reporting issues
|
||||
----------------
|
||||
|
||||
Include the following information in your post:
|
||||
|
||||
- Describe what you expected to happen.
|
||||
- If possible, include a `minimal reproducible example`_ to help us
|
||||
identify the issue. This also helps check that the issue is not with
|
||||
your own code.
|
||||
- Describe what actually happened. Include the full traceback if there
|
||||
was an exception.
|
||||
- List your Python and Jinja versions. If possible, check if this
|
||||
issue is already fixed in the latest releases or the latest code in
|
||||
the repository.
|
||||
|
||||
.. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example
|
||||
|
||||
|
||||
Submitting patches
|
||||
------------------
|
||||
|
||||
If there is not an open issue for what you want to submit, prefer
|
||||
opening one for discussion before working on a PR. You can work on any
|
||||
issue that doesn't have an open PR linked to it or a maintainer assigned
|
||||
to it. These show up in the sidebar. No need to ask if you can work on
|
||||
an issue that interests you.
|
||||
|
||||
Include the following in your patch:
|
||||
|
||||
- Use `Black`_ to format your code. This and other tools will run
|
||||
automatically if you install `pre-commit`_ using the instructions
|
||||
below.
|
||||
- Include tests if your patch adds or changes code. Make sure the test
|
||||
fails without your patch.
|
||||
- Update any relevant docs pages and docstrings. Docs pages and
|
||||
docstrings should be wrapped at 72 characters.
|
||||
- Add an entry in ``CHANGES.rst``. Use the same style as other
|
||||
entries. Also include ``.. versionchanged::`` inline changelogs in
|
||||
relevant docstrings.
|
||||
|
||||
.. _Black: https://black.readthedocs.io
|
||||
.. _pre-commit: https://pre-commit.com
|
||||
|
||||
|
||||
First time setup
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
- Download and install the `latest version of git`_.
|
||||
- Configure git with your `username`_ and `email`_.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ git config --global user.name 'your name'
|
||||
$ git config --global user.email 'your email'
|
||||
|
||||
- Make sure you have a `GitHub account`_.
|
||||
- Fork Jinja to your GitHub account by clicking the `Fork`_ button.
|
||||
- `Clone`_ the main repository locally.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ git clone https://github.com/pallets/jinja
|
||||
$ cd jinja
|
||||
|
||||
- Add your fork as a remote to push your work to. Replace
|
||||
``{username}`` with your username. This names the remote "fork", the
|
||||
default Pallets remote is "origin".
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ git remote add fork https://github.com/{username}/jinja
|
||||
|
||||
- Create a virtualenv.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ python3 -m venv env
|
||||
$ . env/bin/activate
|
||||
|
||||
On Windows, activating is different.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
> env\Scripts\activate
|
||||
|
||||
- Install the development dependencies, then install Jinja in editable
|
||||
mode.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ pip install -r requirements/dev.txt && pip install -e .
|
||||
|
||||
- Install the pre-commit hooks.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ pre-commit install
|
||||
|
||||
.. _latest version of git: https://git-scm.com/downloads
|
||||
.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git
|
||||
.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address
|
||||
.. _GitHub account: https://github.com/join
|
||||
.. _Fork: https://github.com/pallets/jinja/fork
|
||||
.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork
|
||||
|
||||
|
||||
Start coding
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- Create a branch to identify the issue you would like to work on. If
|
||||
you're submitting a bug or documentation fix, branch off of the
|
||||
latest ".x" branch.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ git fetch origin
|
||||
$ git checkout -b your-branch-name origin/3.0.x
|
||||
|
||||
If you're submitting a feature addition or change, branch off of the
|
||||
"main" branch.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ git fetch origin
|
||||
$ git checkout -b your-branch-name origin/main
|
||||
|
||||
- Using your favorite editor, make your changes,
|
||||
`committing as you go`_.
|
||||
- Include tests that cover any code changes you make. Make sure the
|
||||
test fails without your patch. Run the tests as described below.
|
||||
- Push your commits to your fork on GitHub and
|
||||
`create a pull request`_. Link to the issue being addressed with
|
||||
``fixes #123`` in the pull request.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ git push --set-upstream fork your-branch-name
|
||||
|
||||
.. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes
|
||||
.. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
|
||||
|
||||
|
||||
Running the tests
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Run the basic test suite with pytest.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ pytest
|
||||
|
||||
This runs the tests for the current environment, which is usually
|
||||
sufficient. CI will run the full suite when you submit your pull
|
||||
request. You can run the full test suite with tox if you don't want to
|
||||
wait.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ tox
|
||||
|
||||
|
||||
Running test coverage
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Generating a report of lines that do not have test coverage can indicate
|
||||
where to start contributing. Run ``pytest`` using ``coverage`` and
|
||||
generate a report.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ pip install coverage
|
||||
$ coverage run -m pytest
|
||||
$ coverage html
|
||||
|
||||
Open ``htmlcov/index.html`` in your browser to explore the report.
|
||||
|
||||
Read more about `coverage <https://coverage.readthedocs.io>`__.
|
||||
|
||||
|
||||
Building the docs
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Build the docs in the ``docs`` directory using Sphinx.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ cd docs
|
||||
$ make html
|
||||
|
||||
Open ``_build/html/index.html`` in your browser to view the docs.
|
||||
|
||||
Read more about `Sphinx <https://www.sphinx-doc.org/en/stable/>`__.
|
||||
10
README.md
10
README.md
@ -1,3 +1,5 @@
|
||||
<div align="center"><img src="https://raw.githubusercontent.com/pallets/jinja/refs/heads/stable/docs/_static/jinja-name.svg" alt="" height="150"></div>
|
||||
|
||||
# Jinja
|
||||
|
||||
Jinja is a fast, expressive, extensible templating engine. Special
|
||||
@ -47,3 +49,11 @@ allow the maintainers to devote more time to the projects, [please
|
||||
donate today][].
|
||||
|
||||
[please donate today]: https://palletsprojects.com/donate
|
||||
|
||||
## Contributing
|
||||
|
||||
See our [detailed contributing documentation][contrib] for many ways to
|
||||
contribute, including reporting issues, requesting features, asking or answering
|
||||
questions, and making PRs.
|
||||
|
||||
[contrib]: https://palletsprojects.com/contributing/
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 18 KiB |
11
docs/_static/jinja-icon.svg
vendored
Normal file
11
docs/_static/jinja-icon.svg
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<rect id="Icon" x="0" y="0" width="500" height="500" style="fill:none;"/>
|
||||
<clipPath id="_clip1">
|
||||
<rect x="0" y="0" width="500" height="500"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip1)">
|
||||
<path d="M491.941,72.796l-1.81,-0c-88.724,29.526 -204.909,29.526 -237.199,28.989l-29.877,-0.536c-88.119,-3.222 -133.085,-37.043 -211.849,-72.206c-5.432,-2.416 -11.77,1.61 -11.166,7.247c0.604,19.327 5.734,100.39 66.392,121.596c2.112,0.805 4.526,1.61 6.639,2.147c3.018,0.537 4.225,2.953 4.828,4.563l5.131,15.837c0.905,3.758 3.621,6.979 6.639,6.979l5.13,0c4.527,0 8.148,3.221 8.45,7.248l-0,15.3c-0,1.61 -1.509,2.953 -3.32,2.953l-38.929,-0c-3.622,-0 -6.64,2.684 -6.64,5.905l0,23.89c0,3.221 3.018,5.905 6.64,5.905l38.929,-0c1.811,-0 3.32,1.342 3.32,2.953l-0,6.442c-0,1.61 -1.509,2.952 -3.32,2.952l-38.929,0c-3.622,0.269 -6.338,2.685 -6.338,5.906l0,23.889c0,2.685 2.414,5.637 5.13,5.637l40.439,0c1.811,0 3.32,1.342 3.32,2.953l-0,157.027c-0,8.053 7.544,14.764 16.597,14.764l27.462,-0c9.054,-0 16.598,-6.711 16.598,-14.764l0,-157.027c0,-1.611 1.509,-2.953 3.32,-2.953l169.6,-0.268c1.811,-0 3.32,1.342 3.32,2.952l-0,157.833c-0,8.053 7.544,14.764 16.598,14.764l27.462,-0c9.053,-0 16.598,-6.711 16.598,-14.764l-0,-158.101c-0,-1.611 1.508,-2.953 3.319,-2.953c0,0 42.249,-0.268 42.853,-0.537c1.811,-1.073 3.018,-2.952 3.018,-5.1l-0,-23.621c-0,-3.221 -3.018,-5.905 -6.941,-5.905l-41.948,-0l0,-0.269l-0.301,0l-0,-8.857c-0,-1.611 1.508,-2.953 3.319,-2.953l38.93,-0c3.621,-0 6.639,-2.684 6.639,-5.905l-0,-23.89c-0,-3.221 -3.018,-5.905 -6.639,-5.905l-38.93,-0c-1.811,-0 -3.319,-1.343 -3.319,-2.953l-0,-15.3c-0,-3.758 3.621,-7.248 8.449,-7.248l5.131,0c3.621,0 5.733,-3.489 6.639,-6.979l5.13,-15.837c0.604,-2.147 2.716,-4.026 5.13,-4.831c38.93,-8.59 54.924,-34.09 68.203,-74.085l-0,-0.268c1.508,-10.2 -5.432,-12.079 -7.847,-12.616Zm-150.89,114.08l0,23.352c0,3.221 -2.112,5.637 -4.828,5.637l-54.321,0c-2.716,0 -4.828,-2.416 -4.828,-5.637l-0,-23.352c-0,-2.953 2.112,-5.637 4.828,-5.637l54.321,-0c2.414,0.268 4.828,2.684 4.828,5.637Zm-111.96,-0l-0,23.352c-0,2.953 -2.112,5.637 -4.828,5.637l-54.321,0c-2.716,0 -4.828,-2.416 -4.828,-5.637l-0,-23.352c-0,-2.953 2.112,-5.637 4.828,-5.637l54.321,-0c2.414,0.268 4.828,2.684 4.828,5.637Z" style="fill:#7e0c1b;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
BIN
docs/_static/jinja-logo-sidebar.png
vendored
BIN
docs/_static/jinja-logo-sidebar.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
BIN
docs/_static/jinja-logo.png
vendored
BIN
docs/_static/jinja-logo.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
11
docs/_static/jinja-logo.svg
vendored
Normal file
11
docs/_static/jinja-logo.svg
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<rect id="Logo" x="0" y="0" width="500" height="500" style="fill:none;"/>
|
||||
<path id="Box" d="M500,50l0,400c0,27.596 -22.404,50 -50,50l-400,0c-27.596,0 -50,-22.404 -50,-50l0,-400c0,-27.596 22.404,-50 50,-50l400,0c27.596,0 50,22.404 50,50Z" style="fill:url(#_Linear1);"/>
|
||||
<path id="Shadow" d="M500,246.897l0,203.103c0,27.596 -22.404,50 -50,50l-164.98,0l-119.852,-119.852c1.802,1.562 4.252,2.533 6.921,2.533l16.477,0c5.432,0 9.959,-4.026 9.959,-8.858l-0,-94.216c-0,-0.966 0.905,-1.772 1.992,-1.772l101.76,-0.161c1.086,0 1.992,0.806 1.992,1.772l-0,94.7c-0,4.831 4.526,8.858 9.958,8.858l16.478,-0c5.432,-0 9.958,-4.027 9.958,-8.858l0,-94.861c0,-0.967 0.906,-1.772 1.992,-1.772c0,0 25.35,-0.161 25.712,-0.322c1.086,-0.644 1.81,-1.771 1.81,-3.06l0,-14.173c0,-1.932 -1.81,-3.543 -4.164,-3.543l-25.169,0l0,-0.161l-0.181,0l0,-5.315c0,-0.966 0.906,-1.771 1.992,-1.771l23.358,-0c2.173,-0 3.983,-1.611 3.983,-3.543l0,-14.334c0,-1.933 -1.81,-3.543 -3.983,-3.543l-23.358,-0c-1.086,-0 -1.992,-0.806 -1.992,-1.772l0,-9.18c0,-2.255 2.173,-4.349 5.07,-4.349l3.078,0c2.173,0 3.441,-2.093 3.984,-4.187l3.078,-9.502c0.362,-1.289 1.63,-2.416 3.078,-2.899c23.358,-5.154 32.955,-20.454 40.922,-44.451l-0,-0.161c0.422,-2.854 -0.258,-4.622 -1.252,-5.729l101.379,101.379Zm-375.729,-61.204c4.362,3.788 9.511,6.914 15.588,9.039c1.267,0.483 2.716,0.966 3.983,1.288c1.811,0.322 2.535,1.772 2.898,2.738l3.078,9.502c0.543,2.255 2.173,4.187 3.983,4.187l3.078,0c2.716,0 4.889,1.933 5.07,4.349l0,6.575l-37.678,-37.678Zm9.606,62.5c0.716,0.602 1.677,0.975 2.723,0.975l23.358,-0c1.086,-0 1.991,0.805 1.991,1.771l0,3.866c0,0.966 -0.905,1.771 -1.991,1.771l-17.698,0l-8.383,-8.383Zm0.033,28.751c0.543,0.537 1.236,0.891 1.965,0.891l24.264,0c1.086,0 1.991,0.806 1.991,1.772l0,25.557l-28.22,-28.22Zm170.721,-64.819l-0,14.012c-0,1.933 -1.268,3.382 -2.897,3.382l-32.593,0c-1.629,0 -2.897,-1.449 -2.897,-3.382l0,-14.012c0,-1.771 1.268,-3.382 2.897,-3.382l32.593,0c1.448,0.161 2.897,1.611 2.897,3.382Zm-67.176,0l-0,14.012c-0,1.772 -1.268,3.382 -2.897,3.382l-32.593,0c-1.629,0 -2.897,-1.449 -2.897,-3.382l0,-14.012c0,-1.771 1.268,-3.382 2.897,-3.382l32.593,0c1.448,0.161 2.897,1.611 2.897,3.382Z" style="fill:#630b28;"/>
|
||||
<path id="Icon" d="M395.165,143.677l-1.087,0c-53.234,17.716 -122.945,17.716 -142.319,17.394l-17.926,-0.322c-52.872,-1.933 -79.851,-22.225 -127.109,-43.323c-3.26,-1.45 -7.062,0.966 -6.7,4.348c0.362,11.596 3.44,60.234 39.835,72.958c1.267,0.483 2.716,0.966 3.983,1.288c1.811,0.322 2.535,1.772 2.898,2.738l3.078,9.502c0.543,2.255 2.173,4.187 3.983,4.187l3.078,0c2.716,0 4.889,1.933 5.07,4.349l0,9.18c0,0.966 -0.905,1.772 -1.991,1.772l-23.358,-0c-2.173,-0 -3.984,1.61 -3.984,3.543l0,14.334c0,1.932 1.811,3.543 3.984,3.543l23.358,-0c1.086,-0 1.991,0.805 1.991,1.771l0,3.866c0,0.966 -0.905,1.771 -1.991,1.771l-23.358,0c-2.173,0.161 -3.803,1.611 -3.803,3.543l0,14.334c0,1.611 1.449,3.382 3.078,3.382l24.264,0c1.086,0 1.991,0.806 1.991,1.772l0,94.216c0,4.832 4.527,8.858 9.959,8.858l16.477,0c5.432,0 9.959,-4.026 9.959,-8.858l-0,-94.216c-0,-0.966 0.905,-1.772 1.992,-1.772l101.76,-0.161c1.086,0 1.992,0.806 1.992,1.772l-0,94.7c-0,4.831 4.526,8.858 9.958,8.858l16.478,-0c5.432,-0 9.958,-4.027 9.958,-8.858l0,-94.861c0,-0.967 0.906,-1.772 1.992,-1.772c0,0 25.35,-0.161 25.712,-0.322c1.086,-0.644 1.81,-1.771 1.81,-3.06l0,-14.173c0,-1.932 -1.81,-3.543 -4.164,-3.543l-25.169,0l0,-0.161l-0.181,0l0,-5.315c0,-0.966 0.906,-1.771 1.992,-1.771l23.358,-0c2.173,-0 3.983,-1.611 3.983,-3.543l0,-14.334c0,-1.933 -1.81,-3.543 -3.983,-3.543l-23.358,-0c-1.086,-0 -1.992,-0.806 -1.992,-1.772l0,-9.18c0,-2.255 2.173,-4.349 5.07,-4.349l3.078,0c2.173,0 3.441,-2.093 3.984,-4.187l3.078,-9.502c0.362,-1.289 1.63,-2.416 3.078,-2.899c23.358,-5.154 32.955,-20.454 40.922,-44.451l-0,-0.161c0.905,-6.12 -3.26,-7.247 -4.708,-7.57Zm-90.534,68.448l-0,14.012c-0,1.933 -1.268,3.382 -2.897,3.382l-32.593,0c-1.629,0 -2.897,-1.449 -2.897,-3.382l0,-14.012c0,-1.771 1.268,-3.382 2.897,-3.382l32.593,0c1.448,0.161 2.897,1.611 2.897,3.382Zm-67.176,0l-0,14.012c-0,1.772 -1.268,3.382 -2.897,3.382l-32.593,0c-1.629,0 -2.897,-1.449 -2.897,-3.382l0,-14.012c0,-1.771 1.268,-3.382 2.897,-3.382l32.593,0c1.448,0.161 2.897,1.611 2.897,3.382Z" style="fill:#fff;fill-rule:nonzero;"/>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.06162e-14,500,-500,3.06162e-14,267.59,0)"><stop offset="0" style="stop-color:#f6cadc;stop-opacity:1"/><stop offset="1" style="stop-color:#7f0d18;stop-opacity:1"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
19
docs/_static/jinja-name.svg
vendored
Normal file
19
docs/_static/jinja-name.svg
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 664 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g>
|
||||
<path id="Name" d="M416,89.625l0,68.7c0,14.3 -2.95,24.375 -8.85,30.225c-5.9,5.85 -14.45,8.775 -25.65,8.775c-7.2,-0 -13.225,-0.95 -18.075,-2.85c-4.85,-1.9 -9.325,-4.9 -13.425,-9l11.7,-12.75c1.7,1.7 4.275,3.125 7.725,4.275c3.45,1.15 7.4,1.725 11.85,1.725c4.45,-0 7.875,-1.075 10.275,-3.225c2.4,-2.15 3.6,-6.175 3.6,-12.075l0,-73.8l20.85,-0Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M446.45,78.075c4.7,-0 7.75,0.725 9.15,2.175c1.4,1.45 2.1,4.5 2.1,9.15c0,4.65 -0.725,7.7 -2.175,9.15c-1.45,1.45 -4.5,2.175 -9.15,2.175c-4.65,-0 -7.7,-0.75 -9.15,-2.25c-1.45,-1.5 -2.175,-4.55 -2.175,-9.15c0,-4.6 0.725,-7.625 2.175,-9.075c1.45,-1.45 4.525,-2.175 9.225,-2.175Zm9.6,118.35l-19.5,-0l0,-82.5l19.5,-0l0,82.5Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M544.25,196.425l-19.5,-0l0,-54.75c0,-4.9 -1,-8.25 -3,-10.05c-2,-1.8 -4.8,-2.7 -8.4,-2.7l-17.55,7.35l0,60.15l-19.5,-0l0,-85.65l14.55,-0l4.5,10.2l24,-11.1c6.7,-0 12.525,2.475 17.475,7.425c4.95,4.95 7.425,12.175 7.425,21.675l0,57.45Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M583.25,113.925l0,85.65c0,6.1 -1.925,11.35 -5.775,15.75c-3.85,4.4 -9.525,6.6 -17.025,6.6l-11.1,-0l0,-16.5l7.5,-0c4.6,-0 6.9,-2.35 6.9,-7.05l0,-84.45l19.5,-0Zm-9.6,-35.85c4.7,-0 7.75,0.725 9.15,2.175c1.4,1.45 2.1,4.5 2.1,9.15c0,4.65 -0.725,7.7 -2.175,9.15c-1.45,1.45 -4.5,2.175 -9.15,2.175c-4.65,-0 -7.7,-0.75 -9.15,-2.25c-1.45,-1.5 -2.175,-4.55 -2.175,-9.15c0,-4.6 0.725,-7.625 2.175,-9.075c1.45,-1.45 4.525,-2.175 9.225,-2.175Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M663.95,196.425l-13.05,-0l-6,-10.35l-20.85,11.25c-11.6,-0 -19.4,-4.2 -23.4,-12.6c-2,-4.1 -3.375,-8.525 -4.125,-13.275c-0.75,-4.75 -1.125,-9.7 -1.125,-14.85c0,-5.15 0.05,-8.95 0.15,-11.4c0.1,-2.45 0.35,-5.3 0.75,-8.55c0.4,-3.25 0.975,-5.975 1.725,-8.175c0.75,-2.2 1.825,-4.475 3.225,-6.825c1.4,-2.35 3.1,-4.225 5.1,-5.625c4.5,-3.1 10.35,-4.65 17.55,-4.65l20.55,-0l19.5,-1.2l0,86.25Zm-19.5,-27.3l0,-40.2l-14.85,-0c-5.5,-0 -9.325,2.1 -11.475,6.3c-2.15,4.2 -3.225,10.475 -3.225,18.825c0,8.35 1.025,14.225 3.075,17.625c2.05,3.4 5.925,5.1 11.625,5.1l14.85,-7.65Z" style="fill-rule:nonzero;"/>
|
||||
<g id="Logo">
|
||||
<path id="Box" d="M300,30l-0,240c-0,16.557 -13.443,30 -30,30l-240,-0c-16.557,-0 -30,-13.443 -30,-30l0,-240c0,-16.557 13.443,-30 30,-30l240,0c16.557,0 30,13.443 30,30Z" style="fill:url(#_Linear1);"/>
|
||||
<path id="Shadow" d="M300,148.138l0,121.862c0,16.557 -13.443,30 -30,30l-98.988,-0l-71.911,-71.911c1.081,0.937 2.551,1.52 4.152,1.52l9.887,-0c3.259,-0 5.975,-2.416 5.975,-5.315l-0,-56.53c-0,-0.58 0.543,-1.063 1.195,-1.063l61.056,-0.096c0.652,-0 1.195,0.483 1.195,1.063l0,56.819c0,2.899 2.716,5.315 5.975,5.315l9.887,0c3.259,0 5.975,-2.416 5.975,-5.315l-0,-56.916c-0,-0.58 0.543,-1.063 1.195,-1.063c0,-0 15.21,-0.097 15.427,-0.193c0.652,-0.387 1.086,-1.063 1.086,-1.836l0,-8.504c0,-1.16 -1.086,-2.126 -2.498,-2.126l-15.101,0l-0,-0.097l-0.109,0l-0,-3.188c-0,-0.58 0.543,-1.063 1.195,-1.063l14.015,-0c1.303,-0 2.39,-0.967 2.39,-2.126l-0,-8.601c-0,-1.159 -1.087,-2.125 -2.39,-2.125l-14.015,-0c-0.652,-0 -1.195,-0.484 -1.195,-1.063l-0,-5.508c-0,-1.353 1.304,-2.61 3.042,-2.61l1.847,0c1.304,0 2.064,-1.256 2.39,-2.512l1.847,-5.701c0.217,-0.773 0.978,-1.45 1.847,-1.74c14.014,-3.092 19.772,-12.272 24.553,-26.67l-0,-0.097c0.253,-1.712 -0.155,-2.773 -0.751,-3.437l60.827,60.827Zm-225.437,-36.722c2.617,2.273 5.706,4.148 9.352,5.423c0.761,0.29 1.63,0.58 2.39,0.773c1.087,0.193 1.521,1.063 1.739,1.643l1.847,5.701c0.326,1.353 1.303,2.512 2.39,2.512l1.847,0c1.629,0 2.933,1.16 3.042,2.61l-0,3.945l-22.607,-22.607Zm5.763,37.5c0.43,0.361 1.007,0.585 1.634,0.585l14.015,-0c0.651,-0 1.195,0.483 1.195,1.063l-0,2.319c-0,0.58 -0.544,1.063 -1.195,1.063l-10.619,-0l-5.03,-5.03Zm0.02,17.251c0.326,0.321 0.742,0.534 1.179,0.534l14.558,0c0.652,0 1.195,0.483 1.195,1.063l0,15.335l-16.932,-16.932Zm102.432,-38.892l0,8.407c0,1.16 -0.76,2.029 -1.738,2.029l-19.555,0c-0.978,0 -1.738,-0.869 -1.738,-2.029l-0,-8.407c-0,-1.063 0.76,-2.029 1.738,-2.029l19.555,-0c0.869,0.097 1.738,0.966 1.738,2.029Zm-40.305,0l-0,8.407c-0,1.063 -0.761,2.029 -1.738,2.029l-19.556,0c-0.978,0 -1.738,-0.869 -1.738,-2.029l-0,-8.407c-0,-1.063 0.76,-2.029 1.738,-2.029l19.556,-0c0.869,0.097 1.738,0.966 1.738,2.029Z" style="fill:#630b28;"/>
|
||||
<path id="Icon" d="M237.099,86.206l-0.652,0c-31.94,10.63 -73.767,10.63 -85.392,10.437l-10.755,-0.194c-31.723,-1.159 -47.911,-13.335 -76.266,-25.994c-1.955,-0.869 -4.237,0.58 -4.02,2.609c0.218,6.958 2.065,36.141 23.901,43.775c0.761,0.29 1.63,0.58 2.39,0.773c1.087,0.193 1.521,1.063 1.739,1.643l1.847,5.701c0.326,1.353 1.303,2.512 2.39,2.512l1.847,0c1.629,0 2.933,1.16 3.042,2.61l-0,5.508c-0,0.579 -0.544,1.063 -1.195,1.063l-14.015,-0c-1.304,-0 -2.39,0.966 -2.39,2.125l-0,8.601c-0,1.159 1.086,2.126 2.39,2.126l14.015,-0c0.651,-0 1.195,0.483 1.195,1.063l-0,2.319c-0,0.58 -0.544,1.063 -1.195,1.063l-14.015,-0c-1.304,0.096 -2.282,0.966 -2.282,2.126l0,8.6c0,0.966 0.87,2.029 1.847,2.029l14.558,0c0.652,0 1.195,0.483 1.195,1.063l0,56.53c0,2.899 2.716,5.315 5.975,5.315l9.887,-0c3.259,-0 5.975,-2.416 5.975,-5.315l-0,-56.53c-0,-0.58 0.543,-1.063 1.195,-1.063l61.056,-0.096c0.652,-0 1.195,0.483 1.195,1.063l0,56.819c0,2.899 2.716,5.315 5.975,5.315l9.887,0c3.259,0 5.975,-2.416 5.975,-5.315l-0,-56.916c-0,-0.58 0.543,-1.063 1.195,-1.063c0,-0 15.21,-0.097 15.427,-0.193c0.652,-0.387 1.086,-1.063 1.086,-1.836l0,-8.504c0,-1.16 -1.086,-2.126 -2.498,-2.126l-15.101,0l-0,-0.097l-0.109,0l-0,-3.188c-0,-0.58 0.543,-1.063 1.195,-1.063l14.015,-0c1.303,-0 2.39,-0.967 2.39,-2.126l-0,-8.601c-0,-1.159 -1.087,-2.125 -2.39,-2.125l-14.015,-0c-0.652,-0 -1.195,-0.484 -1.195,-1.063l-0,-5.508c-0,-1.353 1.304,-2.61 3.042,-2.61l1.847,0c1.304,0 2.064,-1.256 2.39,-2.512l1.847,-5.701c0.217,-0.773 0.978,-1.45 1.847,-1.74c14.014,-3.092 19.772,-12.272 24.553,-26.67l-0,-0.097c0.543,-3.672 -1.956,-4.348 -2.825,-4.542Zm-54.321,41.069l0,8.407c0,1.16 -0.76,2.029 -1.738,2.029l-19.555,0c-0.978,0 -1.738,-0.869 -1.738,-2.029l-0,-8.407c-0,-1.063 0.76,-2.029 1.738,-2.029l19.555,-0c0.869,0.097 1.738,0.966 1.738,2.029Zm-40.305,0l-0,8.407c-0,1.063 -0.761,2.029 -1.738,2.029l-19.556,0c-0.978,0 -1.738,-0.869 -1.738,-2.029l-0,-8.407c-0,-1.063 0.76,-2.029 1.738,-2.029l19.556,-0c0.869,0.097 1.738,0.966 1.738,2.029Z" style="fill:#fff;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83697e-14,300,-300,1.83697e-14,160.554,0)"><stop offset="0" style="stop-color:#f6cadc;stop-opacity:1"/><stop offset="1" style="stop-color:#7f0d18;stop-opacity:1"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
@ -666,8 +666,8 @@ Now it can be used in templates:
|
||||
|
||||
.. sourcecode:: jinja
|
||||
|
||||
{{ article.pub_date|datetimeformat }}
|
||||
{{ article.pub_date|datetimeformat("%B %Y") }}
|
||||
{{ article.pub_date|datetime_format }}
|
||||
{{ article.pub_date|datetime_format("%B %Y") }}
|
||||
|
||||
Some decorators are available to tell Jinja to pass extra information to
|
||||
the filter. The object is passed as the first argument, making the value
|
||||
@ -697,10 +697,10 @@ enabled before escaping the input and marking the output safe.
|
||||
br = Markup(br)
|
||||
|
||||
result = "\n\n".join(
|
||||
f"<p>{br.join(p.splitlines())}<\p>"
|
||||
f"<p>{br.join(p.splitlines())}</p>"
|
||||
for p in re.split(r"(?:\r\n|\r(?!\n)|\n){2,}", value)
|
||||
)
|
||||
return Markup(result) if autoescape else result
|
||||
return Markup(result) if eval_ctx.autoescape else result
|
||||
|
||||
|
||||
.. _writing-tests:
|
||||
|
||||
@ -24,7 +24,7 @@ autodoc_preserve_defaults = True
|
||||
extlinks = {
|
||||
"issue": ("https://github.com/pallets/jinja/issues/%s", "#%s"),
|
||||
"pr": ("https://github.com/pallets/jinja/pull/%s", "#%s"),
|
||||
"ghsa": ("https://github.com/advisories/GHSA-%s", "GHSA-%s"),
|
||||
"ghsa": ("https://github.com/pallets/jinja/security/advisories/GHSA-%s", "GHSA-%s"),
|
||||
}
|
||||
intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3/", None),
|
||||
@ -49,7 +49,7 @@ html_sidebars = {
|
||||
}
|
||||
singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]}
|
||||
html_static_path = ["_static"]
|
||||
html_favicon = "_static/jinja-logo-sidebar.png"
|
||||
html_logo = "_static/jinja-logo-sidebar.png"
|
||||
html_favicon = "_static/jinja-icon.svg"
|
||||
html_logo = "_static/jinja-logo.svg"
|
||||
html_title = f"Jinja Documentation ({version})"
|
||||
html_show_sourcelink = False
|
||||
|
||||
@ -39,6 +39,10 @@ After enabling, an application has to provide functions for ``gettext``,
|
||||
globally or when rendering. A ``_()`` function is added as an alias to
|
||||
the ``gettext`` function.
|
||||
|
||||
A convenient way to provide these functions is to call one of the below
|
||||
methods depending on the translation system in use. If you do not require
|
||||
actual translation, use ``Environment.install_null_translations`` to
|
||||
install no-op functions.
|
||||
|
||||
Environment Methods
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -70,6 +70,8 @@ these document types.
|
||||
|
||||
While automatic escaping means that you are less likely have an XSS
|
||||
problem, it also requires significant extra processing during compiling
|
||||
and rendering, which can reduce performance. Jinja uses MarkupSafe for
|
||||
and rendering, which can reduce performance. Jinja uses `MarkupSafe`_ for
|
||||
escaping, which provides optimized C code for speed, but it still
|
||||
introduces overhead to track escaping across methods and formatting.
|
||||
|
||||
.. _MarkupSafe: https://markupsafe.palletsprojects.com/
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
Jinja
|
||||
=====
|
||||
|
||||
.. image:: _static/jinja-logo.png
|
||||
.. image:: _static/jinja-name.svg
|
||||
:align: center
|
||||
:target: https://palletsprojects.com/p/jinja/
|
||||
:height: 200px
|
||||
|
||||
Jinja is a fast, expressive, extensible templating engine. Special
|
||||
placeholders in the template allow writing code similar to Python
|
||||
|
||||
@ -30,7 +30,7 @@ Installation
|
||||
------------
|
||||
|
||||
We recommend using the latest version of Python. Jinja supports Python
|
||||
3.8 and newer. We also recommend using a `virtual environment`_ in order
|
||||
3.10 and newer. We also recommend using a `virtual environment`_ in order
|
||||
to isolate your project dependencies from other projects and the system.
|
||||
|
||||
.. _virtual environment: https://packaging.python.org/tutorials/installing-packages/#creating-virtual-environments
|
||||
|
||||
@ -55,6 +55,17 @@ Foo
|
||||
>>> print(result.value)
|
||||
15
|
||||
|
||||
Sandboxed Native Environment
|
||||
----------------------------
|
||||
|
||||
You can combine :class:`.SandboxedEnvironment` and :class:`NativeEnvironment` to
|
||||
get both behaviors.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class SandboxedNativeEnvironment(SandboxedEnvironment, NativeEnvironment):
|
||||
pass
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
|
||||
@ -202,10 +202,11 @@ option can also be set to strip tabs and spaces from the beginning of a
|
||||
line to the start of a block. (Nothing will be stripped if there are
|
||||
other characters before the start of the block.)
|
||||
|
||||
With both `trim_blocks` and `lstrip_blocks` enabled, you can put block tags
|
||||
on their own lines, and the entire block line will be removed when
|
||||
rendered, preserving the whitespace of the contents. For example,
|
||||
without the `trim_blocks` and `lstrip_blocks` options, this template::
|
||||
With both ``trim_blocks`` and ``lstrip_blocks`` disabled (the default), block
|
||||
tags on their own lines will be removed, but a blank line will remain and the
|
||||
spaces in the content will be preserved. For example, this template:
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
<div>
|
||||
{% if True %}
|
||||
@ -213,7 +214,10 @@ without the `trim_blocks` and `lstrip_blocks` options, this template::
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
gets rendered with blank lines inside the div::
|
||||
With both ``trim_blocks`` and ``lstrip_blocks`` disabled, the template is
|
||||
rendered with blank lines inside the div:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
<div>
|
||||
|
||||
@ -221,8 +225,10 @@ gets rendered with blank lines inside the div::
|
||||
|
||||
</div>
|
||||
|
||||
But with both `trim_blocks` and `lstrip_blocks` enabled, the template block
|
||||
lines are removed and other whitespace is preserved::
|
||||
With both ``trim_blocks`` and ``lstrip_blocks`` enabled, the template block
|
||||
lines are completely removed:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
<div>
|
||||
yay
|
||||
@ -522,8 +528,8 @@ However, the name after the `endblock` word must match the block name.
|
||||
Block Nesting and Scope
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Blocks can be nested for more complex layouts. However, per default blocks
|
||||
may not access variables from outer scopes::
|
||||
Blocks can be nested for more complex layouts. By default, a block may not
|
||||
access variables from outside the block (outer scopes)::
|
||||
|
||||
{% for item in seq %}
|
||||
<li>{% block loop_item %}{{ item }}{% endblock %}</li>
|
||||
@ -1080,34 +1086,34 @@ Assignments use the `set` tag and can have multiple targets::
|
||||
Block Assignments
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 2.8
|
||||
It's possible to use `set` as a block to assign the content of the block to a
|
||||
variable. This can be used to create multi-line strings, since Jinja doesn't
|
||||
support Python's triple quotes (``"""``, ``'''``).
|
||||
|
||||
Starting with Jinja 2.8, it's possible to also use block assignments to
|
||||
capture the contents of a block into a variable name. This can be useful
|
||||
in some situations as an alternative for macros. In that case, instead of
|
||||
using an equals sign and a value, you just write the variable name and then
|
||||
everything until ``{% endset %}`` is captured.
|
||||
Instead of using an equals sign and a value, you only write the variable name,
|
||||
and everything until ``{% endset %}`` is captured.
|
||||
|
||||
Example::
|
||||
.. code-block:: jinja
|
||||
|
||||
{% set navigation %}
|
||||
<li><a href="/">Index</a>
|
||||
<li><a href="/downloads">Downloads</a>
|
||||
{% endset %}
|
||||
|
||||
The `navigation` variable then contains the navigation HTML source.
|
||||
Filters applied to the variable name will be applied to the block's content.
|
||||
|
||||
.. versionchanged:: 2.10
|
||||
|
||||
Starting with Jinja 2.10, the block assignment supports filters.
|
||||
|
||||
Example::
|
||||
.. code-block:: jinja
|
||||
|
||||
{% set reply | wordwrap %}
|
||||
You wrote:
|
||||
{{ message }}
|
||||
{% endset %}
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
.. versionchanged:: 2.10
|
||||
|
||||
Block assignment supports filters.
|
||||
|
||||
.. _extends:
|
||||
|
||||
@ -1406,27 +1412,31 @@ Comparisons
|
||||
Logic
|
||||
~~~~~
|
||||
|
||||
For ``if`` statements, ``for`` filtering, and ``if`` expressions, it can be useful to
|
||||
combine multiple expressions:
|
||||
For ``if`` statements, ``for`` filtering, and ``if`` expressions, it can be
|
||||
useful to combine multiple expressions.
|
||||
|
||||
``and``
|
||||
Return true if the left and the right operand are true.
|
||||
For ``x and y``, if ``x`` is false, then the value is ``x``, else ``y``. In
|
||||
a boolean context, this will be treated as ``True`` if both operands are
|
||||
truthy.
|
||||
|
||||
``or``
|
||||
Return true if the left or the right operand are true.
|
||||
For ``x or y``, if ``x`` is true, then the value is ``x``, else ``y``. In a
|
||||
boolean context, this will be treated as ``True`` if at least one operand is
|
||||
truthy.
|
||||
|
||||
``not``
|
||||
negate a statement (see below).
|
||||
For ``not x``, if ``x`` is false, then the value is ``True``, else
|
||||
``False``.
|
||||
|
||||
Prefer negating ``is`` and ``in`` using their infix notation:
|
||||
``foo is not bar`` instead of ``not foo is bar``; ``foo not in bar`` instead
|
||||
of ``not foo in bar``. All other expressions require prefix notation:
|
||||
``not (foo and bar).``
|
||||
|
||||
``(expr)``
|
||||
Parentheses group an expression.
|
||||
|
||||
.. admonition:: Note
|
||||
|
||||
The ``is`` and ``in`` operators support negation using an infix notation,
|
||||
too: ``foo is not bar`` and ``foo not in bar`` instead of ``not foo is bar``
|
||||
and ``not foo in bar``. All other expressions require a prefix notation:
|
||||
``not (foo and bar).``
|
||||
Parentheses group an expression. This is used to change evaluation order, or
|
||||
to make a long expression easier to read or less ambiguous.
|
||||
|
||||
|
||||
Other Operators
|
||||
@ -1668,6 +1678,9 @@ The following functions are available in the global scope by default:
|
||||
|
||||
.. versionadded:: 2.10
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
Namespace attributes can be assigned to in multiple assignment.
|
||||
|
||||
|
||||
Extensions
|
||||
----------
|
||||
@ -1778,7 +1791,7 @@ It's possible to translate strings in expressions with these functions:
|
||||
|
||||
- ``_(message)``: Alias for ``gettext``.
|
||||
- ``gettext(message)``: Translate a message.
|
||||
- ``ngettext(singluar, plural, n)``: Translate a singular or plural
|
||||
- ``ngettext(singular, plural, n)``: Translate a singular or plural
|
||||
message based on a count variable.
|
||||
- ``pgettext(context, message)``: Like ``gettext()``, but picks the
|
||||
translation based on the context string.
|
||||
|
||||
@ -21,7 +21,7 @@ for a neat trick.
|
||||
Usually child templates extend from one template that adds a basic HTML
|
||||
skeleton. However it's possible to put the `extends` tag into an `if` tag to
|
||||
only extend from the layout template if the `standalone` variable evaluates
|
||||
to false which it does per default if it's not defined. Additionally a very
|
||||
to false, which it does by default if it's not defined. Additionally a very
|
||||
basic skeleton is added to the file so that if it's indeed rendered with
|
||||
`standalone` set to `True` a very basic HTML skeleton is added::
|
||||
|
||||
|
||||
@ -6,9 +6,9 @@ env = Environment(
|
||||
{
|
||||
"child.html": """\
|
||||
{% extends default_layout or 'default.html' %}
|
||||
{% include helpers = 'helpers.html' %}
|
||||
{% import 'helpers.html' as helpers %}
|
||||
{% macro get_the_answer() %}42{% endmacro %}
|
||||
{% title = 'Hello World' %}
|
||||
{% set title = 'Hello World' %}
|
||||
{% block body %}
|
||||
{{ get_the_answer() }}
|
||||
{{ helpers.conspirate() }}
|
||||
|
||||
@ -1,99 +0,0 @@
|
||||
parser grammar JinjaGrammar;
|
||||
|
||||
options {
|
||||
tokenVocab=JinjaLexer;
|
||||
}
|
||||
|
||||
start : expressions;
|
||||
|
||||
expression
|
||||
: inline_statement
|
||||
| block_statement
|
||||
;
|
||||
|
||||
expressions : expression*;
|
||||
|
||||
list_literal : LSQB SP? list_literal_values? SP? RSQB;
|
||||
list_literal_values
|
||||
:
|
||||
(list_literal_value SP? COMMA SP?)*
|
||||
list_literal_value
|
||||
;
|
||||
list_literal_value
|
||||
: STRING_LITERAL
|
||||
| variable_name
|
||||
;
|
||||
|
||||
variable_name : IDENTIFIER;
|
||||
|
||||
statement_block
|
||||
:
|
||||
STATEMENT_ID_BLOCK SP IDENTIFIER
|
||||
;
|
||||
|
||||
statement_include_template
|
||||
: STRING_LITERAL
|
||||
| list_literal
|
||||
| variable_name
|
||||
;
|
||||
|
||||
statement_include_context
|
||||
: STATEMENT_INCLUDE_WITH_CONTEXT
|
||||
| STATEMENT_INCLUDE_WITHOUT_CONTEXT
|
||||
;
|
||||
|
||||
statement_include
|
||||
: STATEMENT_ID_INCLUDE
|
||||
(SP statement_include_template)
|
||||
(SP STATEMENT_INCLUDE_IGNORE_MISSING)?
|
||||
(SP statement_include_context)?
|
||||
;
|
||||
|
||||
statement_import_file
|
||||
: STRING_LITERAL
|
||||
| variable_name
|
||||
;
|
||||
|
||||
statement_import_variable
|
||||
: variable_name (SP STATEMENT_ID_IMPORT_AS SP variable_name)?
|
||||
;
|
||||
|
||||
statement_import_variable_list
|
||||
: (statement_import_variable SP? COMMA SP?)* statement_import_variable
|
||||
;
|
||||
|
||||
statement_import
|
||||
: STATEMENT_ID_IMPORT SP statement_import_file SP STATEMENT_ID_IMPORT_AS SP variable_name (SP statement_include_context)?
|
||||
| STATEMENT_ID_FROM SP statement_import_file SP STATEMENT_ID_IMPORT SP statement_import_variable_list (SP statement_include_context)?
|
||||
;
|
||||
|
||||
block_end_statement_id
|
||||
: STATEMENT_END_ID_BLOCK
|
||||
| STATEMENT_END_ID_SET
|
||||
;
|
||||
|
||||
// block_statement_with_parameters
|
||||
// : block_statement_id
|
||||
// | block_statement_id
|
||||
// ;
|
||||
|
||||
block_statement_without_parameters
|
||||
: statement_block
|
||||
;
|
||||
|
||||
block_statement_start_content
|
||||
: block_statement_without_parameters
|
||||
// | block_statement_with_parameters
|
||||
;
|
||||
|
||||
inline_statement_content
|
||||
: statement_include
|
||||
| statement_import
|
||||
;
|
||||
|
||||
inline_statement : STATEMENT_OPEN inline_statement_content STATEMENT_CLOSE;
|
||||
|
||||
block_statement_start : STATEMENT_OPEN block_statement_start_content STATEMENT_CLOSE;
|
||||
block_statement_end : STATEMENT_OPEN block_end_statement_id STATEMENT_CLOSE;
|
||||
|
||||
block_statement : block_statement_start expressions block_statement_end;
|
||||
@ -1,100 +0,0 @@
|
||||
lexer grammar JinjaLexer;
|
||||
|
||||
TRUE_LOWER : 'true';
|
||||
TRUE_PY : 'True';
|
||||
TRUE
|
||||
: TRUE_LOWER
|
||||
| TRUE_PY
|
||||
;
|
||||
|
||||
FALSE_LOWER : 'false';
|
||||
FALSE_PY : 'False';
|
||||
FALSE
|
||||
: FALSE_LOWER
|
||||
| FALSE_PY
|
||||
;
|
||||
|
||||
BOOLEAN
|
||||
: TRUE
|
||||
| FALSE
|
||||
;
|
||||
|
||||
NONE_LOWER : 'none';
|
||||
NONE_PY : 'None';
|
||||
NONE
|
||||
: NONE_LOWER
|
||||
| NONE_PY
|
||||
;
|
||||
|
||||
LPAR : '(';
|
||||
LSQB : '[';
|
||||
LBRACE : '{';
|
||||
RPAR : ')';
|
||||
RSQB : ']';
|
||||
RBRACE : '}';
|
||||
DOT : '.';
|
||||
COLON : ':';
|
||||
COMMA : ',';
|
||||
SEMI : ';';
|
||||
PLUS : '+';
|
||||
MINUS : '-';
|
||||
STAR : '*';
|
||||
SLASH : '/';
|
||||
VBAR : '|';
|
||||
AMPER : '&';
|
||||
LESS : '<';
|
||||
GREATER : '>';
|
||||
EQUAL : '=';
|
||||
PERCENT : '%';
|
||||
EQEQUAL : '==';
|
||||
NOTEQUAL : '!=';
|
||||
LESSEQUAL : '<=';
|
||||
GREATEREQUAL : '>=';
|
||||
TILDE : '~';
|
||||
CIRCUMFLEX : '^';
|
||||
LEFTSHIFT : '<<';
|
||||
RIGHTSHIFT : '>>';
|
||||
DOUBLESTAR : '**';
|
||||
DOUBLESLASH : '//';
|
||||
AT : '@';
|
||||
RARROW : '->';
|
||||
ELLIPSIS : '...';
|
||||
EXCLAMATION : '!';
|
||||
|
||||
STATEMENT_OPEN : '{%' SP?;
|
||||
STATEMENT_CLOSE : SP? '%}';
|
||||
|
||||
EXPRESSION_OPEN : '{{';
|
||||
EXPRESSION_CLOSE : '}}';
|
||||
|
||||
COMMENT_OPEN : '{#';
|
||||
COMMENT_CLOSE : '#}';
|
||||
|
||||
STRING_LITERAL : STRING_LITERAL_SINGLE_QUOTE | STRING_LITERAL_DOUBLE_QUOTE;
|
||||
fragment STRING_LITERAL_SINGLE_QUOTE : '\'' (~[\\\r\n'])* '\'';
|
||||
fragment STRING_LITERAL_DOUBLE_QUOTE : '"' (~[\\\r\n"])* '"';
|
||||
|
||||
SP : [ \t\f]+;
|
||||
|
||||
// Statement identifiers for built-in statements
|
||||
|
||||
STATEMENT_ID_BLOCK : 'block';
|
||||
STATEMENT_END_ID_BLOCK : 'endblock';
|
||||
STATEMENT_ID_FROM : 'from';
|
||||
STATEMENT_ID_IMPORT : 'import';
|
||||
STATEMENT_ID_INCLUDE : 'include';
|
||||
STATEMENT_ID_RAW : 'raw';
|
||||
STATEMENT_ID_SET : 'set';
|
||||
STATEMENT_END_ID_SET : 'endset';
|
||||
|
||||
STATEMENT_ID_IMPORT_AS : 'as';
|
||||
|
||||
STATEMENT_INCLUDE_IGNORE_MISSING : 'ignore missing';
|
||||
STATEMENT_INCLUDE_WITH_CONTEXT : 'with context';
|
||||
STATEMENT_INCLUDE_WITHOUT_CONTEXT : 'without context';
|
||||
|
||||
END_STATEMENT_ID_PREFIX : 'end';
|
||||
|
||||
IDENTIFIER : IDENTIFIER_START IDENTIFIER_CONTINUE*;
|
||||
fragment IDENTIFIER_START : [a-zA-Z_];
|
||||
fragment IDENTIFIER_CONTINUE : [a-zA-Z0-0_];
|
||||
150
pyproject.toml
150
pyproject.toml
@ -1,36 +1,65 @@
|
||||
[project]
|
||||
name = "Jinja2"
|
||||
version = "3.2.0.dev"
|
||||
description = "A very fast and expressive template engine."
|
||||
readme = "README.md"
|
||||
license = {file = "LICENSE.txt"}
|
||||
license = "BSD-3-Clause"
|
||||
license-files = ["LICENSE.txt"]
|
||||
maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: BSD License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||
"Topic :: Text Processing :: Markup :: HTML",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["MarkupSafe>=2.0"]
|
||||
dynamic = ["version"]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = ["MarkupSafe>=3.0"]
|
||||
|
||||
[project.urls]
|
||||
Donate = "https://palletsprojects.com/donate"
|
||||
Documentation = "https://jinja.palletsprojects.com/"
|
||||
Changes = "https://jinja.palletsprojects.com/changes/"
|
||||
Changes = "https://jinja.palletsprojects.com/page/changes/"
|
||||
Source = "https://github.com/pallets/jinja/"
|
||||
Chat = "https://discord.gg/pallets"
|
||||
|
||||
[project.optional-dependencies]
|
||||
i18n = ["Babel>=2.7"]
|
||||
i18n = ["Babel>=2.17"]
|
||||
|
||||
[project.entry-points."babel.extractors"]
|
||||
jinja2 = "jinja2.ext:babel_extract[i18n]"
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff",
|
||||
"tox",
|
||||
"tox-uv",
|
||||
]
|
||||
docs = [
|
||||
"pallets-sphinx-themes",
|
||||
"sphinx",
|
||||
"sphinxcontrib-log-cabinet",
|
||||
]
|
||||
docs-auto = [
|
||||
"sphinx-autobuild",
|
||||
]
|
||||
gha-update = [
|
||||
"gha-update ; python_full_version >= '3.12'",
|
||||
]
|
||||
pre-commit = [
|
||||
"pre-commit",
|
||||
"pre-commit-uv",
|
||||
]
|
||||
tests = [
|
||||
"pytest",
|
||||
"pytest-timeout",
|
||||
"trio"
|
||||
]
|
||||
typing = [
|
||||
"mypy",
|
||||
"pyright",
|
||||
"pytest",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["flit_core<4"]
|
||||
@ -43,15 +72,17 @@ name = "jinja2"
|
||||
include = [
|
||||
"docs/",
|
||||
"examples/",
|
||||
"requirements/",
|
||||
"tests/",
|
||||
"CHANGES.md",
|
||||
"tox.ini",
|
||||
"CHANGES.rst",
|
||||
"uv.lock"
|
||||
]
|
||||
exclude = [
|
||||
"docs/_build/",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
default-groups = ["dev", "pre-commit", "tests", "typing"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
filterwarnings = [
|
||||
@ -65,19 +96,24 @@ source = ["jinja2", "tests"]
|
||||
[tool.coverage.paths]
|
||||
source = ["src", "*/site-packages"]
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_also = [
|
||||
"if t.TYPE_CHECKING",
|
||||
"raise NotImplementedError",
|
||||
": \\.{3}",
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.8"
|
||||
files = ["src/jinja2"]
|
||||
python_version = "3.10"
|
||||
files = ["src"]
|
||||
show_error_codes = true
|
||||
pretty = true
|
||||
strict = true
|
||||
local_partial_types = true
|
||||
warn_unreachable = true
|
||||
|
||||
[tool.pyright]
|
||||
pythonVersion = "3.8"
|
||||
include = ["src/jinja2"]
|
||||
typeCheckingMode = "basic"
|
||||
pythonVersion = "3.10"
|
||||
include = ["src"]
|
||||
typeCheckingMode = "standard"
|
||||
|
||||
[tool.ruff]
|
||||
src = ["src"]
|
||||
@ -94,8 +130,82 @@ select = [
|
||||
"UP", # pyupgrade
|
||||
"W", # pycodestyle warning
|
||||
]
|
||||
ignore-init-module-imports = true
|
||||
ignore = [
|
||||
"UP038", # keep isinstance tuple
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
force-single-line = true
|
||||
order-by-type = false
|
||||
|
||||
[tool.gha-update]
|
||||
tag-only = [
|
||||
"slsa-framework/slsa-github-generator",
|
||||
]
|
||||
|
||||
[tool.tox]
|
||||
env_list = [
|
||||
"py3.13", "py3.12", "py3.11", "py3.10",
|
||||
"pypy3.11",
|
||||
"style",
|
||||
"typing",
|
||||
"docs",
|
||||
]
|
||||
|
||||
[tool.tox.env_run_base]
|
||||
description = "pytest on latest dependency versions"
|
||||
runner = "uv-venv-lock-runner"
|
||||
package = "wheel"
|
||||
wheel_build_env = ".pkg"
|
||||
constrain_package_deps = true
|
||||
use_frozen_constraints = true
|
||||
dependency_groups = ["tests"]
|
||||
commands = [[
|
||||
"pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}",
|
||||
{replace = "posargs", default = [], extend = true},
|
||||
]]
|
||||
|
||||
[tool.tox.env.style]
|
||||
description = "run all pre-commit hooks on all files"
|
||||
dependency_groups = ["pre-commit"]
|
||||
skip_install = true
|
||||
commands = [["pre-commit", "run", "--all-files"]]
|
||||
|
||||
[tool.tox.env.typing]
|
||||
description = "run static type checkers"
|
||||
dependency_groups = ["typing"]
|
||||
commands = [
|
||||
["mypy"],
|
||||
]
|
||||
|
||||
[tool.tox.env.docs]
|
||||
description = "build docs"
|
||||
dependency_groups = ["docs"]
|
||||
commands = [["sphinx-build", "-E", "-W", "-b", "dirhtml", "docs", "docs/_build/dirhtml"]]
|
||||
|
||||
[tool.tox.env.docs-auto]
|
||||
description = "continuously rebuild docs and start a local server"
|
||||
dependency_groups = ["docs", "docs-auto"]
|
||||
commands = [["sphinx-autobuild", "-W", "-b", "dirhtml", "--watch", "src", "docs", "docs/_build/dirhtml"]]
|
||||
|
||||
[tool.tox.env.update-actions]
|
||||
description = "update GitHub Actions pins"
|
||||
labels = ["update"]
|
||||
dependency_groups = ["gha-update"]
|
||||
skip_install = true
|
||||
commands = [["gha-update"]]
|
||||
|
||||
[tool.tox.env.update-pre_commit]
|
||||
description = "update pre-commit pins"
|
||||
labels = ["update"]
|
||||
dependency_groups = ["pre-commit"]
|
||||
skip_install = true
|
||||
commands = [["pre-commit", "autoupdate", "--freeze", "-j4"]]
|
||||
|
||||
[tool.tox.env.update-requirements]
|
||||
description = "update uv lock"
|
||||
labels = ["update"]
|
||||
dependency_groups = []
|
||||
no_default_groups = true
|
||||
skip_install = true
|
||||
commands = [["uv", "lock", {replace = "posargs", default = ["-U"], extend = true}]]
|
||||
|
||||
@ -1 +0,0 @@
|
||||
build
|
||||
@ -1,12 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile build.in
|
||||
#
|
||||
build==1.2.1
|
||||
# via -r build.in
|
||||
packaging==24.0
|
||||
# via build
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
@ -1,5 +0,0 @@
|
||||
-r docs.txt
|
||||
-r tests.txt
|
||||
-r typing.txt
|
||||
pre-commit
|
||||
tox
|
||||
@ -1,148 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile dev.in
|
||||
#
|
||||
alabaster==0.7.16
|
||||
# via sphinx
|
||||
attrs==23.2.0
|
||||
# via
|
||||
# outcome
|
||||
# trio
|
||||
babel==2.15.0
|
||||
# via sphinx
|
||||
build==1.2.1
|
||||
# via pip-tools
|
||||
cachetools==5.3.3
|
||||
# via tox
|
||||
certifi==2024.2.2
|
||||
# via requests
|
||||
cfgv==3.4.0
|
||||
# via pre-commit
|
||||
chardet==5.2.0
|
||||
# via tox
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
click==8.1.7
|
||||
# via
|
||||
# pip-compile-multi
|
||||
# pip-tools
|
||||
colorama==0.4.6
|
||||
# via tox
|
||||
distlib==0.3.8
|
||||
# via virtualenv
|
||||
docutils==0.21.2
|
||||
# via sphinx
|
||||
filelock==3.14.0
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
identify==2.5.36
|
||||
# via pre-commit
|
||||
idna==3.7
|
||||
# via
|
||||
# requests
|
||||
# trio
|
||||
imagesize==1.4.1
|
||||
# via sphinx
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
jinja2==3.1.4
|
||||
# via sphinx
|
||||
markupsafe==2.1.5
|
||||
# via jinja2
|
||||
mypy==1.10.0
|
||||
# via -r typing.in
|
||||
mypy-extensions==1.0.0
|
||||
# via mypy
|
||||
nodeenv==1.8.0
|
||||
# via pre-commit
|
||||
outcome==1.3.0.post0
|
||||
# via trio
|
||||
packaging==24.0
|
||||
# via
|
||||
# build
|
||||
# pallets-sphinx-themes
|
||||
# pyproject-api
|
||||
# pytest
|
||||
# sphinx
|
||||
# tox
|
||||
pallets-sphinx-themes==2.1.3
|
||||
# via -r docs.in
|
||||
pip-compile-multi==2.6.3
|
||||
# via -r dev.in
|
||||
pip-tools==7.4.1
|
||||
# via pip-compile-multi
|
||||
platformdirs==4.2.1
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
pluggy==1.5.0
|
||||
# via
|
||||
# pytest
|
||||
# tox
|
||||
pre-commit==3.7.1
|
||||
# via -r dev.in
|
||||
pygments==2.18.0
|
||||
# via sphinx
|
||||
pyproject-api==1.6.1
|
||||
# via tox
|
||||
pyproject-hooks==1.1.0
|
||||
# via
|
||||
# build
|
||||
# pip-tools
|
||||
pytest==8.2.0
|
||||
# via -r tests.in
|
||||
pyyaml==6.0.1
|
||||
# via pre-commit
|
||||
requests==2.31.0
|
||||
# via sphinx
|
||||
sniffio==1.3.1
|
||||
# via trio
|
||||
snowballstemmer==2.2.0
|
||||
# via sphinx
|
||||
sortedcontainers==2.4.0
|
||||
# via trio
|
||||
sphinx==7.3.7
|
||||
# via
|
||||
# -r docs.in
|
||||
# pallets-sphinx-themes
|
||||
# sphinx-issues
|
||||
# sphinxcontrib-log-cabinet
|
||||
sphinx-issues==4.1.0
|
||||
# via -r docs.in
|
||||
sphinxcontrib-applehelp==1.0.8
|
||||
# via sphinx
|
||||
sphinxcontrib-devhelp==1.0.6
|
||||
# via sphinx
|
||||
sphinxcontrib-htmlhelp==2.0.5
|
||||
# via sphinx
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
# via sphinx
|
||||
sphinxcontrib-log-cabinet==1.0.1
|
||||
# via -r docs.in
|
||||
sphinxcontrib-qthelp==1.0.7
|
||||
# via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.10
|
||||
# via sphinx
|
||||
toposort==1.10
|
||||
# via pip-compile-multi
|
||||
tox==4.15.0
|
||||
# via -r dev.in
|
||||
trio==0.25.0
|
||||
# via -r tests.in
|
||||
typing-extensions==4.11.0
|
||||
# via mypy
|
||||
urllib3==2.2.1
|
||||
# via requests
|
||||
virtualenv==20.26.1
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
wheel==0.43.0
|
||||
# via pip-tools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
# setuptools
|
||||
@ -1,3 +0,0 @@
|
||||
pallets-sphinx-themes
|
||||
sphinx
|
||||
sphinxcontrib-log-cabinet
|
||||
@ -1,60 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile docs.in
|
||||
#
|
||||
alabaster==0.7.16
|
||||
# via sphinx
|
||||
babel==2.15.0
|
||||
# via sphinx
|
||||
certifi==2024.2.2
|
||||
# via requests
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
docutils==0.21.2
|
||||
# via sphinx
|
||||
idna==3.7
|
||||
# via requests
|
||||
imagesize==1.4.1
|
||||
# via sphinx
|
||||
jinja2==3.1.4
|
||||
# via sphinx
|
||||
markupsafe==2.1.5
|
||||
# via jinja2
|
||||
packaging==24.0
|
||||
# via
|
||||
# pallets-sphinx-themes
|
||||
# sphinx
|
||||
pallets-sphinx-themes==2.1.3
|
||||
# via -r docs.in
|
||||
pygments==2.18.0
|
||||
# via sphinx
|
||||
requests==2.31.0
|
||||
# via sphinx
|
||||
snowballstemmer==2.2.0
|
||||
# via sphinx
|
||||
sphinx==7.3.7
|
||||
# via
|
||||
# -r docs.in
|
||||
# pallets-sphinx-themes
|
||||
# sphinx-issues
|
||||
# sphinxcontrib-log-cabinet
|
||||
sphinx-issues==4.1.0
|
||||
# via -r docs.in
|
||||
sphinxcontrib-applehelp==1.0.8
|
||||
# via sphinx
|
||||
sphinxcontrib-devhelp==1.0.6
|
||||
# via sphinx
|
||||
sphinxcontrib-htmlhelp==2.0.5
|
||||
# via sphinx
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
# via sphinx
|
||||
sphinxcontrib-log-cabinet==1.0.1
|
||||
# via -r docs.in
|
||||
sphinxcontrib-qthelp==1.0.7
|
||||
# via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.10
|
||||
# via sphinx
|
||||
urllib3==2.2.1
|
||||
# via requests
|
||||
@ -1,2 +0,0 @@
|
||||
pytest
|
||||
trio
|
||||
@ -1,28 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile tests.in
|
||||
#
|
||||
attrs==23.2.0
|
||||
# via
|
||||
# outcome
|
||||
# trio
|
||||
idna==3.7
|
||||
# via trio
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
outcome==1.3.0.post0
|
||||
# via trio
|
||||
packaging==24.0
|
||||
# via pytest
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
pytest==8.2.0
|
||||
# via -r tests.in
|
||||
sniffio==1.3.1
|
||||
# via trio
|
||||
sortedcontainers==2.4.0
|
||||
# via trio
|
||||
trio==0.25.0
|
||||
# via -r tests.in
|
||||
@ -1,2 +0,0 @@
|
||||
pytest
|
||||
trio==0.22.2
|
||||
@ -1,43 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.7
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile tests37.in
|
||||
#
|
||||
attrs==23.2.0
|
||||
# via
|
||||
# outcome
|
||||
# trio
|
||||
exceptiongroup==1.2.1
|
||||
# via
|
||||
# pytest
|
||||
# trio
|
||||
idna==3.7
|
||||
# via trio
|
||||
importlib-metadata==6.7.0
|
||||
# via
|
||||
# attrs
|
||||
# pluggy
|
||||
# pytest
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
outcome==1.3.0.post0
|
||||
# via trio
|
||||
packaging==24.0
|
||||
# via pytest
|
||||
pluggy==1.2.0
|
||||
# via pytest
|
||||
pytest==7.4.4
|
||||
# via -r tests37.in
|
||||
sniffio==1.3.1
|
||||
# via trio
|
||||
sortedcontainers==2.4.0
|
||||
# via trio
|
||||
tomli==2.0.1
|
||||
# via pytest
|
||||
trio==0.22.2
|
||||
# via -r tests37.in
|
||||
typing-extensions==4.7.1
|
||||
# via importlib-metadata
|
||||
zipp==3.15.0
|
||||
# via importlib-metadata
|
||||
@ -1,3 +0,0 @@
|
||||
mypy
|
||||
pyright
|
||||
pytest
|
||||
@ -1,12 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile typing.in
|
||||
#
|
||||
mypy==1.10.0
|
||||
# via -r typing.in
|
||||
mypy-extensions==1.0.0
|
||||
# via mypy
|
||||
typing-extensions==4.11.0
|
||||
# via mypy
|
||||
@ -54,17 +54,16 @@ def build_pattern(ranges):
|
||||
|
||||
|
||||
def main():
|
||||
"""Build the regex pattern and write it to
|
||||
``jinja2/_identifier.py``.
|
||||
"""
|
||||
"""Build the regex pattern and write it to ``jinja2/_identifier.py``."""
|
||||
pattern = build_pattern(collapse_ranges(get_characters()))
|
||||
filename = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "src", "jinja2", "_identifier.py")
|
||||
)
|
||||
|
||||
with open(filename, "w", encoding="utf8") as f:
|
||||
f.write("# generated by scripts/generate_identifier_pattern.py")
|
||||
f.write(f"# Python {sys.version_info[0]}.{sys.version_info[1]}\n")
|
||||
f.write("import re\n\n")
|
||||
f.write("# generated by scripts/generate_identifier_pattern.py\n")
|
||||
f.write("pattern = re.compile(\n")
|
||||
f.write(f' r"[\\w{pattern}]+" # noqa: B950\n')
|
||||
f.write(")\n")
|
||||
|
||||
@ -3,6 +3,10 @@ non-XML syntax that supports inline expressions and an optional
|
||||
sandboxed environment.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
|
||||
from .bccache import BytecodeCache as BytecodeCache
|
||||
from .bccache import FileSystemBytecodeCache as FileSystemBytecodeCache
|
||||
from .bccache import MemcachedBytecodeCache as MemcachedBytecodeCache
|
||||
@ -35,4 +39,19 @@ from .utils import pass_environment as pass_environment
|
||||
from .utils import pass_eval_context as pass_eval_context
|
||||
from .utils import select_autoescape as select_autoescape
|
||||
|
||||
__version__ = "3.2.0.dev0"
|
||||
|
||||
def __getattr__(name: str) -> t.Any:
|
||||
if name == "__version__":
|
||||
import importlib.metadata
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"The `__version__` attribute is deprecated and will be removed in"
|
||||
" Jinja 3.3. Use feature detection or"
|
||||
' `importlib.metadata.version("jinja2")` instead.',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return importlib.metadata.version("jinja2")
|
||||
|
||||
raise AttributeError(name)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# generated by scripts/generate_identifier_pattern.py for Python 3.10
|
||||
import re
|
||||
|
||||
# generated by scripts/generate_identifier_pattern.py
|
||||
pattern = re.compile(
|
||||
r"[\w·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߽߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛࣓-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣ৾ਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣૺ-૿ଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఄా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഀ-ഃ഻഼ാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳷-᳹᷀-᷹᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꣿꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𐴤-𐽆𐴧-𐽐𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑄴𑅅𑅆𑅳𑆀-𑆂𑆳-𑇀𑇉-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌻𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑑞𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑠬-𑠺𑨁-𑨊𑨳-𑨹𑨻-𑨾𑩇𑩑-𑩛𑪊-𑪙𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𑴱-𑴶𑴺𑴼𑴽𑴿-𑵅𑵇𑶊-𑶎𑶐𑶑𑶓-𑶗𑻳-𑻶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯]+" # noqa: B950
|
||||
r"[\w·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߽߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛࣓-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣ৾ਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣૺ-૿ଁ-ଃ଼ା-ୄେୈୋ-୍୕-ୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఄా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഀ-ഃ഻഼ാ-ൄെ-ൈൊ-്ൗൢൣඁ-ඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᪿᫀᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭᳴᳷-᳹᷀-᷹᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧ꠬ꢀꢁꢴ-ꣅ꣠-꣱ꣿꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𐴤-𐽆𐴧𐺫𐺬-𐽐𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑄴𑅅𑅆𑅳𑆀-𑆂𑆳-𑇀𑇉-𑇌𑇎𑇏𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌻𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑑞𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑠬-𑠺𑤰-𑤵𑤷𑤸𑤻-𑤾𑥀𑥂𑥃𑧑-𑧗𑧚-𑧠𑧤𑨁-𑨊𑨳-𑨹𑨻-𑨾𑩇𑩑-𑩛𑪊-𑪙𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𑴱-𑴶𑴺𑴼𑴽𑴿-𑵅𑵇𑶊-𑶎𑶐𑶑𑶓-𑶗𑻳-𑻶𖫰-𖫴𖬰-𖬶𖽏𖽑-𖾇𖾏-𖾒𖿤𖿰𖿱𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞀪𞄰-𞄶𞋬-𞣐𞋯-𞣖𞥄-𞥊󠄀-󠇯]+" # noqa: B950
|
||||
)
|
||||
|
||||
@ -67,7 +67,7 @@ async def auto_await(value: t.Union[t.Awaitable["V"], "V"]) -> "V":
|
||||
if inspect.isawaitable(value):
|
||||
return await t.cast("t.Awaitable[V]", value)
|
||||
|
||||
return t.cast("V", value)
|
||||
return value
|
||||
|
||||
|
||||
class _IteratorToAsyncIterator(t.Generic[V]):
|
||||
@ -85,7 +85,7 @@ class _IteratorToAsyncIterator(t.Generic[V]):
|
||||
|
||||
|
||||
def auto_aiter(
|
||||
iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||
iterable: "t.AsyncIterable[V] | t.Iterable[V]",
|
||||
) -> "t.AsyncIterator[V]":
|
||||
if hasattr(iterable, "__aiter__"):
|
||||
return iterable.__aiter__()
|
||||
@ -94,6 +94,6 @@ def auto_aiter(
|
||||
|
||||
|
||||
async def auto_to_list(
|
||||
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||
) -> t.List["V"]:
|
||||
value: "t.AsyncIterable[V] | t.Iterable[V]",
|
||||
) -> list["V"]:
|
||||
return [x async for x in auto_aiter(value)]
|
||||
|
||||
@ -27,9 +27,7 @@ if t.TYPE_CHECKING:
|
||||
class _MemcachedClient(te.Protocol):
|
||||
def get(self, key: str) -> bytes: ...
|
||||
|
||||
def set(
|
||||
self, key: str, value: bytes, timeout: t.Optional[int] = None
|
||||
) -> None: ...
|
||||
def set(self, key: str, value: bytes, timeout: int | None = None) -> None: ...
|
||||
|
||||
|
||||
bc_version = 5
|
||||
@ -60,7 +58,7 @@ class Bucket:
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Resets the bucket (unloads the bytecode)."""
|
||||
self.code: t.Optional[CodeType] = None
|
||||
self.code: CodeType | None = None
|
||||
|
||||
def load_bytecode(self, f: t.BinaryIO) -> None:
|
||||
"""Loads bytecode from a file or file like object."""
|
||||
@ -149,9 +147,7 @@ class BytecodeCache:
|
||||
by a particular environment.
|
||||
"""
|
||||
|
||||
def get_cache_key(
|
||||
self, name: str, filename: t.Optional[t.Union[str]] = None
|
||||
) -> str:
|
||||
def get_cache_key(self, name: str, filename: str | None = None) -> str:
|
||||
"""Returns the unique hash key for this template name."""
|
||||
hash = sha1(name.encode("utf-8"))
|
||||
|
||||
@ -168,7 +164,7 @@ class BytecodeCache:
|
||||
self,
|
||||
environment: "Environment",
|
||||
name: str,
|
||||
filename: t.Optional[str],
|
||||
filename: str | None,
|
||||
source: str,
|
||||
) -> Bucket:
|
||||
"""Return a cache bucket for the given template. All arguments are
|
||||
@ -204,7 +200,7 @@ class FileSystemBytecodeCache(BytecodeCache):
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, directory: t.Optional[str] = None, pattern: str = "__jinja2_%s.cache"
|
||||
self, directory: str | None = None, pattern: str = "__jinja2_%s.cache"
|
||||
) -> None:
|
||||
if directory is None:
|
||||
directory = self._get_default_cache_dir()
|
||||
@ -377,7 +373,7 @@ class MemcachedBytecodeCache(BytecodeCache):
|
||||
self,
|
||||
client: "_MemcachedClient",
|
||||
prefix: str = "jinja2/bytecode/",
|
||||
timeout: t.Optional[int] = None,
|
||||
timeout: int | None = None,
|
||||
ignore_memcache_errors: bool = True,
|
||||
):
|
||||
self.client = client
|
||||
|
||||
@ -101,12 +101,12 @@ def _make_unop(
|
||||
def generate(
|
||||
node: nodes.Template,
|
||||
environment: "Environment",
|
||||
name: t.Optional[str],
|
||||
filename: t.Optional[str],
|
||||
stream: t.Optional[t.TextIO] = None,
|
||||
name: str | None,
|
||||
filename: str | None,
|
||||
stream: t.TextIO | None = None,
|
||||
defer_init: bool = False,
|
||||
optimized: bool = True,
|
||||
) -> t.Optional[str]:
|
||||
) -> str | None:
|
||||
"""Generate the python source for a node tree."""
|
||||
if not isinstance(node, nodes.Template):
|
||||
raise TypeError("Can't compile non template nodes")
|
||||
@ -139,9 +139,7 @@ def has_safe_repr(value: t.Any) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def find_undeclared(
|
||||
nodes: t.Iterable[nodes.Node], names: t.Iterable[str]
|
||||
) -> t.Set[str]:
|
||||
def find_undeclared(nodes: t.Iterable[nodes.Node], names: t.Iterable[str]) -> set[str]:
|
||||
"""Check if the names passed are accessed undeclared. The return value
|
||||
is a set of all the undeclared names from the sequence of names found.
|
||||
"""
|
||||
@ -155,7 +153,7 @@ def find_undeclared(
|
||||
|
||||
|
||||
class MacroRef:
|
||||
def __init__(self, node: t.Union[nodes.Macro, nodes.CallBlock]) -> None:
|
||||
def __init__(self, node: nodes.Macro | nodes.CallBlock) -> None:
|
||||
self.node = node
|
||||
self.accesses_caller = False
|
||||
self.accesses_kwargs = False
|
||||
@ -169,7 +167,7 @@ class Frame:
|
||||
self,
|
||||
eval_ctx: EvalContext,
|
||||
parent: t.Optional["Frame"] = None,
|
||||
level: t.Optional[int] = None,
|
||||
level: int | None = None,
|
||||
) -> None:
|
||||
self.eval_ctx = eval_ctx
|
||||
|
||||
@ -187,10 +185,10 @@ class Frame:
|
||||
# this for example affects {% filter %} or {% macro %}. If a frame
|
||||
# is buffered this variable points to the name of the list used as
|
||||
# buffer.
|
||||
self.buffer: t.Optional[str] = None
|
||||
self.buffer: str | None = None
|
||||
|
||||
# the name of the block we're in, otherwise None.
|
||||
self.block: t.Optional[str] = None
|
||||
self.block: str | None = None
|
||||
|
||||
else:
|
||||
self.symbols = Symbols(parent.symbols, level=level)
|
||||
@ -216,7 +214,7 @@ class Frame:
|
||||
# or compile time.
|
||||
self.soft_frame = False
|
||||
|
||||
def copy(self) -> "Frame":
|
||||
def copy(self) -> "te.Self":
|
||||
"""Create a copy of the current one."""
|
||||
rv = object.__new__(self.__class__)
|
||||
rv.__dict__.update(self.__dict__)
|
||||
@ -229,7 +227,7 @@ class Frame:
|
||||
return Frame(self.eval_ctx, level=self.symbols.level + 1)
|
||||
return Frame(self.eval_ctx, self)
|
||||
|
||||
def soft(self) -> "Frame":
|
||||
def soft(self) -> "te.Self":
|
||||
"""Return a soft frame. A soft frame may not be modified as
|
||||
standalone thing as it shares the resources with the frame it
|
||||
was created of, but it's not a rootlevel frame any longer.
|
||||
@ -253,8 +251,8 @@ class DependencyFinderVisitor(NodeVisitor):
|
||||
"""A visitor that collects filter and test calls."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.filters: t.Set[str] = set()
|
||||
self.tests: t.Set[str] = set()
|
||||
self.filters: set[str] = set()
|
||||
self.tests: set[str] = set()
|
||||
|
||||
def visit_Filter(self, node: nodes.Filter) -> None:
|
||||
self.generic_visit(node)
|
||||
@ -276,7 +274,7 @@ class UndeclaredNameVisitor(NodeVisitor):
|
||||
|
||||
def __init__(self, names: t.Iterable[str]) -> None:
|
||||
self.names = set(names)
|
||||
self.undeclared: t.Set[str] = set()
|
||||
self.undeclared: set[str] = set()
|
||||
|
||||
def visit_Name(self, node: nodes.Name) -> None:
|
||||
if node.ctx == "load" and node.name in self.names:
|
||||
@ -301,9 +299,9 @@ class CodeGenerator(NodeVisitor):
|
||||
def __init__(
|
||||
self,
|
||||
environment: "Environment",
|
||||
name: t.Optional[str],
|
||||
filename: t.Optional[str],
|
||||
stream: t.Optional[t.TextIO] = None,
|
||||
name: str | None,
|
||||
filename: str | None,
|
||||
stream: t.TextIO | None = None,
|
||||
defer_init: bool = False,
|
||||
optimized: bool = True,
|
||||
) -> None:
|
||||
@ -315,17 +313,17 @@ class CodeGenerator(NodeVisitor):
|
||||
self.stream = stream
|
||||
self.created_block_context = False
|
||||
self.defer_init = defer_init
|
||||
self.optimizer: t.Optional[Optimizer] = None
|
||||
self.optimizer: Optimizer | None = None
|
||||
|
||||
if optimized:
|
||||
self.optimizer = Optimizer(environment)
|
||||
|
||||
# aliases for imports
|
||||
self.import_aliases: t.Dict[str, str] = {}
|
||||
self.import_aliases: dict[str, str] = {}
|
||||
|
||||
# a registry for all blocks. Because blocks are moved out
|
||||
# into the global python scope they are registered here
|
||||
self.blocks: t.Dict[str, nodes.Block] = {}
|
||||
self.blocks: dict[str, nodes.Block] = {}
|
||||
|
||||
# the number of extends statements so far
|
||||
self.extends_so_far = 0
|
||||
@ -339,12 +337,12 @@ class CodeGenerator(NodeVisitor):
|
||||
self.code_lineno = 1
|
||||
|
||||
# registry of all filters and tests (global, not block local)
|
||||
self.tests: t.Dict[str, str] = {}
|
||||
self.filters: t.Dict[str, str] = {}
|
||||
self.tests: dict[str, str] = {}
|
||||
self.filters: dict[str, str] = {}
|
||||
|
||||
# the debug information
|
||||
self.debug_info: t.List[t.Tuple[int, int]] = []
|
||||
self._write_debug_info: t.Optional[int] = None
|
||||
self.debug_info: list[tuple[int, int]] = []
|
||||
self._write_debug_info: int | None = None
|
||||
|
||||
# the number of new lines before the next write()
|
||||
self._new_lines = 0
|
||||
@ -363,10 +361,10 @@ class CodeGenerator(NodeVisitor):
|
||||
self._indentation = 0
|
||||
|
||||
# Tracks toplevel assignments
|
||||
self._assign_stack: t.List[t.Set[str]] = []
|
||||
self._assign_stack: list[set[str]] = []
|
||||
|
||||
# Tracks parameter definition blocks
|
||||
self._param_def_block: t.List[t.Set[str]] = []
|
||||
self._param_def_block: list[set[str]] = []
|
||||
|
||||
# Tracks the current context.
|
||||
self._context_reference_stack = ["context"]
|
||||
@ -419,7 +417,7 @@ class CodeGenerator(NodeVisitor):
|
||||
"""Outdent by step."""
|
||||
self._indentation -= step
|
||||
|
||||
def start_write(self, frame: Frame, node: t.Optional[nodes.Node] = None) -> None:
|
||||
def start_write(self, frame: Frame, node: nodes.Node | None = None) -> None:
|
||||
"""Yield or write into the frame buffer."""
|
||||
if frame.buffer is None:
|
||||
self.writeline("yield ", node)
|
||||
@ -432,7 +430,7 @@ class CodeGenerator(NodeVisitor):
|
||||
self.write(")")
|
||||
|
||||
def simple_write(
|
||||
self, s: str, frame: Frame, node: t.Optional[nodes.Node] = None
|
||||
self, s: str, frame: Frame, node: nodes.Node | None = None
|
||||
) -> None:
|
||||
"""Simple shortcut for start_write + write + end_write."""
|
||||
self.start_write(frame, node)
|
||||
@ -464,14 +462,12 @@ class CodeGenerator(NodeVisitor):
|
||||
self._new_lines = 0
|
||||
self.stream.write(x)
|
||||
|
||||
def writeline(
|
||||
self, x: str, node: t.Optional[nodes.Node] = None, extra: int = 0
|
||||
) -> None:
|
||||
def writeline(self, x: str, node: nodes.Node | None = None, extra: int = 0) -> None:
|
||||
"""Combination of newline and write."""
|
||||
self.newline(node, extra)
|
||||
self.write(x)
|
||||
|
||||
def newline(self, node: t.Optional[nodes.Node] = None, extra: int = 0) -> None:
|
||||
def newline(self, node: nodes.Node | None = None, extra: int = 0) -> None:
|
||||
"""Add one or more newlines before the next write."""
|
||||
self._new_lines = max(self._new_lines, 1 + extra)
|
||||
if node is not None and node.lineno != self._last_line:
|
||||
@ -480,9 +476,9 @@ class CodeGenerator(NodeVisitor):
|
||||
|
||||
def signature(
|
||||
self,
|
||||
node: t.Union[nodes.Call, nodes.Filter, nodes.Test],
|
||||
node: nodes.Call | nodes.Filter | nodes.Test,
|
||||
frame: Frame,
|
||||
extra_kwargs: t.Optional[t.Mapping[str, t.Any]] = None,
|
||||
extra_kwargs: t.Mapping[str, t.Any] | None = None,
|
||||
) -> None:
|
||||
"""Writes a function call to the stream for the current node.
|
||||
A leading comma is added automatically. The extra keyword
|
||||
@ -612,8 +608,8 @@ class CodeGenerator(NodeVisitor):
|
||||
return f"{self.choose_async()}def {name}"
|
||||
|
||||
def macro_body(
|
||||
self, node: t.Union[nodes.Macro, nodes.CallBlock], frame: Frame
|
||||
) -> t.Tuple[Frame, MacroRef]:
|
||||
self, node: nodes.Macro | nodes.CallBlock, frame: Frame
|
||||
) -> tuple[Frame, MacroRef]:
|
||||
"""Dump the function def of a macro or call block."""
|
||||
frame = frame.inner()
|
||||
frame.symbols.analyze_node(node)
|
||||
@ -811,7 +807,7 @@ class CodeGenerator(NodeVisitor):
|
||||
self.writeline("_block_vars.update({")
|
||||
else:
|
||||
self.writeline("context.vars.update({")
|
||||
for idx, name in enumerate(vars):
|
||||
for idx, name in enumerate(sorted(vars)):
|
||||
if idx:
|
||||
self.write(", ")
|
||||
ref = frame.symbols.ref(name)
|
||||
@ -821,14 +817,12 @@ class CodeGenerator(NodeVisitor):
|
||||
if len(public_names) == 1:
|
||||
self.writeline(f"context.exported_vars.add({public_names[0]!r})")
|
||||
else:
|
||||
names_str = ", ".join(map(repr, public_names))
|
||||
names_str = ", ".join(map(repr, sorted(public_names)))
|
||||
self.writeline(f"context.exported_vars.update(({names_str}))")
|
||||
|
||||
# -- Statement Visitors
|
||||
|
||||
def visit_Template(
|
||||
self, node: nodes.Template, frame: t.Optional[Frame] = None
|
||||
) -> None:
|
||||
def visit_Template(self, node: nodes.Template, frame: Frame | None = None) -> None:
|
||||
assert frame is None, "no root frame allowed"
|
||||
eval_ctx = EvalContext(self.environment, self.name)
|
||||
|
||||
@ -1098,7 +1092,7 @@ class CodeGenerator(NodeVisitor):
|
||||
self.outdent()
|
||||
|
||||
def _import_common(
|
||||
self, node: t.Union[nodes.Import, nodes.FromImport], frame: Frame
|
||||
self, node: nodes.Import | nodes.FromImport, frame: Frame
|
||||
) -> None:
|
||||
self.write(f"{self.choose_async('await ')}environment.get_template(")
|
||||
self.visit(node.template, frame)
|
||||
@ -1141,9 +1135,14 @@ class CodeGenerator(NodeVisitor):
|
||||
)
|
||||
self.writeline(f"if {frame.symbols.ref(alias)} is missing:")
|
||||
self.indent()
|
||||
# The position will contain the template name, and will be formatted
|
||||
# into a string that will be compiled into an f-string. Curly braces
|
||||
# in the name must be replaced with escapes so that they will not be
|
||||
# executed as part of the f-string.
|
||||
position = self.position(node).replace("{", "{{").replace("}", "}}")
|
||||
message = (
|
||||
"the template {included_template.__name__!r}"
|
||||
f" (imported on {self.position(node)})"
|
||||
f" (imported on {position})"
|
||||
f" does not export the requested name {name!r}"
|
||||
)
|
||||
self.writeline(
|
||||
@ -1366,7 +1365,7 @@ class CodeGenerator(NodeVisitor):
|
||||
with_frame = frame.inner()
|
||||
with_frame.symbols.analyze_node(node)
|
||||
self.enter_frame(with_frame)
|
||||
for target, expr in zip(node.targets, node.values):
|
||||
for target, expr in zip(node.targets, node.values, strict=False):
|
||||
self.newline()
|
||||
self.visit(target, with_frame)
|
||||
self.write(" = ")
|
||||
@ -1379,8 +1378,8 @@ class CodeGenerator(NodeVisitor):
|
||||
self.visit(node.node, frame)
|
||||
|
||||
class _FinalizeInfo(t.NamedTuple):
|
||||
const: t.Optional[t.Callable[..., str]]
|
||||
src: t.Optional[str]
|
||||
const: t.Callable[..., str] | None
|
||||
src: str | None
|
||||
|
||||
@staticmethod
|
||||
def _default_finalize(value: t.Any) -> t.Any:
|
||||
@ -1390,7 +1389,7 @@ class CodeGenerator(NodeVisitor):
|
||||
"""
|
||||
return str(value)
|
||||
|
||||
_finalize: t.Optional[_FinalizeInfo] = None
|
||||
_finalize: _FinalizeInfo | None = None
|
||||
|
||||
def _make_finalize(self) -> _FinalizeInfo:
|
||||
"""Build the finalize function to be used on constants and at
|
||||
@ -1408,7 +1407,7 @@ class CodeGenerator(NodeVisitor):
|
||||
if self._finalize is not None:
|
||||
return self._finalize
|
||||
|
||||
finalize: t.Optional[t.Callable[..., t.Any]]
|
||||
finalize: t.Callable[..., t.Any] | None
|
||||
finalize = default = self._default_finalize
|
||||
src = None
|
||||
|
||||
@ -1506,7 +1505,7 @@ class CodeGenerator(NodeVisitor):
|
||||
self.indent()
|
||||
|
||||
finalize = self._make_finalize()
|
||||
body: t.List[t.Union[t.List[t.Any], nodes.Expr]] = []
|
||||
body: list[list[t.Any] | nodes.Expr] = []
|
||||
|
||||
# Evaluate constants at compile time if possible. Each item in
|
||||
# body will be either a list of static data or a node to be
|
||||
@ -1576,6 +1575,29 @@ class CodeGenerator(NodeVisitor):
|
||||
|
||||
def visit_Assign(self, node: nodes.Assign, frame: Frame) -> None:
|
||||
self.push_assign_tracking()
|
||||
|
||||
# ``a.b`` is allowed for assignment, and is parsed as an NSRef. However,
|
||||
# it is only valid if it references a Namespace object. Emit a check for
|
||||
# that for each ref here, before assignment code is emitted. This can't
|
||||
# be done in visit_NSRef as the ref could be in the middle of a tuple.
|
||||
seen_refs: set[str] = set()
|
||||
|
||||
for nsref in node.find_all(nodes.NSRef):
|
||||
if nsref.name in seen_refs:
|
||||
# Only emit the check for each reference once, in case the same
|
||||
# ref is used multiple times in a tuple, `ns.a, ns.b = c, d`.
|
||||
continue
|
||||
|
||||
seen_refs.add(nsref.name)
|
||||
ref = frame.symbols.ref(nsref.name)
|
||||
self.writeline(f"if not isinstance({ref}, Namespace):")
|
||||
self.indent()
|
||||
self.writeline(
|
||||
"raise TemplateRuntimeError"
|
||||
'("cannot assign attribute on non-namespace object")'
|
||||
)
|
||||
self.outdent()
|
||||
|
||||
self.newline(node)
|
||||
self.visit(node.target, frame)
|
||||
self.write(" = ")
|
||||
@ -1632,17 +1654,11 @@ class CodeGenerator(NodeVisitor):
|
||||
self.write(ref)
|
||||
|
||||
def visit_NSRef(self, node: nodes.NSRef, frame: Frame) -> None:
|
||||
# NSRefs can only be used to store values; since they use the normal
|
||||
# `foo.bar` notation they will be parsed as a normal attribute access
|
||||
# when used anywhere but in a `set` context
|
||||
# NSRef is a dotted assignment target a.b=c, but uses a[b]=c internally.
|
||||
# visit_Assign emits code to validate that each ref is to a Namespace
|
||||
# object only. That can't be emitted here as the ref could be in the
|
||||
# middle of a tuple assignment.
|
||||
ref = frame.symbols.ref(node.name)
|
||||
self.writeline(f"if not isinstance({ref}, Namespace):")
|
||||
self.indent()
|
||||
self.writeline(
|
||||
"raise TemplateRuntimeError"
|
||||
'("cannot assign attribute on non-namespace object")'
|
||||
)
|
||||
self.outdent()
|
||||
self.writeline(f"{ref}[{node.attr!r}]")
|
||||
|
||||
def visit_Const(self, node: nodes.Const, frame: Frame) -> None:
|
||||
@ -1771,7 +1787,7 @@ class CodeGenerator(NodeVisitor):
|
||||
|
||||
@contextmanager
|
||||
def _filter_test_common(
|
||||
self, node: t.Union[nodes.Filter, nodes.Test], frame: Frame, is_filter: bool
|
||||
self, node: nodes.Filter | nodes.Test, frame: Frame, is_filter: bool
|
||||
) -> t.Iterator[None]:
|
||||
if self.environment.is_async:
|
||||
self.write("(await auto_await(")
|
||||
|
||||
@ -11,7 +11,7 @@ if t.TYPE_CHECKING:
|
||||
from .runtime import Context
|
||||
|
||||
|
||||
def rewrite_traceback_stack(source: t.Optional[str] = None) -> BaseException:
|
||||
def rewrite_traceback_stack(source: str | None = None) -> BaseException:
|
||||
"""Rewrite the current exception to replace any tracebacks from
|
||||
within compiled template code with tracebacks that look like they
|
||||
came from the template source.
|
||||
@ -74,7 +74,7 @@ def rewrite_traceback_stack(source: t.Optional[str] = None) -> BaseException:
|
||||
|
||||
|
||||
def fake_traceback( # type: ignore
|
||||
exc_value: BaseException, tb: t.Optional[TracebackType], filename: str, lineno: int
|
||||
exc_value: BaseException, tb: TracebackType | None, filename: str, lineno: int
|
||||
) -> TracebackType:
|
||||
"""Produce a new traceback object that looks like it came from the
|
||||
template source instead of the compiled code. The filename, line
|
||||
@ -128,15 +128,15 @@ def fake_traceback( # type: ignore
|
||||
return sys.exc_info()[2].tb_next # type: ignore
|
||||
|
||||
|
||||
def get_template_locals(real_locals: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]:
|
||||
def get_template_locals(real_locals: t.Mapping[str, t.Any]) -> dict[str, t.Any]:
|
||||
"""Based on the runtime locals, get the context that would be
|
||||
available at that point in the template.
|
||||
"""
|
||||
# Start with the current template context.
|
||||
ctx: "t.Optional[Context]" = real_locals.get("context")
|
||||
ctx: Context | None = real_locals.get("context")
|
||||
|
||||
if ctx is not None:
|
||||
data: t.Dict[str, t.Any] = ctx.get_all().copy()
|
||||
data: dict[str, t.Any] = ctx.get_all().copy()
|
||||
else:
|
||||
data = {}
|
||||
|
||||
@ -144,7 +144,7 @@ def get_template_locals(real_locals: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any
|
||||
# rather than pushing a context. Local variables follow the scheme
|
||||
# l_depth_name. Find the highest-depth local that has a value for
|
||||
# each name.
|
||||
local_overrides: t.Dict[str, t.Tuple[int, t.Any]] = {}
|
||||
local_overrides: dict[str, tuple[int, t.Any]] = {}
|
||||
|
||||
for name, value in real_locals.items():
|
||||
if not name.startswith("l_") or value is missing:
|
||||
|
||||
@ -17,8 +17,8 @@ VARIABLE_START_STRING = "{{"
|
||||
VARIABLE_END_STRING = "}}"
|
||||
COMMENT_START_STRING = "{#"
|
||||
COMMENT_END_STRING = "#}"
|
||||
LINE_STATEMENT_PREFIX: t.Optional[str] = None
|
||||
LINE_COMMENT_PREFIX: t.Optional[str] = None
|
||||
LINE_STATEMENT_PREFIX: str | None = None
|
||||
LINE_COMMENT_PREFIX: str | None = None
|
||||
TRIM_BLOCKS = False
|
||||
LSTRIP_BLOCKS = False
|
||||
NEWLINE_SEQUENCE: "te.Literal['\\n', '\\r\\n', '\\r']" = "\n"
|
||||
@ -36,7 +36,7 @@ DEFAULT_NAMESPACE = {
|
||||
}
|
||||
|
||||
# default policies
|
||||
DEFAULT_POLICIES: t.Dict[str, t.Any] = {
|
||||
DEFAULT_POLICIES: dict[str, t.Any] = {
|
||||
"compiler.ascii_str": True,
|
||||
"urlize.rel": "noopener",
|
||||
"urlize.target": None,
|
||||
|
||||
@ -7,6 +7,7 @@ import typing
|
||||
import typing as t
|
||||
import weakref
|
||||
from collections import ChainMap
|
||||
from contextlib import aclosing
|
||||
from functools import lru_cache
|
||||
from functools import partial
|
||||
from functools import reduce
|
||||
@ -66,7 +67,7 @@ _env_bound = t.TypeVar("_env_bound", bound="Environment")
|
||||
|
||||
# for direct template usage we have up to ten living environments
|
||||
@lru_cache(maxsize=10)
|
||||
def get_spontaneous_environment(cls: t.Type[_env_bound], *args: t.Any) -> _env_bound:
|
||||
def get_spontaneous_environment(cls: type[_env_bound], *args: t.Any) -> _env_bound:
|
||||
"""Return a new spontaneous environment. A spontaneous environment
|
||||
is used for templates created directly rather than through an
|
||||
existing environment.
|
||||
@ -81,7 +82,7 @@ def get_spontaneous_environment(cls: t.Type[_env_bound], *args: t.Any) -> _env_b
|
||||
|
||||
def create_cache(
|
||||
size: int,
|
||||
) -> t.Optional[t.MutableMapping[t.Tuple["weakref.ref[BaseLoader]", str], "Template"]]:
|
||||
) -> t.MutableMapping[tuple["weakref.ref[BaseLoader]", str], "Template"] | None:
|
||||
"""Return the cache class for the given size."""
|
||||
if size == 0:
|
||||
return None
|
||||
@ -93,10 +94,8 @@ def create_cache(
|
||||
|
||||
|
||||
def copy_cache(
|
||||
cache: t.Optional[
|
||||
t.MutableMapping[t.Tuple["weakref.ref[BaseLoader]", str], "Template"]
|
||||
],
|
||||
) -> t.Optional[t.MutableMapping[t.Tuple["weakref.ref[BaseLoader]", str], "Template"]]:
|
||||
cache: t.MutableMapping[tuple["weakref.ref[BaseLoader]", str], "Template"] | None,
|
||||
) -> t.MutableMapping[tuple["weakref.ref[BaseLoader]", str], "Template"] | None:
|
||||
"""Create an empty copy of the given cache."""
|
||||
if cache is None:
|
||||
return None
|
||||
@ -109,8 +108,8 @@ def copy_cache(
|
||||
|
||||
def load_extensions(
|
||||
environment: "Environment",
|
||||
extensions: t.Sequence[t.Union[str, t.Type["Extension"]]],
|
||||
) -> t.Dict[str, "Extension"]:
|
||||
extensions: t.Sequence[str | type["Extension"]],
|
||||
) -> dict[str, "Extension"]:
|
||||
"""Load the extensions from the list and bind it to the environment.
|
||||
Returns a dict of instantiated extensions.
|
||||
"""
|
||||
@ -118,18 +117,18 @@ def load_extensions(
|
||||
|
||||
for extension in extensions:
|
||||
if isinstance(extension, str):
|
||||
extension = t.cast(t.Type["Extension"], import_string(extension))
|
||||
extension = t.cast(type["Extension"], import_string(extension))
|
||||
|
||||
result[extension.identifier] = extension(environment)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _environment_config_check(environment: "Environment") -> "Environment":
|
||||
def _environment_config_check(environment: _env_bound) -> _env_bound:
|
||||
"""Perform a sanity check on the environment."""
|
||||
assert issubclass(
|
||||
environment.undefined, Undefined
|
||||
), "'undefined' must be a subclass of 'jinja2.Undefined'."
|
||||
assert issubclass(environment.undefined, Undefined), (
|
||||
"'undefined' must be a subclass of 'jinja2.Undefined'."
|
||||
)
|
||||
assert (
|
||||
environment.block_start_string
|
||||
!= environment.variable_start_string
|
||||
@ -283,15 +282,15 @@ class Environment:
|
||||
|
||||
#: the class that is used for code generation. See
|
||||
#: :class:`~jinja2.compiler.CodeGenerator` for more information.
|
||||
code_generator_class: t.Type["CodeGenerator"] = CodeGenerator
|
||||
code_generator_class: type["CodeGenerator"] = CodeGenerator
|
||||
|
||||
concat = "".join
|
||||
|
||||
#: the context class that is used for templates. See
|
||||
#: :class:`~jinja2.runtime.Context` for more information.
|
||||
context_class: t.Type[Context] = Context
|
||||
context_class: type[Context] = Context
|
||||
|
||||
template_class: t.Type["Template"]
|
||||
template_class: type["Template"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -301,17 +300,17 @@ class Environment:
|
||||
variable_end_string: str = VARIABLE_END_STRING,
|
||||
comment_start_string: str = COMMENT_START_STRING,
|
||||
comment_end_string: str = COMMENT_END_STRING,
|
||||
line_statement_prefix: t.Optional[str] = LINE_STATEMENT_PREFIX,
|
||||
line_comment_prefix: t.Optional[str] = LINE_COMMENT_PREFIX,
|
||||
line_statement_prefix: str | None = LINE_STATEMENT_PREFIX,
|
||||
line_comment_prefix: str | None = LINE_COMMENT_PREFIX,
|
||||
trim_blocks: bool = TRIM_BLOCKS,
|
||||
lstrip_blocks: bool = LSTRIP_BLOCKS,
|
||||
newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = NEWLINE_SEQUENCE,
|
||||
keep_trailing_newline: bool = KEEP_TRAILING_NEWLINE,
|
||||
extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = (),
|
||||
extensions: t.Sequence[str | type["Extension"]] = (),
|
||||
optimized: bool = True,
|
||||
undefined: t.Type[Undefined] = Undefined,
|
||||
finalize: t.Optional[t.Callable[..., t.Any]] = None,
|
||||
autoescape: t.Union[bool, t.Callable[[t.Optional[str]], bool]] = False,
|
||||
undefined: type[Undefined] = Undefined,
|
||||
finalize: t.Callable[..., t.Any] | None = None,
|
||||
autoescape: bool | t.Callable[[str | None], bool] = False,
|
||||
loader: t.Optional["BaseLoader"] = None,
|
||||
cache_size: int = 400,
|
||||
auto_reload: bool = True,
|
||||
@ -344,7 +343,7 @@ class Environment:
|
||||
self.keep_trailing_newline = keep_trailing_newline
|
||||
|
||||
# runtime information
|
||||
self.undefined: t.Type[Undefined] = undefined
|
||||
self.undefined: type[Undefined] = undefined
|
||||
self.optimized = optimized
|
||||
self.finalize = finalize
|
||||
self.autoescape = autoescape
|
||||
@ -369,7 +368,7 @@ class Environment:
|
||||
self.is_async = enable_async
|
||||
_environment_config_check(self)
|
||||
|
||||
def add_extension(self, extension: t.Union[str, t.Type["Extension"]]) -> None:
|
||||
def add_extension(self, extension: str | type["Extension"]) -> None:
|
||||
"""Adds an extension after the environment was created.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
@ -393,23 +392,23 @@ class Environment:
|
||||
variable_end_string: str = missing,
|
||||
comment_start_string: str = missing,
|
||||
comment_end_string: str = missing,
|
||||
line_statement_prefix: t.Optional[str] = missing,
|
||||
line_comment_prefix: t.Optional[str] = missing,
|
||||
line_statement_prefix: str | None = missing,
|
||||
line_comment_prefix: str | None = missing,
|
||||
trim_blocks: bool = missing,
|
||||
lstrip_blocks: bool = missing,
|
||||
newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = missing,
|
||||
keep_trailing_newline: bool = missing,
|
||||
extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = missing,
|
||||
extensions: t.Sequence[str | type["Extension"]] = missing,
|
||||
optimized: bool = missing,
|
||||
undefined: t.Type[Undefined] = missing,
|
||||
finalize: t.Optional[t.Callable[..., t.Any]] = missing,
|
||||
autoescape: t.Union[bool, t.Callable[[t.Optional[str]], bool]] = missing,
|
||||
undefined: type[Undefined] = missing,
|
||||
finalize: t.Callable[..., t.Any] | None = missing,
|
||||
autoescape: bool | t.Callable[[str | None], bool] = missing,
|
||||
loader: t.Optional["BaseLoader"] = missing,
|
||||
cache_size: int = missing,
|
||||
auto_reload: bool = missing,
|
||||
bytecode_cache: t.Optional["BytecodeCache"] = missing,
|
||||
enable_async: bool = False,
|
||||
) -> "Environment":
|
||||
enable_async: bool = missing,
|
||||
) -> "te.Self":
|
||||
"""Create a new overlay environment that shares all the data with the
|
||||
current environment except for cache and the overridden attributes.
|
||||
Extensions cannot be removed for an overlayed environment. An overlayed
|
||||
@ -421,8 +420,11 @@ class Environment:
|
||||
copied over so modifications on the original environment may not shine
|
||||
through.
|
||||
|
||||
.. versionchanged:: 3.1.5
|
||||
``enable_async`` is applied correctly.
|
||||
|
||||
.. versionchanged:: 3.1.2
|
||||
Added the ``newline_sequence``,, ``keep_trailing_newline``,
|
||||
Added the ``newline_sequence``, ``keep_trailing_newline``,
|
||||
and ``enable_async`` parameters to match ``__init__``.
|
||||
"""
|
||||
args = dict(locals())
|
||||
@ -462,9 +464,7 @@ class Environment:
|
||||
"""Iterates over the extensions by priority."""
|
||||
return iter(sorted(self.extensions.values(), key=lambda x: x.priority))
|
||||
|
||||
def getitem(
|
||||
self, obj: t.Any, argument: t.Union[str, t.Any]
|
||||
) -> t.Union[t.Any, Undefined]:
|
||||
def getitem(self, obj: t.Any, argument: str | t.Any) -> t.Any | Undefined:
|
||||
"""Get an item or attribute of an object but prefer the item."""
|
||||
try:
|
||||
return obj[argument]
|
||||
@ -496,12 +496,12 @@ class Environment:
|
||||
|
||||
def _filter_test_common(
|
||||
self,
|
||||
name: t.Union[str, Undefined],
|
||||
name: str | Undefined,
|
||||
value: t.Any,
|
||||
args: t.Optional[t.Sequence[t.Any]],
|
||||
kwargs: t.Optional[t.Mapping[str, t.Any]],
|
||||
context: t.Optional[Context],
|
||||
eval_ctx: t.Optional[EvalContext],
|
||||
args: t.Sequence[t.Any] | None,
|
||||
kwargs: t.Mapping[str, t.Any] | None,
|
||||
context: Context | None,
|
||||
eval_ctx: EvalContext | None,
|
||||
is_filter: bool,
|
||||
) -> t.Any:
|
||||
if is_filter:
|
||||
@ -552,10 +552,10 @@ class Environment:
|
||||
self,
|
||||
name: str,
|
||||
value: t.Any,
|
||||
args: t.Optional[t.Sequence[t.Any]] = None,
|
||||
kwargs: t.Optional[t.Mapping[str, t.Any]] = None,
|
||||
context: t.Optional[Context] = None,
|
||||
eval_ctx: t.Optional[EvalContext] = None,
|
||||
args: t.Sequence[t.Any] | None = None,
|
||||
kwargs: t.Mapping[str, t.Any] | None = None,
|
||||
context: Context | None = None,
|
||||
eval_ctx: EvalContext | None = None,
|
||||
) -> t.Any:
|
||||
"""Invoke a filter on a value the same way the compiler does.
|
||||
|
||||
@ -573,10 +573,10 @@ class Environment:
|
||||
self,
|
||||
name: str,
|
||||
value: t.Any,
|
||||
args: t.Optional[t.Sequence[t.Any]] = None,
|
||||
kwargs: t.Optional[t.Mapping[str, t.Any]] = None,
|
||||
context: t.Optional[Context] = None,
|
||||
eval_ctx: t.Optional[EvalContext] = None,
|
||||
args: t.Sequence[t.Any] | None = None,
|
||||
kwargs: t.Mapping[str, t.Any] | None = None,
|
||||
context: Context | None = None,
|
||||
eval_ctx: EvalContext | None = None,
|
||||
) -> t.Any:
|
||||
"""Invoke a test on a value the same way the compiler does.
|
||||
|
||||
@ -598,8 +598,8 @@ class Environment:
|
||||
def parse(
|
||||
self,
|
||||
source: str,
|
||||
name: t.Optional[str] = None,
|
||||
filename: t.Optional[str] = None,
|
||||
name: str | None = None,
|
||||
filename: str | None = None,
|
||||
) -> nodes.Template:
|
||||
"""Parse the sourcecode and return the abstract syntax tree. This
|
||||
tree of nodes is used by the compiler to convert the template into
|
||||
@ -615,7 +615,7 @@ class Environment:
|
||||
self.handle_exception(source=source)
|
||||
|
||||
def _parse(
|
||||
self, source: str, name: t.Optional[str], filename: t.Optional[str]
|
||||
self, source: str, name: str | None, filename: str | None
|
||||
) -> nodes.Template:
|
||||
"""Internal parsing function used by `parse` and `compile`."""
|
||||
return Parser(self, source, name, filename).parse()
|
||||
@ -623,9 +623,9 @@ class Environment:
|
||||
def lex(
|
||||
self,
|
||||
source: str,
|
||||
name: t.Optional[str] = None,
|
||||
filename: t.Optional[str] = None,
|
||||
) -> t.Iterator[t.Tuple[int, str, str]]:
|
||||
name: str | None = None,
|
||||
filename: str | None = None,
|
||||
) -> t.Iterator[tuple[int, str, str]]:
|
||||
"""Lex the given sourcecode and return a generator that yields
|
||||
tokens as tuples in the form ``(lineno, token_type, value)``.
|
||||
This can be useful for :ref:`extension development <writing-extensions>`
|
||||
@ -644,8 +644,8 @@ class Environment:
|
||||
def preprocess(
|
||||
self,
|
||||
source: str,
|
||||
name: t.Optional[str] = None,
|
||||
filename: t.Optional[str] = None,
|
||||
name: str | None = None,
|
||||
filename: str | None = None,
|
||||
) -> str:
|
||||
"""Preprocesses the source with all extensions. This is automatically
|
||||
called for all parsing and compiling methods but *not* for :meth:`lex`
|
||||
@ -660,9 +660,9 @@ class Environment:
|
||||
def _tokenize(
|
||||
self,
|
||||
source: str,
|
||||
name: t.Optional[str],
|
||||
filename: t.Optional[str] = None,
|
||||
state: t.Optional[str] = None,
|
||||
name: str | None,
|
||||
filename: str | None = None,
|
||||
state: str | None = None,
|
||||
) -> TokenStream:
|
||||
"""Called by the parser to do the preprocessing and filtering
|
||||
for all the extensions. Returns a :class:`~jinja2.lexer.TokenStream`.
|
||||
@ -674,15 +674,15 @@ class Environment:
|
||||
stream = ext.filter_stream(stream) # type: ignore
|
||||
|
||||
if not isinstance(stream, TokenStream):
|
||||
stream = TokenStream(stream, name, filename) # type: ignore[unreachable]
|
||||
stream = TokenStream(stream, name, filename)
|
||||
|
||||
return stream
|
||||
|
||||
def _generate(
|
||||
self,
|
||||
source: nodes.Template,
|
||||
name: t.Optional[str],
|
||||
filename: t.Optional[str],
|
||||
name: str | None,
|
||||
filename: str | None,
|
||||
defer_init: bool = False,
|
||||
) -> str:
|
||||
"""Internal hook that can be overridden to hook a different generate
|
||||
@ -708,11 +708,11 @@ class Environment:
|
||||
return compile(source, filename, "exec")
|
||||
|
||||
@typing.overload
|
||||
def compile( # type: ignore
|
||||
def compile(
|
||||
self,
|
||||
source: t.Union[str, nodes.Template],
|
||||
name: t.Optional[str] = None,
|
||||
filename: t.Optional[str] = None,
|
||||
source: str | nodes.Template,
|
||||
name: str | None = None,
|
||||
filename: str | None = None,
|
||||
raw: "te.Literal[False]" = False,
|
||||
defer_init: bool = False,
|
||||
) -> CodeType: ...
|
||||
@ -720,9 +720,9 @@ class Environment:
|
||||
@typing.overload
|
||||
def compile(
|
||||
self,
|
||||
source: t.Union[str, nodes.Template],
|
||||
name: t.Optional[str] = None,
|
||||
filename: t.Optional[str] = None,
|
||||
source: str | nodes.Template,
|
||||
name: str | None = None,
|
||||
filename: str | None = None,
|
||||
raw: "te.Literal[True]" = ...,
|
||||
defer_init: bool = False,
|
||||
) -> str: ...
|
||||
@ -730,12 +730,12 @@ class Environment:
|
||||
@internalcode
|
||||
def compile(
|
||||
self,
|
||||
source: t.Union[str, nodes.Template],
|
||||
name: t.Optional[str] = None,
|
||||
filename: t.Optional[str] = None,
|
||||
source: str | nodes.Template,
|
||||
name: str | None = None,
|
||||
filename: str | None = None,
|
||||
raw: bool = False,
|
||||
defer_init: bool = False,
|
||||
) -> t.Union[str, CodeType]:
|
||||
) -> str | CodeType:
|
||||
"""Compile a node or template source code. The `name` parameter is
|
||||
the load name of the template after it was joined using
|
||||
:meth:`join_path` if necessary, not the filename on the file system.
|
||||
@ -817,10 +817,10 @@ class Environment:
|
||||
def compile_templates(
|
||||
self,
|
||||
target: t.Union[str, "os.PathLike[str]"],
|
||||
extensions: t.Optional[t.Collection[str]] = None,
|
||||
filter_func: t.Optional[t.Callable[[str], bool]] = None,
|
||||
zip: t.Optional[str] = "deflated",
|
||||
log_function: t.Optional[t.Callable[[str], None]] = None,
|
||||
extensions: t.Collection[str] | None = None,
|
||||
filter_func: t.Callable[[str], bool] | None = None,
|
||||
zip: str | None = "deflated",
|
||||
log_function: t.Callable[[str], None] | None = None,
|
||||
ignore_errors: bool = True,
|
||||
) -> None:
|
||||
"""Finds all the templates the loader can find, compiles them
|
||||
@ -897,9 +897,9 @@ class Environment:
|
||||
|
||||
def list_templates(
|
||||
self,
|
||||
extensions: t.Optional[t.Collection[str]] = None,
|
||||
filter_func: t.Optional[t.Callable[[str], bool]] = None,
|
||||
) -> t.List[str]:
|
||||
extensions: t.Collection[str] | None = None,
|
||||
filter_func: t.Callable[[str], bool] | None = None,
|
||||
) -> list[str]:
|
||||
"""Returns a list of templates for this environment. This requires
|
||||
that the loader supports the loader's
|
||||
:meth:`~BaseLoader.list_templates` method.
|
||||
@ -932,7 +932,7 @@ class Environment:
|
||||
|
||||
return names
|
||||
|
||||
def handle_exception(self, source: t.Optional[str] = None) -> "te.NoReturn":
|
||||
def handle_exception(self, source: str | None = None) -> "te.NoReturn":
|
||||
"""Exception handling helper. This is used internally to either raise
|
||||
rewritten exceptions or return a rendered traceback for the template.
|
||||
"""
|
||||
@ -954,7 +954,7 @@ class Environment:
|
||||
|
||||
@internalcode
|
||||
def _load_template(
|
||||
self, name: str, globals: t.Optional[t.MutableMapping[str, t.Any]]
|
||||
self, name: str, globals: t.MutableMapping[str, t.Any] | None
|
||||
) -> "Template":
|
||||
if self.loader is None:
|
||||
raise TypeError("no loader for this environment specified")
|
||||
@ -981,8 +981,8 @@ class Environment:
|
||||
def get_template(
|
||||
self,
|
||||
name: t.Union[str, "Template"],
|
||||
parent: t.Optional[str] = None,
|
||||
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||
parent: str | None = None,
|
||||
globals: t.MutableMapping[str, t.Any] | None = None,
|
||||
) -> "Template":
|
||||
"""Load a template by name with :attr:`loader` and return a
|
||||
:class:`Template`. If the template does not exist a
|
||||
@ -1018,8 +1018,8 @@ class Environment:
|
||||
def select_template(
|
||||
self,
|
||||
names: t.Iterable[t.Union[str, "Template"]],
|
||||
parent: t.Optional[str] = None,
|
||||
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||
parent: str | None = None,
|
||||
globals: t.MutableMapping[str, t.Any] | None = None,
|
||||
) -> "Template":
|
||||
"""Like :meth:`get_template`, but tries loading multiple names.
|
||||
If none of the names can be loaded a :exc:`TemplatesNotFound`
|
||||
@ -1071,11 +1071,9 @@ class Environment:
|
||||
@internalcode
|
||||
def get_or_select_template(
|
||||
self,
|
||||
template_name_or_list: t.Union[
|
||||
str, "Template", t.List[t.Union[str, "Template"]]
|
||||
],
|
||||
parent: t.Optional[str] = None,
|
||||
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||
template_name_or_list: t.Union[str, "Template", list[t.Union[str, "Template"]]],
|
||||
parent: str | None = None,
|
||||
globals: t.MutableMapping[str, t.Any] | None = None,
|
||||
) -> "Template":
|
||||
"""Use :meth:`select_template` if an iterable of template names
|
||||
is given, or :meth:`get_template` if one name is given.
|
||||
@ -1090,9 +1088,9 @@ class Environment:
|
||||
|
||||
def from_string(
|
||||
self,
|
||||
source: t.Union[str, nodes.Template],
|
||||
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||
template_class: t.Optional[t.Type["Template"]] = None,
|
||||
source: str | nodes.Template,
|
||||
globals: t.MutableMapping[str, t.Any] | None = None,
|
||||
template_class: type["Template"] | None = None,
|
||||
) -> "Template":
|
||||
"""Load a template from a source string without using
|
||||
:attr:`loader`.
|
||||
@ -1110,7 +1108,7 @@ class Environment:
|
||||
return cls.from_code(self, self.compile(source), gs, None)
|
||||
|
||||
def make_globals(
|
||||
self, d: t.Optional[t.MutableMapping[str, t.Any]]
|
||||
self, d: t.MutableMapping[str, t.Any] | None
|
||||
) -> t.MutableMapping[str, t.Any]:
|
||||
"""Make the globals map for a template. Any given template
|
||||
globals overlay the environment :attr:`globals`.
|
||||
@ -1151,38 +1149,38 @@ class Template:
|
||||
|
||||
#: Type of environment to create when creating a template directly
|
||||
#: rather than through an existing environment.
|
||||
environment_class: t.Type[Environment] = Environment
|
||||
environment_class: type[Environment] = Environment
|
||||
|
||||
environment: Environment
|
||||
globals: t.MutableMapping[str, t.Any]
|
||||
name: t.Optional[str]
|
||||
filename: t.Optional[str]
|
||||
blocks: t.Dict[str, t.Callable[[Context], t.Iterator[str]]]
|
||||
name: str | None
|
||||
filename: str | None
|
||||
blocks: dict[str, t.Callable[[Context], t.Iterator[str]]]
|
||||
root_render_func: t.Callable[[Context], t.Iterator[str]]
|
||||
_module: t.Optional["TemplateModule"]
|
||||
_debug_info: str
|
||||
_uptodate: t.Optional[t.Callable[[], bool]]
|
||||
_uptodate: t.Callable[[], bool] | None
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
source: t.Union[str, nodes.Template],
|
||||
source: str | nodes.Template,
|
||||
block_start_string: str = BLOCK_START_STRING,
|
||||
block_end_string: str = BLOCK_END_STRING,
|
||||
variable_start_string: str = VARIABLE_START_STRING,
|
||||
variable_end_string: str = VARIABLE_END_STRING,
|
||||
comment_start_string: str = COMMENT_START_STRING,
|
||||
comment_end_string: str = COMMENT_END_STRING,
|
||||
line_statement_prefix: t.Optional[str] = LINE_STATEMENT_PREFIX,
|
||||
line_comment_prefix: t.Optional[str] = LINE_COMMENT_PREFIX,
|
||||
line_statement_prefix: str | None = LINE_STATEMENT_PREFIX,
|
||||
line_comment_prefix: str | None = LINE_COMMENT_PREFIX,
|
||||
trim_blocks: bool = TRIM_BLOCKS,
|
||||
lstrip_blocks: bool = LSTRIP_BLOCKS,
|
||||
newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = NEWLINE_SEQUENCE,
|
||||
keep_trailing_newline: bool = KEEP_TRAILING_NEWLINE,
|
||||
extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = (),
|
||||
extensions: t.Sequence[str | type["Extension"]] = (),
|
||||
optimized: bool = True,
|
||||
undefined: t.Type[Undefined] = Undefined,
|
||||
finalize: t.Optional[t.Callable[..., t.Any]] = None,
|
||||
autoescape: t.Union[bool, t.Callable[[t.Optional[str]], bool]] = False,
|
||||
undefined: type[Undefined] = Undefined,
|
||||
finalize: t.Callable[..., t.Any] | None = None,
|
||||
autoescape: bool | t.Callable[[str | None], bool] = False,
|
||||
enable_async: bool = False,
|
||||
) -> t.Any: # it returns a `Template`, but this breaks the sphinx build...
|
||||
env = get_spontaneous_environment(
|
||||
@ -1218,7 +1216,7 @@ class Template:
|
||||
environment: Environment,
|
||||
code: CodeType,
|
||||
globals: t.MutableMapping[str, t.Any],
|
||||
uptodate: t.Optional[t.Callable[[], bool]] = None,
|
||||
uptodate: t.Callable[[], bool] | None = None,
|
||||
) -> "Template":
|
||||
"""Creates a template object from compiled code and the globals. This
|
||||
is used by the loaders and environment to create a template object.
|
||||
@ -1250,7 +1248,7 @@ class Template:
|
||||
namespace: t.MutableMapping[str, t.Any],
|
||||
globals: t.MutableMapping[str, t.Any],
|
||||
) -> "Template":
|
||||
t: "Template" = object.__new__(cls)
|
||||
t: Template = object.__new__(cls)
|
||||
t.environment = environment
|
||||
t.globals = globals
|
||||
t.name = namespace["name"]
|
||||
@ -1333,7 +1331,7 @@ class Template:
|
||||
if self.environment.is_async:
|
||||
import asyncio
|
||||
|
||||
async def to_list() -> t.List[str]:
|
||||
async def to_list() -> list[str]:
|
||||
return [x async for x in self.generate_async(*args, **kwargs)]
|
||||
|
||||
yield from asyncio.run(to_list())
|
||||
@ -1360,22 +1358,19 @@ class Template:
|
||||
ctx = self.new_context(dict(*args, **kwargs))
|
||||
|
||||
try:
|
||||
agen = self.root_render_func(ctx)
|
||||
try:
|
||||
async for event in agen: # type: ignore
|
||||
agen: t.AsyncGenerator[str, None] = self.root_render_func(ctx) # type: ignore[assignment]
|
||||
|
||||
async with aclosing(agen):
|
||||
async for event in agen:
|
||||
yield event
|
||||
finally:
|
||||
# we can't use async with aclosing(...) because that's only
|
||||
# in 3.10+
|
||||
await agen.aclose() # type: ignore
|
||||
except Exception:
|
||||
yield self.environment.handle_exception()
|
||||
|
||||
def new_context(
|
||||
self,
|
||||
vars: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
vars: dict[str, t.Any] | None = None,
|
||||
shared: bool = False,
|
||||
locals: t.Optional[t.Mapping[str, t.Any]] = None,
|
||||
locals: t.Mapping[str, t.Any] | None = None,
|
||||
) -> Context:
|
||||
"""Create a new :class:`Context` for this template. The vars
|
||||
provided will be passed to the template. Per default the globals
|
||||
@ -1390,9 +1385,9 @@ class Template:
|
||||
|
||||
def make_module(
|
||||
self,
|
||||
vars: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
vars: dict[str, t.Any] | None = None,
|
||||
shared: bool = False,
|
||||
locals: t.Optional[t.Mapping[str, t.Any]] = None,
|
||||
locals: t.Mapping[str, t.Any] | None = None,
|
||||
) -> "TemplateModule":
|
||||
"""This method works like the :attr:`module` attribute when called
|
||||
without arguments but it will evaluate the template on every call
|
||||
@ -1405,9 +1400,9 @@ class Template:
|
||||
|
||||
async def make_module_async(
|
||||
self,
|
||||
vars: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
vars: dict[str, t.Any] | None = None,
|
||||
shared: bool = False,
|
||||
locals: t.Optional[t.Mapping[str, t.Any]] = None,
|
||||
locals: t.Mapping[str, t.Any] | None = None,
|
||||
) -> "TemplateModule":
|
||||
"""As template module creation can invoke template code for
|
||||
asynchronous executions this method must be used instead of the
|
||||
@ -1422,7 +1417,7 @@ class Template:
|
||||
)
|
||||
|
||||
@internalcode
|
||||
def _get_default_module(self, ctx: t.Optional[Context] = None) -> "TemplateModule":
|
||||
def _get_default_module(self, ctx: Context | None = None) -> "TemplateModule":
|
||||
"""If a context is passed in, this means that the template was
|
||||
imported. Imported templates have access to the current
|
||||
template's globals by default, but they can only be accessed via
|
||||
@ -1449,7 +1444,7 @@ class Template:
|
||||
return self._module
|
||||
|
||||
async def _get_default_module_async(
|
||||
self, ctx: t.Optional[Context] = None
|
||||
self, ctx: Context | None = None
|
||||
) -> "TemplateModule":
|
||||
if ctx is not None:
|
||||
keys = ctx.globals_keys - self.globals.keys()
|
||||
@ -1495,7 +1490,7 @@ class Template:
|
||||
return self._uptodate()
|
||||
|
||||
@property
|
||||
def debug_info(self) -> t.List[t.Tuple[int, int]]:
|
||||
def debug_info(self) -> list[tuple[int, int]]:
|
||||
"""The debug info mapping."""
|
||||
if self._debug_info:
|
||||
return [
|
||||
@ -1523,7 +1518,7 @@ class TemplateModule:
|
||||
self,
|
||||
template: Template,
|
||||
context: Context,
|
||||
body_stream: t.Optional[t.Iterable[str]] = None,
|
||||
body_stream: t.Iterable[str] | None = None,
|
||||
) -> None:
|
||||
if body_stream is None:
|
||||
if context.environment.is_async:
|
||||
@ -1563,7 +1558,7 @@ class TemplateExpression:
|
||||
self._template = template
|
||||
self._undefined_to_none = undefined_to_none
|
||||
|
||||
def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Optional[t.Any]:
|
||||
def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any | None:
|
||||
context = self._template.new_context(dict(*args, **kwargs))
|
||||
consume(self._template.root_render_func(context))
|
||||
rv = context.vars["result"]
|
||||
@ -1589,9 +1584,9 @@ class TemplateStream:
|
||||
|
||||
def dump(
|
||||
self,
|
||||
fp: t.Union[str, t.IO[bytes]],
|
||||
encoding: t.Optional[str] = None,
|
||||
errors: t.Optional[str] = "strict",
|
||||
fp: str | t.IO[bytes],
|
||||
encoding: str | None = None,
|
||||
errors: str | None = "strict",
|
||||
) -> None:
|
||||
"""Dump the complete stream into a file or file-like object.
|
||||
Per default strings are written, if you want to encode
|
||||
@ -1633,7 +1628,7 @@ class TemplateStream:
|
||||
self.buffered = False
|
||||
|
||||
def _buffered_generator(self, size: int) -> t.Iterator[str]:
|
||||
buf: t.List[str] = []
|
||||
buf: list[str] = []
|
||||
c_size = 0
|
||||
push = buf.append
|
||||
|
||||
|
||||
@ -7,11 +7,11 @@ if t.TYPE_CHECKING:
|
||||
class TemplateError(Exception):
|
||||
"""Baseclass for all template errors."""
|
||||
|
||||
def __init__(self, message: t.Optional[str] = None) -> None:
|
||||
def __init__(self, message: str | None = None) -> None:
|
||||
super().__init__(message)
|
||||
|
||||
@property
|
||||
def message(self) -> t.Optional[str]:
|
||||
def message(self) -> str | None:
|
||||
return self.args[0] if self.args else None
|
||||
|
||||
|
||||
@ -25,12 +25,12 @@ class TemplateNotFound(IOError, LookupError, TemplateError):
|
||||
|
||||
# Silence the Python warning about message being deprecated since
|
||||
# it's not valid here.
|
||||
message: t.Optional[str] = None
|
||||
message: str | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: t.Optional[t.Union[str, "Undefined"]],
|
||||
message: t.Optional[str] = None,
|
||||
name: t.Union[str, "Undefined"] | None,
|
||||
message: str | None = None,
|
||||
) -> None:
|
||||
IOError.__init__(self, name)
|
||||
|
||||
@ -65,7 +65,7 @@ class TemplatesNotFound(TemplateNotFound):
|
||||
def __init__(
|
||||
self,
|
||||
names: t.Sequence[t.Union[str, "Undefined"]] = (),
|
||||
message: t.Optional[str] = None,
|
||||
message: str | None = None,
|
||||
) -> None:
|
||||
if message is None:
|
||||
from .runtime import Undefined
|
||||
@ -92,14 +92,14 @@ class TemplateSyntaxError(TemplateError):
|
||||
self,
|
||||
message: str,
|
||||
lineno: int,
|
||||
name: t.Optional[str] = None,
|
||||
filename: t.Optional[str] = None,
|
||||
name: str | None = None,
|
||||
filename: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(message)
|
||||
self.lineno = lineno
|
||||
self.name = name
|
||||
self.filename = filename
|
||||
self.source: t.Optional[str] = None
|
||||
self.source: str | None = None
|
||||
|
||||
# this is set to True if the debug.translate_syntax_error
|
||||
# function translated the syntax error into a new traceback
|
||||
|
||||
@ -37,12 +37,12 @@ if t.TYPE_CHECKING:
|
||||
self, context: str, singular: str, plural: str, n: int
|
||||
) -> str: ...
|
||||
|
||||
_SupportedTranslations = t.Union[_TranslationsBasic, _TranslationsContext]
|
||||
_SupportedTranslations = _TranslationsBasic | _TranslationsContext
|
||||
|
||||
|
||||
# I18N functions available in Jinja templates. If the I18N library
|
||||
# provides ugettext, it will be assigned to gettext.
|
||||
GETTEXT_FUNCTIONS: t.Tuple[str, ...] = (
|
||||
GETTEXT_FUNCTIONS: tuple[str, ...] = (
|
||||
"_",
|
||||
"gettext",
|
||||
"ngettext",
|
||||
@ -77,7 +77,7 @@ class Extension:
|
||||
cls.identifier = f"{cls.__module__}.{cls.__name__}"
|
||||
|
||||
#: if this extension parses this is the list of tags it's listening to.
|
||||
tags: t.Set[str] = set()
|
||||
tags: set[str] = set()
|
||||
|
||||
#: the priority of that extension. This is especially useful for
|
||||
#: extensions that preprocess values. A lower value means higher
|
||||
@ -89,7 +89,7 @@ class Extension:
|
||||
def __init__(self, environment: Environment) -> None:
|
||||
self.environment = environment
|
||||
|
||||
def bind(self, environment: Environment) -> "Extension":
|
||||
def bind(self, environment: Environment) -> "te.Self":
|
||||
"""Create a copy of this extension bound to another environment."""
|
||||
rv = object.__new__(self.__class__)
|
||||
rv.__dict__.update(self.__dict__)
|
||||
@ -97,7 +97,7 @@ class Extension:
|
||||
return rv
|
||||
|
||||
def preprocess(
|
||||
self, source: str, name: t.Optional[str], filename: t.Optional[str] = None
|
||||
self, source: str, name: str | None, filename: str | None = None
|
||||
) -> str:
|
||||
"""This method is called before the actual lexing and can be used to
|
||||
preprocess the source. The `filename` is optional. The return value
|
||||
@ -115,7 +115,7 @@ class Extension:
|
||||
"""
|
||||
return stream
|
||||
|
||||
def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
|
||||
def parse(self, parser: "Parser") -> nodes.Node | list[nodes.Node]:
|
||||
"""If any of the :attr:`tags` matched this method is called with the
|
||||
parser as first argument. The token the parser stream is pointing at
|
||||
is the name token that matched. This method has to return one or a
|
||||
@ -123,9 +123,7 @@ class Extension:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def attr(
|
||||
self, name: str, lineno: t.Optional[int] = None
|
||||
) -> nodes.ExtensionAttribute:
|
||||
def attr(self, name: str, lineno: int | None = None) -> nodes.ExtensionAttribute:
|
||||
"""Return an attribute node for the current extension. This is useful
|
||||
to pass constants on extensions to generated template code.
|
||||
|
||||
@ -138,11 +136,11 @@ class Extension:
|
||||
def call_method(
|
||||
self,
|
||||
name: str,
|
||||
args: t.Optional[t.List[nodes.Expr]] = None,
|
||||
kwargs: t.Optional[t.List[nodes.Keyword]] = None,
|
||||
dyn_args: t.Optional[nodes.Expr] = None,
|
||||
dyn_kwargs: t.Optional[nodes.Expr] = None,
|
||||
lineno: t.Optional[int] = None,
|
||||
args: list[nodes.Expr] | None = None,
|
||||
kwargs: list[nodes.Keyword] | None = None,
|
||||
dyn_args: nodes.Expr | None = None,
|
||||
dyn_kwargs: nodes.Expr | None = None,
|
||||
lineno: int | None = None,
|
||||
) -> nodes.Call:
|
||||
"""Call a method of the extension. This is a shortcut for
|
||||
:meth:`attr` + :class:`jinja2.nodes.Call`.
|
||||
@ -164,7 +162,7 @@ class Extension:
|
||||
@pass_context
|
||||
def _gettext_alias(
|
||||
__context: Context, *args: t.Any, **kwargs: t.Any
|
||||
) -> t.Union[t.Any, Undefined]:
|
||||
) -> t.Any | Undefined:
|
||||
return __context.call(__context.resolve("gettext"), *args, **kwargs)
|
||||
|
||||
|
||||
@ -268,7 +266,7 @@ class InternationalizationExtension(Extension):
|
||||
)
|
||||
|
||||
def _install(
|
||||
self, translations: "_SupportedTranslations", newstyle: t.Optional[bool] = None
|
||||
self, translations: "_SupportedTranslations", newstyle: bool | None = None
|
||||
) -> None:
|
||||
# ugettext and ungettext are preferred in case the I18N library
|
||||
# is providing compatibility with older Python versions.
|
||||
@ -285,7 +283,7 @@ class InternationalizationExtension(Extension):
|
||||
gettext, ngettext, newstyle=newstyle, pgettext=pgettext, npgettext=npgettext
|
||||
)
|
||||
|
||||
def _install_null(self, newstyle: t.Optional[bool] = None) -> None:
|
||||
def _install_null(self, newstyle: bool | None = None) -> None:
|
||||
import gettext
|
||||
|
||||
translations = gettext.NullTranslations()
|
||||
@ -301,9 +299,9 @@ class InternationalizationExtension(Extension):
|
||||
self,
|
||||
gettext: t.Callable[[str], str],
|
||||
ngettext: t.Callable[[str, str, int], str],
|
||||
newstyle: t.Optional[bool] = None,
|
||||
pgettext: t.Optional[t.Callable[[str, str], str]] = None,
|
||||
npgettext: t.Optional[t.Callable[[str, str, str, int], str]] = None,
|
||||
newstyle: bool | None = None,
|
||||
pgettext: t.Callable[[str, str], str] | None = None,
|
||||
npgettext: t.Callable[[str, str, str, int], str] | None = None,
|
||||
) -> None:
|
||||
if newstyle is not None:
|
||||
self.environment.newstyle_gettext = newstyle # type: ignore
|
||||
@ -327,16 +325,14 @@ class InternationalizationExtension(Extension):
|
||||
|
||||
def _extract(
|
||||
self,
|
||||
source: t.Union[str, nodes.Template],
|
||||
source: str | nodes.Template,
|
||||
gettext_functions: t.Sequence[str] = GETTEXT_FUNCTIONS,
|
||||
) -> t.Iterator[
|
||||
t.Tuple[int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]]
|
||||
]:
|
||||
) -> t.Iterator[tuple[int, str, str | None | tuple[str | None, ...]]]:
|
||||
if isinstance(source, str):
|
||||
source = self.environment.parse(source)
|
||||
return extract_from_ast(source, gettext_functions)
|
||||
|
||||
def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
|
||||
def parse(self, parser: "Parser") -> nodes.Node | list[nodes.Node]:
|
||||
"""Parse a translatable tag."""
|
||||
lineno = next(parser.stream).lineno
|
||||
|
||||
@ -349,10 +345,10 @@ class InternationalizationExtension(Extension):
|
||||
# find all the variables referenced. Additionally a variable can be
|
||||
# defined in the body of the trans block too, but this is checked at
|
||||
# a later state.
|
||||
plural_expr: t.Optional[nodes.Expr] = None
|
||||
plural_expr_assignment: t.Optional[nodes.Assign] = None
|
||||
plural_expr: nodes.Expr | None = None
|
||||
plural_expr_assignment: nodes.Assign | None = None
|
||||
num_called_num = False
|
||||
variables: t.Dict[str, nodes.Expr] = {}
|
||||
variables: dict[str, nodes.Expr] = {}
|
||||
trimmed = None
|
||||
while parser.stream.current.type != "block_end":
|
||||
if variables:
|
||||
@ -463,7 +459,7 @@ class InternationalizationExtension(Extension):
|
||||
|
||||
def _parse_block(
|
||||
self, parser: "Parser", allow_pluralize: bool
|
||||
) -> t.Tuple[t.List[str], str]:
|
||||
) -> tuple[list[str], str]:
|
||||
"""Parse until the next block tag with a given name."""
|
||||
referenced = []
|
||||
buf = []
|
||||
@ -511,10 +507,10 @@ class InternationalizationExtension(Extension):
|
||||
def _make_node(
|
||||
self,
|
||||
singular: str,
|
||||
plural: t.Optional[str],
|
||||
context: t.Optional[str],
|
||||
variables: t.Dict[str, nodes.Expr],
|
||||
plural_expr: t.Optional[nodes.Expr],
|
||||
plural: str | None,
|
||||
context: str | None,
|
||||
variables: dict[str, nodes.Expr],
|
||||
plural_expr: nodes.Expr | None,
|
||||
vars_referenced: bool,
|
||||
num_called_num: bool,
|
||||
) -> nodes.Output:
|
||||
@ -530,7 +526,7 @@ class InternationalizationExtension(Extension):
|
||||
plural = plural.replace("%%", "%")
|
||||
|
||||
func_name = "gettext"
|
||||
func_args: t.List[nodes.Expr] = [nodes.Const(singular)]
|
||||
func_args: list[nodes.Expr] = [nodes.Const(singular)]
|
||||
|
||||
if context is not None:
|
||||
func_args.insert(0, nodes.Const(context))
|
||||
@ -589,7 +585,7 @@ class LoopControlExtension(Extension):
|
||||
|
||||
tags = {"break", "continue"}
|
||||
|
||||
def parse(self, parser: "Parser") -> t.Union[nodes.Break, nodes.Continue]:
|
||||
def parse(self, parser: "Parser") -> nodes.Break | nodes.Continue:
|
||||
token = next(parser.stream)
|
||||
if token.value == "break":
|
||||
return nodes.Break(lineno=token.lineno)
|
||||
@ -640,9 +636,7 @@ def extract_from_ast(
|
||||
ast: nodes.Template,
|
||||
gettext_functions: t.Sequence[str] = GETTEXT_FUNCTIONS,
|
||||
babel_style: bool = True,
|
||||
) -> t.Iterator[
|
||||
t.Tuple[int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]]
|
||||
]:
|
||||
) -> t.Iterator[tuple[int, str, str | None | tuple[str | None, ...]]]:
|
||||
"""Extract localizable strings from the given template node. Per
|
||||
default this function returns matches in babel style that means non string
|
||||
parameters as well as keyword arguments are returned as `None`. This
|
||||
@ -677,7 +671,7 @@ def extract_from_ast(
|
||||
to extract any comments. For comment support you have to use the babel
|
||||
extraction interface or extract comments yourself.
|
||||
"""
|
||||
out: t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]
|
||||
out: str | None | tuple[str | None, ...]
|
||||
|
||||
for node in ast.find_all(nodes.Call):
|
||||
if (
|
||||
@ -686,7 +680,7 @@ def extract_from_ast(
|
||||
):
|
||||
continue
|
||||
|
||||
strings: t.List[t.Optional[str]] = []
|
||||
strings: list[str | None] = []
|
||||
|
||||
for arg in node.args:
|
||||
if isinstance(arg, nodes.Const) and isinstance(arg.value, str):
|
||||
@ -723,14 +717,14 @@ class _CommentFinder:
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, tokens: t.Sequence[t.Tuple[int, str, str]], comment_tags: t.Sequence[str]
|
||||
self, tokens: t.Sequence[tuple[int, str, str]], comment_tags: t.Sequence[str]
|
||||
) -> None:
|
||||
self.tokens = tokens
|
||||
self.comment_tags = comment_tags
|
||||
self.offset = 0
|
||||
self.last_lineno = 0
|
||||
|
||||
def find_backwards(self, offset: int) -> t.List[str]:
|
||||
def find_backwards(self, offset: int) -> list[str]:
|
||||
try:
|
||||
for _, token_type, token_value in reversed(
|
||||
self.tokens[self.offset : offset]
|
||||
@ -746,7 +740,7 @@ class _CommentFinder:
|
||||
finally:
|
||||
self.offset = offset
|
||||
|
||||
def find_comments(self, lineno: int) -> t.List[str]:
|
||||
def find_comments(self, lineno: int) -> list[str]:
|
||||
if not self.comment_tags or self.last_lineno > lineno:
|
||||
return []
|
||||
for idx, (token_lineno, _, _) in enumerate(self.tokens[self.offset :]):
|
||||
@ -759,12 +753,8 @@ def babel_extract(
|
||||
fileobj: t.BinaryIO,
|
||||
keywords: t.Sequence[str],
|
||||
comment_tags: t.Sequence[str],
|
||||
options: t.Dict[str, t.Any],
|
||||
) -> t.Iterator[
|
||||
t.Tuple[
|
||||
int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]], t.List[str]
|
||||
]
|
||||
]:
|
||||
options: dict[str, t.Any],
|
||||
) -> t.Iterator[tuple[int, str, str | None | tuple[str | None, ...], list[str]]]:
|
||||
"""Babel extraction method for Jinja templates.
|
||||
|
||||
.. versionchanged:: 2.3
|
||||
@ -792,7 +782,7 @@ def babel_extract(
|
||||
:return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
|
||||
(comments will be empty currently)
|
||||
"""
|
||||
extensions: t.Dict[t.Type[Extension], None] = {}
|
||||
extensions: dict[type[Extension], None] = {}
|
||||
|
||||
for extension_name in options.get("extensions", "").split(","):
|
||||
extension_name = extension_name.strip()
|
||||
|
||||
@ -6,6 +6,7 @@ import re
|
||||
import typing
|
||||
import typing as t
|
||||
from collections import abc
|
||||
from inspect import getattr_static
|
||||
from itertools import chain
|
||||
from itertools import groupby
|
||||
|
||||
@ -56,9 +57,9 @@ def ignore_case(value: V) -> V:
|
||||
|
||||
def make_attrgetter(
|
||||
environment: "Environment",
|
||||
attribute: t.Optional[t.Union[str, int]],
|
||||
postprocess: t.Optional[t.Callable[[t.Any], t.Any]] = None,
|
||||
default: t.Optional[t.Any] = None,
|
||||
attribute: str | int | None,
|
||||
postprocess: t.Callable[[t.Any], t.Any] | None = None,
|
||||
default: t.Any | None = None,
|
||||
) -> t.Callable[[t.Any], t.Any]:
|
||||
"""Returns a callable that looks up the given attribute from a
|
||||
passed object with the rules of the environment. Dots are allowed
|
||||
@ -84,9 +85,9 @@ def make_attrgetter(
|
||||
|
||||
def make_multi_attrgetter(
|
||||
environment: "Environment",
|
||||
attribute: t.Optional[t.Union[str, int]],
|
||||
postprocess: t.Optional[t.Callable[[t.Any], t.Any]] = None,
|
||||
) -> t.Callable[[t.Any], t.List[t.Any]]:
|
||||
attribute: str | int | None,
|
||||
postprocess: t.Callable[[t.Any], t.Any] | None = None,
|
||||
) -> t.Callable[[t.Any], list[t.Any]]:
|
||||
"""Returns a callable that looks up the given comma separated
|
||||
attributes from a passed object with the rules of the environment.
|
||||
Dots are allowed to access attributes of each attribute. Integer
|
||||
@ -98,13 +99,13 @@ def make_multi_attrgetter(
|
||||
Examples of attribute: "attr1,attr2", "attr1.inner1.0,attr2.inner2.0", etc.
|
||||
"""
|
||||
if isinstance(attribute, str):
|
||||
split: t.Sequence[t.Union[str, int, None]] = attribute.split(",")
|
||||
split: t.Sequence[str | int | None] = attribute.split(",")
|
||||
else:
|
||||
split = [attribute]
|
||||
|
||||
parts = [_prepare_attribute_parts(item) for item in split]
|
||||
|
||||
def attrgetter(item: t.Any) -> t.List[t.Any]:
|
||||
def attrgetter(item: t.Any) -> list[t.Any]:
|
||||
items = [None] * len(parts)
|
||||
|
||||
for i, attribute_part in enumerate(parts):
|
||||
@ -124,8 +125,8 @@ def make_multi_attrgetter(
|
||||
|
||||
|
||||
def _prepare_attribute_parts(
|
||||
attr: t.Optional[t.Union[str, int]],
|
||||
) -> t.List[t.Union[str, int]]:
|
||||
attr: str | int | None,
|
||||
) -> list[str | int]:
|
||||
if attr is None:
|
||||
return []
|
||||
|
||||
@ -135,7 +136,7 @@ def _prepare_attribute_parts(
|
||||
return [attr]
|
||||
|
||||
|
||||
def do_forceescape(value: "t.Union[str, HasHTML]") -> Markup:
|
||||
def do_forceescape(value: "str | HasHTML") -> Markup:
|
||||
"""Enforce HTML escaping. This will probably double escape variables."""
|
||||
if hasattr(value, "__html__"):
|
||||
value = t.cast("HasHTML", value).__html__()
|
||||
@ -144,7 +145,7 @@ def do_forceescape(value: "t.Union[str, HasHTML]") -> Markup:
|
||||
|
||||
|
||||
def do_urlencode(
|
||||
value: t.Union[str, t.Mapping[str, t.Any], t.Iterable[t.Tuple[str, t.Any]]],
|
||||
value: str | t.Mapping[str, t.Any] | t.Iterable[tuple[str, t.Any]],
|
||||
) -> str:
|
||||
"""Quote data for use in a URL path or query using UTF-8.
|
||||
|
||||
@ -165,7 +166,7 @@ def do_urlencode(
|
||||
return url_quote(value)
|
||||
|
||||
if isinstance(value, dict):
|
||||
items: t.Iterable[t.Tuple[str, t.Any]] = value.items()
|
||||
items: t.Iterable[tuple[str, t.Any]] = value.items()
|
||||
else:
|
||||
items = value # type: ignore
|
||||
|
||||
@ -176,7 +177,7 @@ def do_urlencode(
|
||||
|
||||
@pass_eval_context
|
||||
def do_replace(
|
||||
eval_ctx: "EvalContext", s: str, old: str, new: str, count: t.Optional[int] = None
|
||||
eval_ctx: "EvalContext", s: str, old: str, new: str, count: int | None = None
|
||||
) -> str:
|
||||
"""Return a copy of the value with all occurrences of a substring
|
||||
replaced with a new one. The first argument is the substring
|
||||
@ -220,7 +221,7 @@ def do_lower(s: str) -> str:
|
||||
return soft_str(s).lower()
|
||||
|
||||
|
||||
def do_items(value: t.Union[t.Mapping[K, V], Undefined]) -> t.Iterator[t.Tuple[K, V]]:
|
||||
def do_items(value: t.Mapping[K, V] | Undefined) -> t.Iterator[tuple[K, V]]:
|
||||
"""Return an iterator over the ``(key, value)`` items of a mapping.
|
||||
|
||||
``x|items`` is the same as ``x.items()``, except if ``x`` is
|
||||
@ -345,7 +346,7 @@ def do_dictsort(
|
||||
case_sensitive: bool = False,
|
||||
by: 'te.Literal["key", "value"]' = "key",
|
||||
reverse: bool = False,
|
||||
) -> t.List[t.Tuple[K, V]]:
|
||||
) -> list[tuple[K, V]]:
|
||||
"""Sort a dict and yield (key, value) pairs. Python dicts may not
|
||||
be in the order you want to display them in, so sort them first.
|
||||
|
||||
@ -370,7 +371,7 @@ def do_dictsort(
|
||||
else:
|
||||
raise FilterArgumentError('You can only sort by either "key" or "value"')
|
||||
|
||||
def sort_func(item: t.Tuple[t.Any, t.Any]) -> t.Any:
|
||||
def sort_func(item: tuple[t.Any, t.Any]) -> t.Any:
|
||||
value = item[pos]
|
||||
|
||||
if not case_sensitive:
|
||||
@ -387,8 +388,8 @@ def do_sort(
|
||||
value: "t.Iterable[V]",
|
||||
reverse: bool = False,
|
||||
case_sensitive: bool = False,
|
||||
attribute: t.Optional[t.Union[str, int]] = None,
|
||||
) -> "t.List[V]":
|
||||
attribute: str | int | None = None,
|
||||
) -> "list[V]":
|
||||
"""Sort an iterable using Python's :func:`sorted`.
|
||||
|
||||
.. sourcecode:: jinja
|
||||
@ -438,11 +439,11 @@ def do_sort(
|
||||
|
||||
|
||||
@pass_environment
|
||||
def do_unique(
|
||||
def sync_do_unique(
|
||||
environment: "Environment",
|
||||
value: "t.Iterable[V]",
|
||||
case_sensitive: bool = False,
|
||||
attribute: t.Optional[t.Union[str, int]] = None,
|
||||
attribute: str | int | None = None,
|
||||
) -> "t.Iterator[V]":
|
||||
"""Returns a list of unique items from the given iterable.
|
||||
|
||||
@ -470,13 +471,25 @@ def do_unique(
|
||||
yield item
|
||||
|
||||
|
||||
@async_variant(sync_do_unique) # type: ignore
|
||||
async def do_unique(
|
||||
environment: "Environment",
|
||||
value: "t.AsyncIterable[V] | t.Iterable[V]",
|
||||
case_sensitive: bool = False,
|
||||
attribute: str | int | None = None,
|
||||
) -> "t.Iterator[V]":
|
||||
return sync_do_unique(
|
||||
environment, await auto_to_list(value), case_sensitive, attribute
|
||||
)
|
||||
|
||||
|
||||
def _min_or_max(
|
||||
environment: "Environment",
|
||||
value: "t.Iterable[V]",
|
||||
func: "t.Callable[..., V]",
|
||||
case_sensitive: bool,
|
||||
attribute: t.Optional[t.Union[str, int]],
|
||||
) -> "t.Union[V, Undefined]":
|
||||
attribute: str | int | None,
|
||||
) -> "V | Undefined":
|
||||
it = iter(value)
|
||||
|
||||
try:
|
||||
@ -495,8 +508,8 @@ def do_min(
|
||||
environment: "Environment",
|
||||
value: "t.Iterable[V]",
|
||||
case_sensitive: bool = False,
|
||||
attribute: t.Optional[t.Union[str, int]] = None,
|
||||
) -> "t.Union[V, Undefined]":
|
||||
attribute: str | int | None = None,
|
||||
) -> "V | Undefined":
|
||||
"""Return the smallest item from the sequence.
|
||||
|
||||
.. sourcecode:: jinja
|
||||
@ -515,8 +528,8 @@ def do_max(
|
||||
environment: "Environment",
|
||||
value: "t.Iterable[V]",
|
||||
case_sensitive: bool = False,
|
||||
attribute: t.Optional[t.Union[str, int]] = None,
|
||||
) -> "t.Union[V, Undefined]":
|
||||
attribute: str | int | None = None,
|
||||
) -> "V | Undefined":
|
||||
"""Return the largest item from the sequence.
|
||||
|
||||
.. sourcecode:: jinja
|
||||
@ -568,7 +581,7 @@ def sync_do_join(
|
||||
eval_ctx: "EvalContext",
|
||||
value: t.Iterable[t.Any],
|
||||
d: str = "",
|
||||
attribute: t.Optional[t.Union[str, int]] = None,
|
||||
attribute: str | int | None = None,
|
||||
) -> str:
|
||||
"""Return a string which is the concatenation of the strings in the
|
||||
sequence. The separator between elements is an empty string per
|
||||
@ -624,9 +637,9 @@ def sync_do_join(
|
||||
@async_variant(sync_do_join) # type: ignore
|
||||
async def do_join(
|
||||
eval_ctx: "EvalContext",
|
||||
value: t.Union[t.AsyncIterable[t.Any], t.Iterable[t.Any]],
|
||||
value: t.AsyncIterable[t.Any] | t.Iterable[t.Any],
|
||||
d: str = "",
|
||||
attribute: t.Optional[t.Union[str, int]] = None,
|
||||
attribute: str | int | None = None,
|
||||
) -> str:
|
||||
return sync_do_join(eval_ctx, await auto_to_list(value), d, attribute)
|
||||
|
||||
@ -637,9 +650,7 @@ def do_center(value: str, width: int = 80) -> str:
|
||||
|
||||
|
||||
@pass_environment
|
||||
def sync_do_first(
|
||||
environment: "Environment", seq: "t.Iterable[V]"
|
||||
) -> "t.Union[V, Undefined]":
|
||||
def sync_do_first(environment: "Environment", seq: "t.Iterable[V]") -> "V | Undefined":
|
||||
"""Return the first item of a sequence."""
|
||||
try:
|
||||
return next(iter(seq))
|
||||
@ -649,8 +660,8 @@ def sync_do_first(
|
||||
|
||||
@async_variant(sync_do_first) # type: ignore
|
||||
async def do_first(
|
||||
environment: "Environment", seq: "t.Union[t.AsyncIterable[V], t.Iterable[V]]"
|
||||
) -> "t.Union[V, Undefined]":
|
||||
environment: "Environment", seq: "t.AsyncIterable[V] | t.Iterable[V]"
|
||||
) -> "V | Undefined":
|
||||
try:
|
||||
return await auto_aiter(seq).__anext__()
|
||||
except StopAsyncIteration:
|
||||
@ -658,9 +669,7 @@ async def do_first(
|
||||
|
||||
|
||||
@pass_environment
|
||||
def do_last(
|
||||
environment: "Environment", seq: "t.Reversible[V]"
|
||||
) -> "t.Union[V, Undefined]":
|
||||
def do_last(environment: "Environment", seq: "t.Reversible[V]") -> "V | Undefined":
|
||||
"""Return the last item of a sequence.
|
||||
|
||||
Note: Does not work with generators. You may want to explicitly
|
||||
@ -680,7 +689,7 @@ def do_last(
|
||||
|
||||
|
||||
@pass_context
|
||||
def do_random(context: "Context", seq: "t.Sequence[V]") -> "t.Union[V, Undefined]":
|
||||
def do_random(context: "Context", seq: "t.Sequence[V]") -> "V | Undefined":
|
||||
"""Return a random item from the sequence."""
|
||||
try:
|
||||
return random.choice(seq)
|
||||
@ -688,7 +697,7 @@ def do_random(context: "Context", seq: "t.Sequence[V]") -> "t.Union[V, Undefined
|
||||
return context.environment.undefined("No random item, sequence was empty.")
|
||||
|
||||
|
||||
def do_filesizeformat(value: t.Union[str, float, int], binary: bool = False) -> str:
|
||||
def do_filesizeformat(value: str | float | int, binary: bool = False) -> str:
|
||||
"""Format the value like a 'human-readable' file size (i.e. 13 kB,
|
||||
4.1 MB, 102 Bytes, etc). Per default decimal prefixes are used (Mega,
|
||||
Giga, etc.), if the second parameter is set to `True` the binary
|
||||
@ -733,11 +742,11 @@ _uri_scheme_re = re.compile(r"^([\w.+-]{2,}:(/){0,2})$")
|
||||
def do_urlize(
|
||||
eval_ctx: "EvalContext",
|
||||
value: str,
|
||||
trim_url_limit: t.Optional[int] = None,
|
||||
trim_url_limit: int | None = None,
|
||||
nofollow: bool = False,
|
||||
target: t.Optional[str] = None,
|
||||
rel: t.Optional[str] = None,
|
||||
extra_schemes: t.Optional[t.Iterable[str]] = None,
|
||||
target: str | None = None,
|
||||
rel: str | None = None,
|
||||
extra_schemes: t.Iterable[str] | None = None,
|
||||
) -> str:
|
||||
"""Convert URLs in text into clickable links.
|
||||
|
||||
@ -810,7 +819,7 @@ def do_urlize(
|
||||
|
||||
|
||||
def do_indent(
|
||||
s: str, width: t.Union[int, str] = 4, first: bool = False, blank: bool = False
|
||||
s: str, width: int | str = 4, first: bool = False, blank: bool = False
|
||||
) -> str:
|
||||
"""Return a copy of the string with each line indented by 4 spaces. The
|
||||
first line and blank lines are not indented by default.
|
||||
@ -864,7 +873,7 @@ def do_truncate(
|
||||
length: int = 255,
|
||||
killwords: bool = False,
|
||||
end: str = "...",
|
||||
leeway: t.Optional[int] = None,
|
||||
leeway: int | None = None,
|
||||
) -> str:
|
||||
"""Return a truncated copy of the string. The length is specified
|
||||
with the first parameter which defaults to ``255``. If the second
|
||||
@ -911,7 +920,7 @@ def do_wordwrap(
|
||||
s: str,
|
||||
width: int = 79,
|
||||
break_long_words: bool = True,
|
||||
wrapstring: t.Optional[str] = None,
|
||||
wrapstring: str | None = None,
|
||||
break_on_hyphens: bool = True,
|
||||
) -> str:
|
||||
"""Wrap a string to the given width. Existing newlines are treated
|
||||
@ -987,7 +996,7 @@ def do_int(value: t.Any, default: int = 0, base: int = 10) -> int:
|
||||
# this quirk is necessary so that "42.23"|int gives 42.
|
||||
try:
|
||||
return int(float(value))
|
||||
except (TypeError, ValueError):
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return default
|
||||
|
||||
|
||||
@ -1030,12 +1039,12 @@ def do_format(value: str, *args: t.Any, **kwargs: t.Any) -> str:
|
||||
return soft_str(value) % (kwargs or args)
|
||||
|
||||
|
||||
def do_trim(value: str, chars: t.Optional[str] = None) -> str:
|
||||
def do_trim(value: str, chars: str | None = None) -> str:
|
||||
"""Strip leading and trailing characters, by default whitespace."""
|
||||
return soft_str(value).strip(chars)
|
||||
|
||||
|
||||
def do_striptags(value: "t.Union[str, HasHTML]") -> str:
|
||||
def do_striptags(value: "str | HasHTML") -> str:
|
||||
"""Strip SGML/XML tags and replace adjacent whitespace by one space."""
|
||||
if hasattr(value, "__html__"):
|
||||
value = t.cast("HasHTML", value).__html__()
|
||||
@ -1044,8 +1053,8 @@ def do_striptags(value: "t.Union[str, HasHTML]") -> str:
|
||||
|
||||
|
||||
def sync_do_slice(
|
||||
value: "t.Collection[V]", slices: int, fill_with: "t.Optional[V]" = None
|
||||
) -> "t.Iterator[t.List[V]]":
|
||||
value: "t.Collection[V]", slices: int, fill_with: "V | None" = None
|
||||
) -> "t.Iterator[list[V]]":
|
||||
"""Slice an iterator and return a list of lists containing
|
||||
those items. Useful if you want to create a div containing
|
||||
three ul tags that represent columns:
|
||||
@ -1088,16 +1097,16 @@ def sync_do_slice(
|
||||
|
||||
@async_variant(sync_do_slice) # type: ignore
|
||||
async def do_slice(
|
||||
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||
value: "t.AsyncIterable[V] | t.Iterable[V]",
|
||||
slices: int,
|
||||
fill_with: t.Optional[t.Any] = None,
|
||||
) -> "t.Iterator[t.List[V]]":
|
||||
fill_with: t.Any | None = None,
|
||||
) -> "t.Iterator[list[V]]":
|
||||
return sync_do_slice(await auto_to_list(value), slices, fill_with)
|
||||
|
||||
|
||||
def do_batch(
|
||||
value: "t.Iterable[V]", linecount: int, fill_with: "t.Optional[V]" = None
|
||||
) -> "t.Iterator[t.List[V]]":
|
||||
value: "t.Iterable[V]", linecount: int, fill_with: "V | None" = None
|
||||
) -> "t.Iterator[list[V]]":
|
||||
"""
|
||||
A filter that batches items. It works pretty much like `slice`
|
||||
just the other way round. It returns a list of lists with the
|
||||
@ -1116,7 +1125,7 @@ def do_batch(
|
||||
{%- endfor %}
|
||||
</table>
|
||||
"""
|
||||
tmp: "t.List[V]" = []
|
||||
tmp: list[V] = []
|
||||
|
||||
for item in value:
|
||||
if len(tmp) == linecount:
|
||||
@ -1174,7 +1183,7 @@ def do_round(
|
||||
|
||||
class _GroupTuple(t.NamedTuple):
|
||||
grouper: t.Any
|
||||
list: t.List[t.Any]
|
||||
list: list[t.Any]
|
||||
|
||||
# Use the regular tuple repr to hide this subclass if users print
|
||||
# out the value during debugging.
|
||||
@ -1189,10 +1198,10 @@ class _GroupTuple(t.NamedTuple):
|
||||
def sync_do_groupby(
|
||||
environment: "Environment",
|
||||
value: "t.Iterable[V]",
|
||||
attribute: t.Union[str, int],
|
||||
default: t.Optional[t.Any] = None,
|
||||
attribute: str | int,
|
||||
default: t.Any | None = None,
|
||||
case_sensitive: bool = False,
|
||||
) -> "t.List[_GroupTuple]":
|
||||
) -> "list[_GroupTuple]":
|
||||
"""Group a sequence of objects by an attribute using Python's
|
||||
:func:`itertools.groupby`. The attribute can use dot notation for
|
||||
nested access, like ``"address.city"``. Unlike Python's ``groupby``,
|
||||
@ -1272,11 +1281,11 @@ def sync_do_groupby(
|
||||
@async_variant(sync_do_groupby) # type: ignore
|
||||
async def do_groupby(
|
||||
environment: "Environment",
|
||||
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||
attribute: t.Union[str, int],
|
||||
default: t.Optional[t.Any] = None,
|
||||
value: "t.AsyncIterable[V] | t.Iterable[V]",
|
||||
attribute: str | int,
|
||||
default: t.Any | None = None,
|
||||
case_sensitive: bool = False,
|
||||
) -> "t.List[_GroupTuple]":
|
||||
) -> "list[_GroupTuple]":
|
||||
expr = make_attrgetter(
|
||||
environment,
|
||||
attribute,
|
||||
@ -1300,7 +1309,7 @@ async def do_groupby(
|
||||
def sync_do_sum(
|
||||
environment: "Environment",
|
||||
iterable: "t.Iterable[V]",
|
||||
attribute: t.Optional[t.Union[str, int]] = None,
|
||||
attribute: str | int | None = None,
|
||||
start: V = 0, # type: ignore
|
||||
) -> V:
|
||||
"""Returns the sum of a sequence of numbers plus the value of parameter
|
||||
@ -1326,8 +1335,8 @@ def sync_do_sum(
|
||||
@async_variant(sync_do_sum) # type: ignore
|
||||
async def do_sum(
|
||||
environment: "Environment",
|
||||
iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||
attribute: t.Optional[t.Union[str, int]] = None,
|
||||
iterable: "t.AsyncIterable[V] | t.Iterable[V]",
|
||||
attribute: str | int | None = None,
|
||||
start: V = 0, # type: ignore
|
||||
) -> V:
|
||||
rv = start
|
||||
@ -1345,7 +1354,7 @@ async def do_sum(
|
||||
return rv
|
||||
|
||||
|
||||
def sync_do_list(value: "t.Iterable[V]") -> "t.List[V]":
|
||||
def sync_do_list(value: "t.Iterable[V]") -> "list[V]":
|
||||
"""Convert the value into a list. If it was a string the returned list
|
||||
will be a list of characters.
|
||||
"""
|
||||
@ -1353,7 +1362,7 @@ def sync_do_list(value: "t.Iterable[V]") -> "t.List[V]":
|
||||
|
||||
|
||||
@async_variant(sync_do_list) # type: ignore
|
||||
async def do_list(value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]") -> "t.List[V]":
|
||||
async def do_list(value: "t.AsyncIterable[V] | t.Iterable[V]") -> "list[V]":
|
||||
return await auto_to_list(value)
|
||||
|
||||
|
||||
@ -1377,7 +1386,7 @@ def do_reverse(value: str) -> str: ...
|
||||
def do_reverse(value: "t.Iterable[V]") -> "t.Iterable[V]": ...
|
||||
|
||||
|
||||
def do_reverse(value: t.Union[str, t.Iterable[V]]) -> t.Union[str, t.Iterable[V]]:
|
||||
def do_reverse(value: str | t.Iterable[V]) -> str | t.Iterable[V]:
|
||||
"""Reverse the object or return an iterator that iterates over it the other
|
||||
way round.
|
||||
"""
|
||||
@ -1396,34 +1405,26 @@ def do_reverse(value: t.Union[str, t.Iterable[V]]) -> t.Union[str, t.Iterable[V]
|
||||
|
||||
|
||||
@pass_environment
|
||||
def do_attr(
|
||||
environment: "Environment", obj: t.Any, name: str
|
||||
) -> t.Union[Undefined, t.Any]:
|
||||
"""Get an attribute of an object. ``foo|attr("bar")`` works like
|
||||
``foo.bar`` just that always an attribute is returned and items are not
|
||||
looked up.
|
||||
def do_attr(environment: "Environment", obj: t.Any, name: str) -> Undefined | t.Any:
|
||||
"""Get an attribute of an object. ``foo|attr("bar")`` works like
|
||||
``foo.bar``, but returns undefined instead of falling back to ``foo["bar"]``
|
||||
if the attribute doesn't exist.
|
||||
|
||||
See :ref:`Notes on subscriptions <notes-on-subscriptions>` for more details.
|
||||
"""
|
||||
# Environment.getattr will fall back to obj[name] if obj.name doesn't exist.
|
||||
# But we want to call env.getattr to get behavior such as sandboxing.
|
||||
# Determine if the attr exists first, so we know the fallback won't trigger.
|
||||
try:
|
||||
name = str(name)
|
||||
except UnicodeError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
value = getattr(obj, name)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if environment.sandboxed:
|
||||
environment = t.cast("SandboxedEnvironment", environment)
|
||||
# This avoids executing properties/descriptors, but misses __getattr__
|
||||
# and __getattribute__ dynamic attrs.
|
||||
getattr_static(obj, name)
|
||||
except AttributeError:
|
||||
# This finds dynamic attrs, and we know it's not a descriptor at this point.
|
||||
if not hasattr(obj, name):
|
||||
return environment.undefined(obj=obj, name=name)
|
||||
|
||||
if not environment.is_safe_attribute(obj, name, value):
|
||||
return environment.unsafe_undefined(obj, name)
|
||||
|
||||
return value
|
||||
|
||||
return environment.undefined(obj=obj, name=name)
|
||||
return environment.getattr(obj, name)
|
||||
|
||||
|
||||
@typing.overload
|
||||
@ -1442,7 +1443,7 @@ def sync_do_map(
|
||||
value: t.Iterable[t.Any],
|
||||
*,
|
||||
attribute: str = ...,
|
||||
default: t.Optional[t.Any] = None,
|
||||
default: t.Any | None = None,
|
||||
) -> t.Iterable[t.Any]: ...
|
||||
|
||||
|
||||
@ -1499,7 +1500,7 @@ def sync_do_map(
|
||||
@typing.overload
|
||||
def do_map(
|
||||
context: "Context",
|
||||
value: t.Union[t.AsyncIterable[t.Any], t.Iterable[t.Any]],
|
||||
value: t.AsyncIterable[t.Any] | t.Iterable[t.Any],
|
||||
name: str,
|
||||
*args: t.Any,
|
||||
**kwargs: t.Any,
|
||||
@ -1509,17 +1510,17 @@ def do_map(
|
||||
@typing.overload
|
||||
def do_map(
|
||||
context: "Context",
|
||||
value: t.Union[t.AsyncIterable[t.Any], t.Iterable[t.Any]],
|
||||
value: t.AsyncIterable[t.Any] | t.Iterable[t.Any],
|
||||
*,
|
||||
attribute: str = ...,
|
||||
default: t.Optional[t.Any] = None,
|
||||
default: t.Any | None = None,
|
||||
) -> t.Iterable[t.Any]: ...
|
||||
|
||||
|
||||
@async_variant(sync_do_map) # type: ignore
|
||||
async def do_map(
|
||||
context: "Context",
|
||||
value: t.Union[t.AsyncIterable[t.Any], t.Iterable[t.Any]],
|
||||
value: t.AsyncIterable[t.Any] | t.Iterable[t.Any],
|
||||
*args: t.Any,
|
||||
**kwargs: t.Any,
|
||||
) -> t.AsyncIterable[t.Any]:
|
||||
@ -1564,7 +1565,7 @@ def sync_do_select(
|
||||
@async_variant(sync_do_select) # type: ignore
|
||||
async def do_select(
|
||||
context: "Context",
|
||||
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||
value: "t.AsyncIterable[V] | t.Iterable[V]",
|
||||
*args: t.Any,
|
||||
**kwargs: t.Any,
|
||||
) -> "t.AsyncIterator[V]":
|
||||
@ -1600,7 +1601,7 @@ def sync_do_reject(
|
||||
@async_variant(sync_do_reject) # type: ignore
|
||||
async def do_reject(
|
||||
context: "Context",
|
||||
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||
value: "t.AsyncIterable[V] | t.Iterable[V]",
|
||||
*args: t.Any,
|
||||
**kwargs: t.Any,
|
||||
) -> "t.AsyncIterator[V]":
|
||||
@ -1629,8 +1630,8 @@ def sync_do_selectattr(
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
(u for user in users if user.is_active)
|
||||
(u for user in users if test_none(user.email))
|
||||
(user for user in users if user.is_active)
|
||||
(user for user in users if test_none(user.email))
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
@ -1640,7 +1641,7 @@ def sync_do_selectattr(
|
||||
@async_variant(sync_do_selectattr) # type: ignore
|
||||
async def do_selectattr(
|
||||
context: "Context",
|
||||
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||
value: "t.AsyncIterable[V] | t.Iterable[V]",
|
||||
*args: t.Any,
|
||||
**kwargs: t.Any,
|
||||
) -> "t.AsyncIterator[V]":
|
||||
@ -1667,8 +1668,8 @@ def sync_do_rejectattr(
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
(u for user in users if not user.is_active)
|
||||
(u for user in users if not test_none(user.email))
|
||||
(user for user in users if not user.is_active)
|
||||
(user for user in users if not test_none(user.email))
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
@ -1678,7 +1679,7 @@ def sync_do_rejectattr(
|
||||
@async_variant(sync_do_rejectattr) # type: ignore
|
||||
async def do_rejectattr(
|
||||
context: "Context",
|
||||
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||
value: "t.AsyncIterable[V] | t.Iterable[V]",
|
||||
*args: t.Any,
|
||||
**kwargs: t.Any,
|
||||
) -> "t.AsyncIterator[V]":
|
||||
@ -1687,7 +1688,7 @@ async def do_rejectattr(
|
||||
|
||||
@pass_eval_context
|
||||
def do_tojson(
|
||||
eval_ctx: "EvalContext", value: t.Any, indent: t.Optional[int] = None
|
||||
eval_ctx: "EvalContext", value: t.Any, indent: int | None = None
|
||||
) -> Markup:
|
||||
"""Serialize an object to a string of JSON, and mark it safe to
|
||||
render in HTML. This filter is only for use in HTML documents.
|
||||
@ -1715,7 +1716,7 @@ def do_tojson(
|
||||
|
||||
|
||||
def prepare_map(
|
||||
context: "Context", args: t.Tuple[t.Any, ...], kwargs: t.Dict[str, t.Any]
|
||||
context: "Context", args: tuple[t.Any, ...], kwargs: dict[str, t.Any]
|
||||
) -> t.Callable[[t.Any], t.Any]:
|
||||
if not args and "attribute" in kwargs:
|
||||
attribute = kwargs.pop("attribute")
|
||||
@ -1744,8 +1745,8 @@ def prepare_map(
|
||||
|
||||
def prepare_select_or_reject(
|
||||
context: "Context",
|
||||
args: t.Tuple[t.Any, ...],
|
||||
kwargs: t.Dict[str, t.Any],
|
||||
args: tuple[t.Any, ...],
|
||||
kwargs: dict[str, t.Any],
|
||||
modfunc: t.Callable[[t.Any], t.Any],
|
||||
lookup_attr: bool,
|
||||
) -> t.Callable[[t.Any], t.Any]:
|
||||
@ -1768,7 +1769,7 @@ def prepare_select_or_reject(
|
||||
args = args[1 + off :]
|
||||
|
||||
def func(item: t.Any) -> t.Any:
|
||||
return context.environment.call_test(name, item, args, kwargs)
|
||||
return context.environment.call_test(name, item, args, kwargs, context)
|
||||
|
||||
except LookupError:
|
||||
func = bool # type: ignore
|
||||
@ -1779,8 +1780,8 @@ def prepare_select_or_reject(
|
||||
def select_or_reject(
|
||||
context: "Context",
|
||||
value: "t.Iterable[V]",
|
||||
args: t.Tuple[t.Any, ...],
|
||||
kwargs: t.Dict[str, t.Any],
|
||||
args: tuple[t.Any, ...],
|
||||
kwargs: dict[str, t.Any],
|
||||
modfunc: t.Callable[[t.Any], t.Any],
|
||||
lookup_attr: bool,
|
||||
) -> "t.Iterator[V]":
|
||||
@ -1794,9 +1795,9 @@ def select_or_reject(
|
||||
|
||||
async def async_select_or_reject(
|
||||
context: "Context",
|
||||
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||
args: t.Tuple[t.Any, ...],
|
||||
kwargs: t.Dict[str, t.Any],
|
||||
value: "t.AsyncIterable[V] | t.Iterable[V]",
|
||||
args: tuple[t.Any, ...],
|
||||
kwargs: dict[str, t.Any],
|
||||
modfunc: t.Callable[[t.Any], t.Any],
|
||||
lookup_attr: bool,
|
||||
) -> "t.AsyncIterator[V]":
|
||||
|
||||
@ -3,6 +3,9 @@ import typing as t
|
||||
from . import nodes
|
||||
from .visitor import NodeVisitor
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
VAR_LOAD_PARAMETER = "param"
|
||||
VAR_LOAD_RESOLVE = "resolve"
|
||||
VAR_LOAD_ALIAS = "alias"
|
||||
@ -29,7 +32,7 @@ def symbols_for_node(
|
||||
|
||||
class Symbols:
|
||||
def __init__(
|
||||
self, parent: t.Optional["Symbols"] = None, level: t.Optional[int] = None
|
||||
self, parent: t.Optional["Symbols"] = None, level: int | None = None
|
||||
) -> None:
|
||||
if level is None:
|
||||
if parent is None:
|
||||
@ -39,24 +42,22 @@ class Symbols:
|
||||
|
||||
self.level: int = level
|
||||
self.parent = parent
|
||||
self.refs: t.Dict[str, str] = {}
|
||||
self.loads: t.Dict[str, t.Any] = {}
|
||||
self.stores: t.Set[str] = set()
|
||||
self.refs: dict[str, str] = {}
|
||||
self.loads: dict[str, t.Any] = {}
|
||||
self.stores: set[str] = set()
|
||||
|
||||
def analyze_node(self, node: nodes.Node, **kwargs: t.Any) -> None:
|
||||
visitor = RootVisitor(self)
|
||||
visitor.visit(node, **kwargs)
|
||||
|
||||
def _define_ref(
|
||||
self, name: str, load: t.Optional[t.Tuple[str, t.Optional[str]]] = None
|
||||
) -> str:
|
||||
def _define_ref(self, name: str, load: tuple[str, str | None] | None = None) -> str:
|
||||
ident = f"l_{self.level}_{name}"
|
||||
self.refs[name] = ident
|
||||
if load is not None:
|
||||
self.loads[ident] = load
|
||||
return ident
|
||||
|
||||
def find_load(self, target: str) -> t.Optional[t.Any]:
|
||||
def find_load(self, target: str) -> t.Any | None:
|
||||
if target in self.loads:
|
||||
return self.loads[target]
|
||||
|
||||
@ -65,7 +66,7 @@ class Symbols:
|
||||
|
||||
return None
|
||||
|
||||
def find_ref(self, name: str) -> t.Optional[str]:
|
||||
def find_ref(self, name: str) -> str | None:
|
||||
if name in self.refs:
|
||||
return self.refs[name]
|
||||
|
||||
@ -83,7 +84,7 @@ class Symbols:
|
||||
)
|
||||
return rv
|
||||
|
||||
def copy(self) -> "Symbols":
|
||||
def copy(self) -> "te.Self":
|
||||
rv = object.__new__(self.__class__)
|
||||
rv.__dict__.update(self.__dict__)
|
||||
rv.refs = self.refs.copy()
|
||||
@ -118,23 +119,20 @@ class Symbols:
|
||||
self._define_ref(name, load=(VAR_LOAD_RESOLVE, name))
|
||||
|
||||
def branch_update(self, branch_symbols: t.Sequence["Symbols"]) -> None:
|
||||
stores: t.Dict[str, int] = {}
|
||||
stores: set[str] = set()
|
||||
|
||||
for branch in branch_symbols:
|
||||
for target in branch.stores:
|
||||
if target in self.stores:
|
||||
continue
|
||||
stores[target] = stores.get(target, 0) + 1
|
||||
stores.update(branch.stores)
|
||||
|
||||
stores.difference_update(self.stores)
|
||||
|
||||
for sym in branch_symbols:
|
||||
self.refs.update(sym.refs)
|
||||
self.loads.update(sym.loads)
|
||||
self.stores.update(sym.stores)
|
||||
|
||||
for name, branch_count in stores.items():
|
||||
if branch_count == len(branch_symbols):
|
||||
continue
|
||||
|
||||
target = self.find_ref(name) # type: ignore
|
||||
for name in stores:
|
||||
target = self.find_ref(name)
|
||||
assert target is not None, "should not happen"
|
||||
|
||||
if self.parent is not None:
|
||||
@ -144,9 +142,9 @@ class Symbols:
|
||||
continue
|
||||
self.loads[target] = (VAR_LOAD_RESOLVE, name)
|
||||
|
||||
def dump_stores(self) -> t.Dict[str, str]:
|
||||
rv: t.Dict[str, str] = {}
|
||||
node: t.Optional["Symbols"] = self
|
||||
def dump_stores(self) -> dict[str, str]:
|
||||
rv: dict[str, str] = {}
|
||||
node: Symbols | None = self
|
||||
|
||||
while node is not None:
|
||||
for name in sorted(node.stores):
|
||||
@ -157,9 +155,9 @@ class Symbols:
|
||||
|
||||
return rv
|
||||
|
||||
def dump_param_targets(self) -> t.Set[str]:
|
||||
def dump_param_targets(self) -> set[str]:
|
||||
rv = set()
|
||||
node: t.Optional["Symbols"] = self
|
||||
node: Symbols | None = self
|
||||
|
||||
while node is not None:
|
||||
for target, (instr, _) in self.loads.items():
|
||||
|
||||
@ -21,7 +21,7 @@ if t.TYPE_CHECKING:
|
||||
|
||||
# cache for the lexers. Exists in order to be able to have multiple
|
||||
# environments with the same lexer
|
||||
_lexer_cache: t.MutableMapping[t.Tuple, "Lexer"] = LRUCache(50) # type: ignore
|
||||
_lexer_cache: t.MutableMapping[tuple, "Lexer"] = LRUCache(50) # type: ignore
|
||||
|
||||
# static regular expressions
|
||||
whitespace_re = re.compile(r"\s+")
|
||||
@ -210,7 +210,7 @@ def count_newlines(value: str) -> int:
|
||||
return len(newline_re.findall(value))
|
||||
|
||||
|
||||
def compile_rules(environment: "Environment") -> t.List[t.Tuple[str, str]]:
|
||||
def compile_rules(environment: "Environment") -> list[tuple[str, str]]:
|
||||
"""Compiles all the rules from the environment into a list of rules."""
|
||||
e = re.escape
|
||||
rules = [
|
||||
@ -257,12 +257,12 @@ class Failure:
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, message: str, cls: t.Type[TemplateSyntaxError] = TemplateSyntaxError
|
||||
self, message: str, cls: type[TemplateSyntaxError] = TemplateSyntaxError
|
||||
) -> None:
|
||||
self.message = message
|
||||
self.error_class = cls
|
||||
|
||||
def __call__(self, lineno: int, filename: str) -> "te.NoReturn":
|
||||
def __call__(self, lineno: int, filename: str | None) -> "te.NoReturn":
|
||||
raise self.error_class(self.message, lineno, filename)
|
||||
|
||||
|
||||
@ -325,11 +325,11 @@ class TokenStream:
|
||||
def __init__(
|
||||
self,
|
||||
generator: t.Iterable[Token],
|
||||
name: t.Optional[str],
|
||||
filename: t.Optional[str],
|
||||
name: str | None,
|
||||
filename: str | None,
|
||||
):
|
||||
self._iter = iter(generator)
|
||||
self._pushed: "te.Deque[Token]" = deque()
|
||||
self._pushed: deque[Token] = deque()
|
||||
self.name = name
|
||||
self.filename = filename
|
||||
self.closed = False
|
||||
@ -364,7 +364,7 @@ class TokenStream:
|
||||
for _ in range(n):
|
||||
next(self)
|
||||
|
||||
def next_if(self, expr: str) -> t.Optional[Token]:
|
||||
def next_if(self, expr: str) -> Token | None:
|
||||
"""Perform the token test and return the token if it matched.
|
||||
Otherwise the return value is `None`.
|
||||
"""
|
||||
@ -464,8 +464,8 @@ class OptionalLStrip(tuple): # type: ignore[type-arg]
|
||||
|
||||
class _Rule(t.NamedTuple):
|
||||
pattern: t.Pattern[str]
|
||||
tokens: t.Union[str, t.Tuple[str, ...], t.Tuple[Failure]]
|
||||
command: t.Optional[str]
|
||||
tokens: str | tuple[str, ...] | tuple[Failure]
|
||||
command: str | None
|
||||
|
||||
|
||||
class Lexer:
|
||||
@ -484,7 +484,7 @@ class Lexer:
|
||||
return re.compile(x, re.M | re.S)
|
||||
|
||||
# lexing rules for tags
|
||||
tag_rules: t.List[_Rule] = [
|
||||
tag_rules: list[_Rule] = [
|
||||
_Rule(whitespace_re, TOKEN_WHITESPACE, None),
|
||||
_Rule(float_re, TOKEN_FLOAT, None),
|
||||
_Rule(integer_re, TOKEN_INTEGER, None),
|
||||
@ -523,7 +523,7 @@ class Lexer:
|
||||
)
|
||||
|
||||
# global lexing rules
|
||||
self.rules: t.Dict[str, t.List[_Rule]] = {
|
||||
self.rules: dict[str, list[_Rule]] = {
|
||||
"root": [
|
||||
# directives
|
||||
_Rule(
|
||||
@ -604,9 +604,9 @@ class Lexer:
|
||||
def tokenize(
|
||||
self,
|
||||
source: str,
|
||||
name: t.Optional[str] = None,
|
||||
filename: t.Optional[str] = None,
|
||||
state: t.Optional[str] = None,
|
||||
name: str | None = None,
|
||||
filename: str | None = None,
|
||||
state: str | None = None,
|
||||
) -> TokenStream:
|
||||
"""Calls tokeniter + tokenize and wraps it in a token stream."""
|
||||
stream = self.tokeniter(source, name, filename, state)
|
||||
@ -614,9 +614,9 @@ class Lexer:
|
||||
|
||||
def wrap(
|
||||
self,
|
||||
stream: t.Iterable[t.Tuple[int, str, str]],
|
||||
name: t.Optional[str] = None,
|
||||
filename: t.Optional[str] = None,
|
||||
stream: t.Iterable[tuple[int, str, str]],
|
||||
name: str | None = None,
|
||||
filename: str | None = None,
|
||||
) -> t.Iterator[Token]:
|
||||
"""This is called with the stream as returned by `tokenize` and wraps
|
||||
every token in a :class:`Token` and converts the value.
|
||||
@ -669,10 +669,10 @@ class Lexer:
|
||||
def tokeniter(
|
||||
self,
|
||||
source: str,
|
||||
name: t.Optional[str],
|
||||
filename: t.Optional[str] = None,
|
||||
state: t.Optional[str] = None,
|
||||
) -> t.Iterator[t.Tuple[int, str, str]]:
|
||||
name: str | None,
|
||||
filename: str | None = None,
|
||||
state: str | None = None,
|
||||
) -> t.Iterator[tuple[int, str, str]]:
|
||||
"""This method tokenizes the text and returns the tokens in a
|
||||
generator. Use this method if you just want to tokenize a template.
|
||||
|
||||
@ -696,7 +696,7 @@ class Lexer:
|
||||
|
||||
statetokens = self.rules[stack[-1]]
|
||||
source_length = len(source)
|
||||
balancing_stack: t.List[str] = []
|
||||
balancing_stack: list[str] = []
|
||||
newlines_stripped = 0
|
||||
line_starting = True
|
||||
|
||||
@ -757,7 +757,7 @@ class Lexer:
|
||||
|
||||
for idx, token in enumerate(tokens):
|
||||
# failure group
|
||||
if token.__class__ is Failure:
|
||||
if isinstance(token, Failure):
|
||||
raise token(lineno, filename)
|
||||
# bygroup is a bit more complex, in that case we
|
||||
# yield for the current token the first named
|
||||
@ -778,7 +778,7 @@ class Lexer:
|
||||
data = groups[idx]
|
||||
|
||||
if data or token not in ignore_if_empty:
|
||||
yield lineno, token, data
|
||||
yield lineno, token, data # type: ignore[misc]
|
||||
|
||||
lineno += data.count("\n") + newlines_stripped
|
||||
newlines_stripped = 0
|
||||
|
||||
@ -22,7 +22,7 @@ if t.TYPE_CHECKING:
|
||||
from .environment import Template
|
||||
|
||||
|
||||
def split_template_path(template: str) -> t.List[str]:
|
||||
def split_template_path(template: str) -> list[str]:
|
||||
"""Split a path into segments and perform a sanity check. If it detects
|
||||
'..' in the path it will raise a `TemplateNotFound` error.
|
||||
"""
|
||||
@ -74,7 +74,7 @@ class BaseLoader:
|
||||
|
||||
def get_source(
|
||||
self, environment: "Environment", template: str
|
||||
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
|
||||
) -> tuple[str, str | None, t.Callable[[], bool] | None]:
|
||||
"""Get the template source, filename and reload helper for a template.
|
||||
It's passed the environment and template name and has to return a
|
||||
tuple in the form ``(source, filename, uptodate)`` or raise a
|
||||
@ -98,7 +98,7 @@ class BaseLoader:
|
||||
)
|
||||
raise TemplateNotFound(template)
|
||||
|
||||
def list_templates(self) -> t.List[str]:
|
||||
def list_templates(self) -> list[str]:
|
||||
"""Iterates over all templates. If the loader does not support that
|
||||
it should raise a :exc:`TypeError` which is the default behavior.
|
||||
"""
|
||||
@ -109,7 +109,7 @@ class BaseLoader:
|
||||
self,
|
||||
environment: "Environment",
|
||||
name: str,
|
||||
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||
globals: t.MutableMapping[str, t.Any] | None = None,
|
||||
) -> "Template":
|
||||
"""Loads a template. This method looks up the template in the cache
|
||||
or loads one by calling :meth:`get_source`. Subclasses should not
|
||||
@ -193,7 +193,7 @@ class FileSystemLoader(BaseLoader):
|
||||
|
||||
def get_source(
|
||||
self, environment: "Environment", template: str
|
||||
) -> t.Tuple[str, str, t.Callable[[], bool]]:
|
||||
) -> tuple[str, str, t.Callable[[], bool]]:
|
||||
pieces = split_template_path(template)
|
||||
|
||||
for searchpath in self.searchpath:
|
||||
@ -204,7 +204,12 @@ class FileSystemLoader(BaseLoader):
|
||||
if os.path.isfile(filename):
|
||||
break
|
||||
else:
|
||||
raise TemplateNotFound(template)
|
||||
plural = "path" if len(self.searchpath) == 1 else "paths"
|
||||
paths_str = ", ".join(repr(p) for p in self.searchpath)
|
||||
raise TemplateNotFound(
|
||||
template,
|
||||
f"{template!r} not found in search {plural}: {paths_str}",
|
||||
)
|
||||
|
||||
with open(filename, encoding=self.encoding) as f:
|
||||
contents = f.read()
|
||||
@ -220,7 +225,7 @@ class FileSystemLoader(BaseLoader):
|
||||
# Use normpath to convert Windows altsep to sep.
|
||||
return contents, os.path.normpath(filename), uptodate
|
||||
|
||||
def list_templates(self) -> t.List[str]:
|
||||
def list_templates(self) -> list[str]:
|
||||
found = set()
|
||||
for searchpath in self.searchpath:
|
||||
walk_dir = os.walk(searchpath, followlinks=self.followlinks)
|
||||
@ -238,6 +243,29 @@ class FileSystemLoader(BaseLoader):
|
||||
return sorted(found)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
|
||||
def _get_zipimporter_files(z: t.Any) -> dict[str, object]:
|
||||
try:
|
||||
get_files = z._get_files
|
||||
except AttributeError as e:
|
||||
raise TypeError(
|
||||
"This zip import does not have the required metadata to list templates."
|
||||
) from e
|
||||
return get_files()
|
||||
|
||||
else:
|
||||
|
||||
def _get_zipimporter_files(z: t.Any) -> dict[str, object]:
|
||||
try:
|
||||
files = z._files
|
||||
except AttributeError as e:
|
||||
raise TypeError(
|
||||
"This zip import does not have the required metadata to list templates."
|
||||
) from e
|
||||
return files # type: ignore[no-any-return]
|
||||
|
||||
|
||||
class PackageLoader(BaseLoader):
|
||||
"""Load templates from a directory in a Python package.
|
||||
|
||||
@ -298,14 +326,13 @@ class PackageLoader(BaseLoader):
|
||||
assert loader is not None, "A loader was not found for the package."
|
||||
self._loader = loader
|
||||
self._archive = None
|
||||
template_root = None
|
||||
|
||||
if isinstance(loader, zipimport.zipimporter):
|
||||
self._archive = loader.archive
|
||||
pkgdir = next(iter(spec.submodule_search_locations)) # type: ignore
|
||||
template_root = os.path.join(pkgdir, package_path).rstrip(os.sep)
|
||||
else:
|
||||
roots: t.List[str] = []
|
||||
roots: list[str] = []
|
||||
|
||||
# One element for regular packages, multiple for namespace
|
||||
# packages, or None for single module file.
|
||||
@ -315,31 +342,36 @@ class PackageLoader(BaseLoader):
|
||||
elif spec.origin is not None:
|
||||
roots.append(os.path.dirname(spec.origin))
|
||||
|
||||
if not roots:
|
||||
raise ValueError(
|
||||
f"The {package_name!r} package was not installed in a"
|
||||
" way that PackageLoader understands."
|
||||
)
|
||||
|
||||
for root in roots:
|
||||
root = os.path.join(root, package_path)
|
||||
|
||||
if os.path.isdir(root):
|
||||
template_root = root
|
||||
break
|
||||
|
||||
if template_root is None:
|
||||
raise ValueError(
|
||||
f"The {package_name!r} package was not installed in a"
|
||||
" way that PackageLoader understands."
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"PackageLoader could not find a {package_path!r} directory"
|
||||
f" in the {package_name!r} package."
|
||||
)
|
||||
|
||||
self._template_root = template_root
|
||||
|
||||
def get_source(
|
||||
self, environment: "Environment", template: str
|
||||
) -> t.Tuple[str, str, t.Optional[t.Callable[[], bool]]]:
|
||||
) -> tuple[str, str, t.Callable[[], bool] | None]:
|
||||
# Use posixpath even on Windows to avoid "drive:" or UNC
|
||||
# segments breaking out of the search directory. Use normpath to
|
||||
# convert Windows altsep to sep.
|
||||
p = os.path.normpath(
|
||||
posixpath.join(self._template_root, *split_template_path(template))
|
||||
)
|
||||
up_to_date: t.Optional[t.Callable[[], bool]]
|
||||
up_to_date: t.Callable[[], bool] | None
|
||||
|
||||
if self._archive is None:
|
||||
# Package is a directory.
|
||||
@ -368,8 +400,8 @@ class PackageLoader(BaseLoader):
|
||||
|
||||
return source.decode(self.encoding), p, up_to_date
|
||||
|
||||
def list_templates(self) -> t.List[str]:
|
||||
results: t.List[str] = []
|
||||
def list_templates(self) -> list[str]:
|
||||
results: list[str] = []
|
||||
|
||||
if self._archive is None:
|
||||
# Package is a directory.
|
||||
@ -382,17 +414,13 @@ class PackageLoader(BaseLoader):
|
||||
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."
|
||||
)
|
||||
files = _get_zipimporter_files(self._loader)
|
||||
|
||||
# Package is a zip file.
|
||||
prefix = self._template_root[len(self._archive) :].lstrip(os.sep) + os.sep
|
||||
offset = len(prefix)
|
||||
|
||||
for name in self._loader._files.keys():
|
||||
for name in files:
|
||||
# Find names under the templates directory that aren't directories.
|
||||
if name.startswith(prefix) and name[-1] != os.sep:
|
||||
results.append(name[offset:].replace(os.sep, "/"))
|
||||
@ -407,7 +435,7 @@ class DictLoader(BaseLoader):
|
||||
|
||||
>>> loader = DictLoader({'index.html': 'source here'})
|
||||
|
||||
Because auto reloading is rarely useful this is disabled per default.
|
||||
Because auto reloading is rarely useful this is disabled by default.
|
||||
"""
|
||||
|
||||
def __init__(self, mapping: t.Mapping[str, str]) -> None:
|
||||
@ -415,13 +443,13 @@ class DictLoader(BaseLoader):
|
||||
|
||||
def get_source(
|
||||
self, environment: "Environment", template: str
|
||||
) -> t.Tuple[str, None, t.Callable[[], bool]]:
|
||||
) -> tuple[str, None, t.Callable[[], bool]]:
|
||||
if template in self.mapping:
|
||||
source = self.mapping[template]
|
||||
return source, None, lambda: source == self.mapping.get(template)
|
||||
raise TemplateNotFound(template)
|
||||
|
||||
def list_templates(self) -> t.List[str]:
|
||||
def list_templates(self) -> list[str]:
|
||||
return sorted(self.mapping)
|
||||
|
||||
|
||||
@ -447,18 +475,14 @@ class FunctionLoader(BaseLoader):
|
||||
self,
|
||||
load_func: t.Callable[
|
||||
[str],
|
||||
t.Optional[
|
||||
t.Union[
|
||||
str, t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]
|
||||
]
|
||||
],
|
||||
str | tuple[str, str | None, t.Callable[[], bool] | None] | None,
|
||||
],
|
||||
) -> None:
|
||||
self.load_func = load_func
|
||||
|
||||
def get_source(
|
||||
self, environment: "Environment", template: str
|
||||
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
|
||||
) -> tuple[str, str | None, t.Callable[[], bool] | None]:
|
||||
rv = self.load_func(template)
|
||||
|
||||
if rv is None:
|
||||
@ -491,7 +515,7 @@ class PrefixLoader(BaseLoader):
|
||||
self.mapping = mapping
|
||||
self.delimiter = delimiter
|
||||
|
||||
def get_loader(self, template: str) -> t.Tuple[BaseLoader, str]:
|
||||
def get_loader(self, template: str) -> tuple[BaseLoader, str]:
|
||||
try:
|
||||
prefix, name = template.split(self.delimiter, 1)
|
||||
loader = self.mapping[prefix]
|
||||
@ -501,7 +525,7 @@ class PrefixLoader(BaseLoader):
|
||||
|
||||
def get_source(
|
||||
self, environment: "Environment", template: str
|
||||
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
|
||||
) -> tuple[str, str | None, t.Callable[[], bool] | None]:
|
||||
loader, name = self.get_loader(template)
|
||||
try:
|
||||
return loader.get_source(environment, name)
|
||||
@ -515,7 +539,7 @@ class PrefixLoader(BaseLoader):
|
||||
self,
|
||||
environment: "Environment",
|
||||
name: str,
|
||||
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||
globals: t.MutableMapping[str, t.Any] | None = None,
|
||||
) -> "Template":
|
||||
loader, local_name = self.get_loader(name)
|
||||
try:
|
||||
@ -525,7 +549,7 @@ class PrefixLoader(BaseLoader):
|
||||
# (the one that includes the prefix)
|
||||
raise TemplateNotFound(name) from e
|
||||
|
||||
def list_templates(self) -> t.List[str]:
|
||||
def list_templates(self) -> list[str]:
|
||||
result = []
|
||||
for prefix, loader in self.mapping.items():
|
||||
for template in loader.list_templates():
|
||||
@ -552,7 +576,7 @@ class ChoiceLoader(BaseLoader):
|
||||
|
||||
def get_source(
|
||||
self, environment: "Environment", template: str
|
||||
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
|
||||
) -> tuple[str, str | None, t.Callable[[], bool] | None]:
|
||||
for loader in self.loaders:
|
||||
try:
|
||||
return loader.get_source(environment, template)
|
||||
@ -565,7 +589,7 @@ class ChoiceLoader(BaseLoader):
|
||||
self,
|
||||
environment: "Environment",
|
||||
name: str,
|
||||
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||
globals: t.MutableMapping[str, t.Any] | None = None,
|
||||
) -> "Template":
|
||||
for loader in self.loaders:
|
||||
try:
|
||||
@ -574,7 +598,7 @@ class ChoiceLoader(BaseLoader):
|
||||
pass
|
||||
raise TemplateNotFound(name)
|
||||
|
||||
def list_templates(self) -> t.List[str]:
|
||||
def list_templates(self) -> list[str]:
|
||||
found = set()
|
||||
for loader in self.loaders:
|
||||
found.update(loader.list_templates())
|
||||
@ -590,10 +614,7 @@ class ModuleLoader(BaseLoader):
|
||||
|
||||
Example usage:
|
||||
|
||||
>>> loader = ChoiceLoader([
|
||||
... ModuleLoader('/path/to/compiled/templates'),
|
||||
... FileSystemLoader('/path/to/templates')
|
||||
... ])
|
||||
>>> loader = ModuleLoader('/path/to/compiled/templates')
|
||||
|
||||
Templates can be precompiled with :meth:`Environment.compile_templates`.
|
||||
"""
|
||||
@ -640,7 +661,7 @@ class ModuleLoader(BaseLoader):
|
||||
self,
|
||||
environment: "Environment",
|
||||
name: str,
|
||||
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||
globals: t.MutableMapping[str, t.Any] | None = None,
|
||||
) -> "Template":
|
||||
key = self.get_template_key(name)
|
||||
module = f"{self.package_name}.{key}"
|
||||
|
||||
@ -17,7 +17,7 @@ class TrackingCodeGenerator(CodeGenerator):
|
||||
|
||||
def __init__(self, environment: "Environment") -> None:
|
||||
super().__init__(environment, "<introspection>", "<introspection>")
|
||||
self.undeclared_identifiers: t.Set[str] = set()
|
||||
self.undeclared_identifiers: set[str] = set()
|
||||
|
||||
def write(self, x: str) -> None:
|
||||
"""Don't write."""
|
||||
@ -31,7 +31,7 @@ class TrackingCodeGenerator(CodeGenerator):
|
||||
self.undeclared_identifiers.add(param)
|
||||
|
||||
|
||||
def find_undeclared_variables(ast: nodes.Template) -> t.Set[str]:
|
||||
def find_undeclared_variables(ast: nodes.Template) -> set[str]:
|
||||
"""Returns a set of all variables in the AST that will be looked up from
|
||||
the context at runtime. Because at compile time it's not known which
|
||||
variables will be used depending on the path the execution takes at
|
||||
@ -56,10 +56,10 @@ def find_undeclared_variables(ast: nodes.Template) -> t.Set[str]:
|
||||
|
||||
|
||||
_ref_types = (nodes.Extends, nodes.FromImport, nodes.Import, nodes.Include)
|
||||
_RefType = t.Union[nodes.Extends, nodes.FromImport, nodes.Import, nodes.Include]
|
||||
_RefType = nodes.Extends | nodes.FromImport | nodes.Import | nodes.Include
|
||||
|
||||
|
||||
def find_referenced_templates(ast: nodes.Template) -> t.Iterator[t.Optional[str]]:
|
||||
def find_referenced_templates(ast: nodes.Template) -> t.Iterator[str | None]:
|
||||
"""Finds all the referenced templates from the AST. This will return an
|
||||
iterator over all the hardcoded template extensions, inclusions and
|
||||
imports. If dynamic inheritance or inclusion is used, `None` will be
|
||||
|
||||
@ -13,7 +13,7 @@ from .environment import Environment
|
||||
from .environment import Template
|
||||
|
||||
|
||||
def native_concat(values: t.Iterable[t.Any]) -> t.Optional[t.Any]:
|
||||
def native_concat(values: t.Iterable[t.Any]) -> t.Any | None:
|
||||
"""Return a native Python type from the list of compiled nodes. If
|
||||
the result is a single node, its value is returned. Otherwise, the
|
||||
nodes are concatenated as strings. If the result can be parsed with
|
||||
|
||||
@ -19,7 +19,7 @@ if t.TYPE_CHECKING:
|
||||
|
||||
_NodeBound = t.TypeVar("_NodeBound", bound="Node")
|
||||
|
||||
_binop_to_func: t.Dict[str, t.Callable[[t.Any, t.Any], t.Any]] = {
|
||||
_binop_to_func: dict[str, t.Callable[[t.Any, t.Any], t.Any]] = {
|
||||
"*": operator.mul,
|
||||
"/": operator.truediv,
|
||||
"//": operator.floordiv,
|
||||
@ -29,13 +29,13 @@ _binop_to_func: t.Dict[str, t.Callable[[t.Any, t.Any], t.Any]] = {
|
||||
"-": operator.sub,
|
||||
}
|
||||
|
||||
_uaop_to_func: t.Dict[str, t.Callable[[t.Any], t.Any]] = {
|
||||
_uaop_to_func: dict[str, t.Callable[[t.Any], t.Any]] = {
|
||||
"not": operator.not_,
|
||||
"+": operator.pos,
|
||||
"-": operator.neg,
|
||||
}
|
||||
|
||||
_cmpop_to_func: t.Dict[str, t.Callable[[t.Any, t.Any], t.Any]] = {
|
||||
_cmpop_to_func: dict[str, t.Callable[[t.Any, t.Any], t.Any]] = {
|
||||
"eq": operator.eq,
|
||||
"ne": operator.ne,
|
||||
"gt": operator.gt,
|
||||
@ -58,7 +58,7 @@ class NodeType(type):
|
||||
|
||||
def __new__(mcs, name, bases, d): # type: ignore
|
||||
for attr in "fields", "attributes":
|
||||
storage: t.List[t.Tuple[str, ...]] = []
|
||||
storage: list[tuple[str, ...]] = []
|
||||
storage.extend(getattr(bases[0] if bases else object, attr, ()))
|
||||
storage.extend(d.get(attr, ()))
|
||||
assert len(bases) <= 1, "multiple inheritance not allowed"
|
||||
@ -74,7 +74,7 @@ class EvalContext:
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, environment: "Environment", template_name: t.Optional[str] = None
|
||||
self, environment: "Environment", template_name: str | None = None
|
||||
) -> None:
|
||||
self.environment = environment
|
||||
if callable(environment.autoescape):
|
||||
@ -91,7 +91,7 @@ class EvalContext:
|
||||
self.__dict__.update(old)
|
||||
|
||||
|
||||
def get_eval_context(node: "Node", ctx: t.Optional[EvalContext]) -> EvalContext:
|
||||
def get_eval_context(node: "Node", ctx: EvalContext | None) -> EvalContext:
|
||||
if ctx is None:
|
||||
if node.environment is None:
|
||||
raise RuntimeError(
|
||||
@ -119,8 +119,8 @@ class Node(metaclass=NodeType):
|
||||
all nodes automatically.
|
||||
"""
|
||||
|
||||
fields: t.Tuple[str, ...] = ()
|
||||
attributes: t.Tuple[str, ...] = ("lineno", "environment")
|
||||
fields: tuple[str, ...] = ()
|
||||
attributes: tuple[str, ...] = ("lineno", "environment")
|
||||
abstract = True
|
||||
|
||||
lineno: int
|
||||
@ -137,7 +137,7 @@ class Node(metaclass=NodeType):
|
||||
f"{type(self).__name__!r} takes 0 or {len(self.fields)}"
|
||||
f" argument{'s' if len(self.fields) != 1 else ''}"
|
||||
)
|
||||
for name, arg in zip(self.fields, fields):
|
||||
for name, arg in zip(self.fields, fields, strict=False):
|
||||
setattr(self, name, arg)
|
||||
for attr in self.attributes:
|
||||
setattr(self, attr, attributes.pop(attr, None))
|
||||
@ -146,9 +146,9 @@ class Node(metaclass=NodeType):
|
||||
|
||||
def iter_fields(
|
||||
self,
|
||||
exclude: t.Optional[t.Container[str]] = None,
|
||||
only: t.Optional[t.Container[str]] = None,
|
||||
) -> t.Iterator[t.Tuple[str, t.Any]]:
|
||||
exclude: t.Container[str] | None = None,
|
||||
only: t.Container[str] | None = None,
|
||||
) -> t.Iterator[tuple[str, t.Any]]:
|
||||
"""This method iterates over all fields that are defined and yields
|
||||
``(key, value)`` tuples. Per default all fields are returned, but
|
||||
it's possible to limit that to some fields by providing the `only`
|
||||
@ -168,8 +168,8 @@ class Node(metaclass=NodeType):
|
||||
|
||||
def iter_child_nodes(
|
||||
self,
|
||||
exclude: t.Optional[t.Container[str]] = None,
|
||||
only: t.Optional[t.Container[str]] = None,
|
||||
exclude: t.Container[str] | None = None,
|
||||
only: t.Container[str] | None = None,
|
||||
) -> t.Iterator["Node"]:
|
||||
"""Iterates over all direct child nodes of the node. This iterates
|
||||
over all fields and yields the values of they are nodes. If the value
|
||||
@ -183,7 +183,7 @@ class Node(metaclass=NodeType):
|
||||
elif isinstance(item, Node):
|
||||
yield item
|
||||
|
||||
def find(self, node_type: t.Type[_NodeBound]) -> t.Optional[_NodeBound]:
|
||||
def find(self, node_type: type[_NodeBound]) -> _NodeBound | None:
|
||||
"""Find the first node of a given type. If no such node exists the
|
||||
return value is `None`.
|
||||
"""
|
||||
@ -193,7 +193,7 @@ class Node(metaclass=NodeType):
|
||||
return None
|
||||
|
||||
def find_all(
|
||||
self, node_type: t.Union[t.Type[_NodeBound], t.Tuple[t.Type[_NodeBound], ...]]
|
||||
self, node_type: type[_NodeBound] | tuple[type[_NodeBound], ...]
|
||||
) -> t.Iterator[_NodeBound]:
|
||||
"""Find all the nodes of a given type. If the type is a tuple,
|
||||
the check is performed for any of the tuple items.
|
||||
@ -250,7 +250,7 @@ class Node(metaclass=NodeType):
|
||||
return f"{type(self).__name__}({args_str})"
|
||||
|
||||
def dump(self) -> str:
|
||||
def _dump(node: t.Union[Node, t.Any]) -> None:
|
||||
def _dump(node: Node | t.Any) -> None:
|
||||
if not isinstance(node, Node):
|
||||
buf.append(repr(node))
|
||||
return
|
||||
@ -274,7 +274,7 @@ class Node(metaclass=NodeType):
|
||||
_dump(value)
|
||||
buf.append(")")
|
||||
|
||||
buf: t.List[str] = []
|
||||
buf: list[str] = []
|
||||
_dump(self)
|
||||
return "".join(buf)
|
||||
|
||||
@ -297,7 +297,7 @@ class Template(Node):
|
||||
"""
|
||||
|
||||
fields = ("body",)
|
||||
body: t.List[Node]
|
||||
body: list[Node]
|
||||
|
||||
|
||||
class Output(Stmt):
|
||||
@ -306,7 +306,7 @@ class Output(Stmt):
|
||||
"""
|
||||
|
||||
fields = ("nodes",)
|
||||
nodes: t.List["Expr"]
|
||||
nodes: list["Expr"]
|
||||
|
||||
|
||||
class Extends(Stmt):
|
||||
@ -328,9 +328,9 @@ class For(Stmt):
|
||||
fields = ("target", "iter", "body", "else_", "test", "recursive")
|
||||
target: Node
|
||||
iter: Node
|
||||
body: t.List[Node]
|
||||
else_: t.List[Node]
|
||||
test: t.Optional[Node]
|
||||
body: list[Node]
|
||||
else_: list[Node]
|
||||
test: Node | None
|
||||
recursive: bool
|
||||
|
||||
|
||||
@ -339,9 +339,9 @@ class If(Stmt):
|
||||
|
||||
fields = ("test", "body", "elif_", "else_")
|
||||
test: Node
|
||||
body: t.List[Node]
|
||||
elif_: t.List["If"]
|
||||
else_: t.List[Node]
|
||||
body: list[Node]
|
||||
elif_: list["If"]
|
||||
else_: list[Node]
|
||||
|
||||
|
||||
class Macro(Stmt):
|
||||
@ -352,9 +352,9 @@ class Macro(Stmt):
|
||||
|
||||
fields = ("name", "args", "defaults", "body")
|
||||
name: str
|
||||
args: t.List["Name"]
|
||||
defaults: t.List["Expr"]
|
||||
body: t.List[Node]
|
||||
args: list["Name"]
|
||||
defaults: list["Expr"]
|
||||
body: list[Node]
|
||||
|
||||
|
||||
class CallBlock(Stmt):
|
||||
@ -364,16 +364,16 @@ class CallBlock(Stmt):
|
||||
|
||||
fields = ("call", "args", "defaults", "body")
|
||||
call: "Call"
|
||||
args: t.List["Name"]
|
||||
defaults: t.List["Expr"]
|
||||
body: t.List[Node]
|
||||
args: list["Name"]
|
||||
defaults: list["Expr"]
|
||||
body: list[Node]
|
||||
|
||||
|
||||
class FilterBlock(Stmt):
|
||||
"""Node for filter sections."""
|
||||
|
||||
fields = ("body", "filter")
|
||||
body: t.List[Node]
|
||||
body: list[Node]
|
||||
filter: "Filter"
|
||||
|
||||
|
||||
@ -385,9 +385,9 @@ class With(Stmt):
|
||||
"""
|
||||
|
||||
fields = ("targets", "values", "body")
|
||||
targets: t.List["Expr"]
|
||||
values: t.List["Expr"]
|
||||
body: t.List[Node]
|
||||
targets: list["Expr"]
|
||||
values: list["Expr"]
|
||||
body: list[Node]
|
||||
|
||||
|
||||
class Block(Stmt):
|
||||
@ -399,7 +399,7 @@ class Block(Stmt):
|
||||
|
||||
fields = ("name", "body", "scoped", "required")
|
||||
name: str
|
||||
body: t.List[Node]
|
||||
body: list[Node]
|
||||
scoped: bool
|
||||
required: bool
|
||||
|
||||
@ -436,7 +436,7 @@ class FromImport(Stmt):
|
||||
|
||||
fields = ("template", "names", "with_context")
|
||||
template: "Expr"
|
||||
names: t.List[t.Union[str, t.Tuple[str, str]]]
|
||||
names: list[str | tuple[str, str]]
|
||||
with_context: bool
|
||||
|
||||
|
||||
@ -461,7 +461,7 @@ class AssignBlock(Stmt):
|
||||
fields = ("target", "filter", "body")
|
||||
target: "Expr"
|
||||
filter: t.Optional["Filter"]
|
||||
body: t.List[Node]
|
||||
body: list[Node]
|
||||
|
||||
|
||||
class Expr(Node):
|
||||
@ -469,7 +469,7 @@ class Expr(Node):
|
||||
|
||||
abstract = True
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> t.Any:
|
||||
"""Return the value of the expression as constant or raise
|
||||
:exc:`Impossible` if this was not possible.
|
||||
|
||||
@ -496,7 +496,7 @@ class BinExpr(Expr):
|
||||
operator: str
|
||||
abstract = True
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> t.Any:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
|
||||
# intercepted operators cannot be folded at compile time
|
||||
@ -520,7 +520,7 @@ class UnaryExpr(Expr):
|
||||
operator: str
|
||||
abstract = True
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> t.Any:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
|
||||
# intercepted operators cannot be folded at compile time
|
||||
@ -584,15 +584,15 @@ class Const(Literal):
|
||||
fields = ("value",)
|
||||
value: t.Any
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> t.Any:
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def from_untrusted(
|
||||
cls,
|
||||
value: t.Any,
|
||||
lineno: t.Optional[int] = None,
|
||||
environment: "t.Optional[Environment]" = None,
|
||||
lineno: int | None = None,
|
||||
environment: "Environment | None" = None,
|
||||
) -> "Const":
|
||||
"""Return a const object if the value is representable as
|
||||
constant value in the generated code, otherwise it will raise
|
||||
@ -611,7 +611,7 @@ class TemplateData(Literal):
|
||||
fields = ("data",)
|
||||
data: str
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> str:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> str:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
if eval_ctx.volatile:
|
||||
raise Impossible()
|
||||
@ -627,10 +627,10 @@ class Tuple(Literal):
|
||||
"""
|
||||
|
||||
fields = ("items", "ctx")
|
||||
items: t.List[Expr]
|
||||
items: list[Expr]
|
||||
ctx: str
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Tuple[t.Any, ...]:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> tuple[t.Any, ...]:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
return tuple(x.as_const(eval_ctx) for x in self.items)
|
||||
|
||||
@ -645,9 +645,9 @@ class List(Literal):
|
||||
"""Any list literal such as ``[1, 2, 3]``"""
|
||||
|
||||
fields = ("items",)
|
||||
items: t.List[Expr]
|
||||
items: list[Expr]
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.List[t.Any]:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> list[t.Any]:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
return [x.as_const(eval_ctx) for x in self.items]
|
||||
|
||||
@ -658,11 +658,9 @@ class Dict(Literal):
|
||||
"""
|
||||
|
||||
fields = ("items",)
|
||||
items: t.List["Pair"]
|
||||
items: list["Pair"]
|
||||
|
||||
def as_const(
|
||||
self, eval_ctx: t.Optional[EvalContext] = None
|
||||
) -> t.Dict[t.Any, t.Any]:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> dict[t.Any, t.Any]:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
return dict(x.as_const(eval_ctx) for x in self.items)
|
||||
|
||||
@ -674,9 +672,7 @@ class Pair(Helper):
|
||||
key: Expr
|
||||
value: Expr
|
||||
|
||||
def as_const(
|
||||
self, eval_ctx: t.Optional[EvalContext] = None
|
||||
) -> t.Tuple[t.Any, t.Any]:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> tuple[t.Any, t.Any]:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
return self.key.as_const(eval_ctx), self.value.as_const(eval_ctx)
|
||||
|
||||
@ -688,7 +684,7 @@ class Keyword(Helper):
|
||||
key: str
|
||||
value: Expr
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Tuple[str, t.Any]:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> tuple[str, t.Any]:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
return self.key, self.value.as_const(eval_ctx)
|
||||
|
||||
@ -701,9 +697,9 @@ class CondExpr(Expr):
|
||||
fields = ("test", "expr1", "expr2")
|
||||
test: Expr
|
||||
expr1: Expr
|
||||
expr2: t.Optional[Expr]
|
||||
expr2: Expr | None
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> t.Any:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
if self.test.as_const(eval_ctx):
|
||||
return self.expr1.as_const(eval_ctx)
|
||||
@ -716,8 +712,8 @@ class CondExpr(Expr):
|
||||
|
||||
|
||||
def args_as_const(
|
||||
node: t.Union["_FilterTestCommon", "Call"], eval_ctx: t.Optional[EvalContext]
|
||||
) -> t.Tuple[t.List[t.Any], t.Dict[t.Any, t.Any]]:
|
||||
node: t.Union["_FilterTestCommon", "Call"], eval_ctx: EvalContext | None
|
||||
) -> tuple[list[t.Any], dict[t.Any, t.Any]]:
|
||||
args = [x.as_const(eval_ctx) for x in node.args]
|
||||
kwargs = dict(x.as_const(eval_ctx) for x in node.kwargs)
|
||||
|
||||
@ -740,14 +736,14 @@ class _FilterTestCommon(Expr):
|
||||
fields = ("node", "name", "args", "kwargs", "dyn_args", "dyn_kwargs")
|
||||
node: Expr
|
||||
name: str
|
||||
args: t.List[Expr]
|
||||
kwargs: t.List[Pair]
|
||||
dyn_args: t.Optional[Expr]
|
||||
dyn_kwargs: t.Optional[Expr]
|
||||
args: list[Expr]
|
||||
kwargs: list[Pair]
|
||||
dyn_args: Expr | None
|
||||
dyn_kwargs: Expr | None
|
||||
abstract = True
|
||||
_is_filter = True
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> t.Any:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
|
||||
if eval_ctx.volatile:
|
||||
@ -792,9 +788,9 @@ class Filter(_FilterTestCommon):
|
||||
and is applied to the content of the block.
|
||||
"""
|
||||
|
||||
node: t.Optional[Expr] # type: ignore
|
||||
node: Expr | None # type: ignore
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> t.Any:
|
||||
if self.node is None:
|
||||
raise Impossible()
|
||||
|
||||
@ -824,10 +820,10 @@ class Call(Expr):
|
||||
|
||||
fields = ("node", "args", "kwargs", "dyn_args", "dyn_kwargs")
|
||||
node: Expr
|
||||
args: t.List[Expr]
|
||||
kwargs: t.List[Keyword]
|
||||
dyn_args: t.Optional[Expr]
|
||||
dyn_kwargs: t.Optional[Expr]
|
||||
args: list[Expr]
|
||||
kwargs: list[Keyword]
|
||||
dyn_args: Expr | None
|
||||
dyn_kwargs: Expr | None
|
||||
|
||||
|
||||
class Getitem(Expr):
|
||||
@ -838,7 +834,7 @@ class Getitem(Expr):
|
||||
arg: Expr
|
||||
ctx: str
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> t.Any:
|
||||
if self.ctx != "load":
|
||||
raise Impossible()
|
||||
|
||||
@ -862,7 +858,7 @@ class Getattr(Expr):
|
||||
attr: str
|
||||
ctx: str
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> t.Any:
|
||||
if self.ctx != "load":
|
||||
raise Impossible()
|
||||
|
||||
@ -880,14 +876,14 @@ class Slice(Expr):
|
||||
"""
|
||||
|
||||
fields = ("start", "stop", "step")
|
||||
start: t.Optional[Expr]
|
||||
stop: t.Optional[Expr]
|
||||
step: t.Optional[Expr]
|
||||
start: Expr | None
|
||||
stop: Expr | None
|
||||
step: Expr | None
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> slice:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> slice:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
|
||||
def const(obj: t.Optional[Expr]) -> t.Optional[t.Any]:
|
||||
def const(obj: Expr | None) -> t.Any | None:
|
||||
if obj is None:
|
||||
return None
|
||||
return obj.as_const(eval_ctx)
|
||||
@ -901,9 +897,9 @@ class Concat(Expr):
|
||||
"""
|
||||
|
||||
fields = ("nodes",)
|
||||
nodes: t.List[Expr]
|
||||
nodes: list[Expr]
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> str:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> str:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
return "".join(str(x.as_const(eval_ctx)) for x in self.nodes)
|
||||
|
||||
@ -915,9 +911,9 @@ class Compare(Expr):
|
||||
|
||||
fields = ("expr", "ops")
|
||||
expr: Expr
|
||||
ops: t.List["Operand"]
|
||||
ops: list["Operand"]
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> t.Any:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
result = value = self.expr.as_const(eval_ctx)
|
||||
|
||||
@ -993,7 +989,7 @@ class And(BinExpr):
|
||||
|
||||
operator = "and"
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> t.Any:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
return self.left.as_const(eval_ctx) and self.right.as_const(eval_ctx)
|
||||
|
||||
@ -1003,7 +999,7 @@ class Or(BinExpr):
|
||||
|
||||
operator = "or"
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> t.Any:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
return self.left.as_const(eval_ctx) or self.right.as_const(eval_ctx)
|
||||
|
||||
@ -1086,7 +1082,7 @@ class MarkSafe(Expr):
|
||||
fields = ("expr",)
|
||||
expr: Expr
|
||||
|
||||
def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> Markup:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> Markup:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
return Markup(self.expr.as_const(eval_ctx))
|
||||
|
||||
@ -1101,9 +1097,7 @@ class MarkSafeIfAutoescape(Expr):
|
||||
fields = ("expr",)
|
||||
expr: Expr
|
||||
|
||||
def as_const(
|
||||
self, eval_ctx: t.Optional[EvalContext] = None
|
||||
) -> t.Union[Markup, t.Any]:
|
||||
def as_const(self, eval_ctx: EvalContext | None = None) -> Markup | t.Any:
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
if eval_ctx.volatile:
|
||||
raise Impossible()
|
||||
@ -1152,7 +1146,7 @@ class Scope(Stmt):
|
||||
"""An artificial scope."""
|
||||
|
||||
fields = ("body",)
|
||||
body: t.List[Node]
|
||||
body: list[Node]
|
||||
|
||||
|
||||
class OverlayScope(Stmt):
|
||||
@ -1171,7 +1165,7 @@ class OverlayScope(Stmt):
|
||||
|
||||
fields = ("context", "body")
|
||||
context: Expr
|
||||
body: t.List[Node]
|
||||
body: list[Node]
|
||||
|
||||
|
||||
class EvalContextModifier(Stmt):
|
||||
@ -1184,7 +1178,7 @@ class EvalContextModifier(Stmt):
|
||||
"""
|
||||
|
||||
fields = ("options",)
|
||||
options: t.List[Keyword]
|
||||
options: list[Keyword]
|
||||
|
||||
|
||||
class ScopedEvalContextModifier(EvalContextModifier):
|
||||
@ -1194,7 +1188,7 @@ class ScopedEvalContextModifier(EvalContextModifier):
|
||||
"""
|
||||
|
||||
fields = ("body",)
|
||||
body: t.List[Node]
|
||||
body: list[Node]
|
||||
|
||||
|
||||
# make sure nobody creates custom nodes
|
||||
|
||||
@ -25,7 +25,7 @@ def optimize(node: nodes.Node, environment: "Environment") -> nodes.Node:
|
||||
|
||||
|
||||
class Optimizer(NodeTransformer):
|
||||
def __init__(self, environment: "t.Optional[Environment]") -> None:
|
||||
def __init__(self, environment: "Environment | None") -> None:
|
||||
self.environment = environment
|
||||
|
||||
def generic_visit(
|
||||
|
||||
@ -35,7 +35,7 @@ _statement_keywords = frozenset(
|
||||
)
|
||||
_compare_operators = frozenset(["eq", "ne", "lt", "lteq", "gt", "gteq"])
|
||||
|
||||
_math_nodes: t.Dict[str, t.Type[nodes.Expr]] = {
|
||||
_math_nodes: dict[str, type[nodes.Expr]] = {
|
||||
"add": nodes.Add,
|
||||
"sub": nodes.Sub,
|
||||
"mul": nodes.Mul,
|
||||
@ -54,30 +54,30 @@ class Parser:
|
||||
self,
|
||||
environment: "Environment",
|
||||
source: str,
|
||||
name: t.Optional[str] = None,
|
||||
filename: t.Optional[str] = None,
|
||||
state: t.Optional[str] = None,
|
||||
name: str | None = None,
|
||||
filename: str | None = None,
|
||||
state: str | None = None,
|
||||
) -> None:
|
||||
self.environment = environment
|
||||
self.stream = environment._tokenize(source, name, filename, state)
|
||||
self.name = name
|
||||
self.filename = filename
|
||||
self.closed = False
|
||||
self.extensions: t.Dict[
|
||||
str, t.Callable[["Parser"], t.Union[nodes.Node, t.List[nodes.Node]]]
|
||||
self.extensions: dict[
|
||||
str, t.Callable[[Parser], nodes.Node | list[nodes.Node]]
|
||||
] = {}
|
||||
for extension in environment.iter_extensions():
|
||||
for tag in extension.tags:
|
||||
self.extensions[tag] = extension.parse
|
||||
self._last_identifier = 0
|
||||
self._tag_stack: t.List[str] = []
|
||||
self._end_token_stack: t.List[t.Tuple[str, ...]] = []
|
||||
self._tag_stack: list[str] = []
|
||||
self._end_token_stack: list[tuple[str, ...]] = []
|
||||
|
||||
def fail(
|
||||
self,
|
||||
msg: str,
|
||||
lineno: t.Optional[int] = None,
|
||||
exc: t.Type[TemplateSyntaxError] = TemplateSyntaxError,
|
||||
lineno: int | None = None,
|
||||
exc: type[TemplateSyntaxError] = TemplateSyntaxError,
|
||||
) -> "te.NoReturn":
|
||||
"""Convenience method that raises `exc` with the message, passed
|
||||
line number or last line number as well as the current name and
|
||||
@ -89,15 +89,15 @@ class Parser:
|
||||
|
||||
def _fail_ut_eof(
|
||||
self,
|
||||
name: t.Optional[str],
|
||||
end_token_stack: t.List[t.Tuple[str, ...]],
|
||||
lineno: t.Optional[int],
|
||||
name: str | None,
|
||||
end_token_stack: list[tuple[str, ...]],
|
||||
lineno: int | None,
|
||||
) -> "te.NoReturn":
|
||||
expected: t.Set[str] = set()
|
||||
expected: set[str] = set()
|
||||
for exprs in end_token_stack:
|
||||
expected.update(map(describe_token_expr, exprs))
|
||||
if end_token_stack:
|
||||
currently_looking: t.Optional[str] = " or ".join(
|
||||
currently_looking: str | None = " or ".join(
|
||||
map(repr, map(describe_token_expr, end_token_stack[-1]))
|
||||
)
|
||||
else:
|
||||
@ -127,9 +127,7 @@ class Parser:
|
||||
|
||||
self.fail(" ".join(message), lineno)
|
||||
|
||||
def fail_unknown_tag(
|
||||
self, name: str, lineno: t.Optional[int] = None
|
||||
) -> "te.NoReturn":
|
||||
def fail_unknown_tag(self, name: str, lineno: int | None = None) -> "te.NoReturn":
|
||||
"""Called if the parser encounters an unknown tag. Tries to fail
|
||||
with a human readable error message that could help to identify
|
||||
the problem.
|
||||
@ -138,8 +136,8 @@ class Parser:
|
||||
|
||||
def fail_eof(
|
||||
self,
|
||||
end_tokens: t.Optional[t.Tuple[str, ...]] = None,
|
||||
lineno: t.Optional[int] = None,
|
||||
end_tokens: tuple[str, ...] | None = None,
|
||||
lineno: int | None = None,
|
||||
) -> "te.NoReturn":
|
||||
"""Like fail_unknown_tag but for end of template situations."""
|
||||
stack = list(self._end_token_stack)
|
||||
@ -147,9 +145,7 @@ class Parser:
|
||||
stack.append(end_tokens)
|
||||
self._fail_ut_eof(None, stack, lineno)
|
||||
|
||||
def is_tuple_end(
|
||||
self, extra_end_rules: t.Optional[t.Tuple[str, ...]] = None
|
||||
) -> bool:
|
||||
def is_tuple_end(self, extra_end_rules: tuple[str, ...] | None = None) -> bool:
|
||||
"""Are we at the end of a tuple?"""
|
||||
if self.stream.current.type in ("variable_end", "block_end", "rparen"):
|
||||
return True
|
||||
@ -157,14 +153,14 @@ class Parser:
|
||||
return self.stream.current.test_any(extra_end_rules) # type: ignore
|
||||
return False
|
||||
|
||||
def free_identifier(self, lineno: t.Optional[int] = None) -> nodes.InternalName:
|
||||
def free_identifier(self, lineno: int | None = None) -> nodes.InternalName:
|
||||
"""Return a new free identifier as :class:`~jinja2.nodes.InternalName`."""
|
||||
self._last_identifier += 1
|
||||
rv = object.__new__(nodes.InternalName)
|
||||
nodes.Node.__init__(rv, f"fi{self._last_identifier}", lineno=lineno)
|
||||
return rv
|
||||
|
||||
def parse_statement(self) -> t.Union[nodes.Node, t.List[nodes.Node]]:
|
||||
def parse_statement(self) -> nodes.Node | list[nodes.Node]:
|
||||
"""Parse a single statement."""
|
||||
token = self.stream.current
|
||||
if token.type != "name":
|
||||
@ -194,8 +190,8 @@ class Parser:
|
||||
self._tag_stack.pop()
|
||||
|
||||
def parse_statements(
|
||||
self, end_tokens: t.Tuple[str, ...], drop_needle: bool = False
|
||||
) -> t.List[nodes.Node]:
|
||||
self, end_tokens: tuple[str, ...], drop_needle: bool = False
|
||||
) -> list[nodes.Node]:
|
||||
"""Parse multiple statements into a list until one of the end tokens
|
||||
is reached. This is used to parse the body of statements as it also
|
||||
parses template data if appropriate. The parser checks first if the
|
||||
@ -222,7 +218,7 @@ class Parser:
|
||||
next(self.stream)
|
||||
return result
|
||||
|
||||
def parse_set(self) -> t.Union[nodes.Assign, nodes.AssignBlock]:
|
||||
def parse_set(self) -> nodes.Assign | nodes.AssignBlock:
|
||||
"""Parse an assign statement."""
|
||||
lineno = next(self.stream).lineno
|
||||
target = self.parse_assign_target(with_namespace=True)
|
||||
@ -272,8 +268,8 @@ class Parser:
|
||||
|
||||
def parse_with(self) -> nodes.With:
|
||||
node = nodes.With(lineno=next(self.stream).lineno)
|
||||
targets: t.List[nodes.Expr] = []
|
||||
values: t.List[nodes.Expr] = []
|
||||
targets: list[nodes.Expr] = []
|
||||
values: list[nodes.Expr] = []
|
||||
while self.stream.current.type != "block_end":
|
||||
if targets:
|
||||
self.stream.expect("comma")
|
||||
@ -466,17 +462,17 @@ class Parser:
|
||||
self,
|
||||
with_tuple: bool = True,
|
||||
name_only: bool = False,
|
||||
extra_end_rules: t.Optional[t.Tuple[str, ...]] = None,
|
||||
extra_end_rules: tuple[str, ...] | None = None,
|
||||
with_namespace: bool = False,
|
||||
) -> t.Union[nodes.NSRef, nodes.Name, nodes.Tuple]: ...
|
||||
) -> nodes.NSRef | nodes.Name | nodes.Tuple: ...
|
||||
|
||||
def parse_assign_target(
|
||||
self,
|
||||
with_tuple: bool = True,
|
||||
name_only: bool = False,
|
||||
extra_end_rules: t.Optional[t.Tuple[str, ...]] = None,
|
||||
extra_end_rules: tuple[str, ...] | None = None,
|
||||
with_namespace: bool = False,
|
||||
) -> t.Union[nodes.NSRef, nodes.Name, nodes.Tuple]:
|
||||
) -> nodes.NSRef | nodes.Name | nodes.Tuple:
|
||||
"""Parse an assignment target. As Jinja allows assignments to
|
||||
tuples, this function can parse all allowed assignment targets. Per
|
||||
default assignments to tuples are parsed, that can be disable however
|
||||
@ -487,21 +483,18 @@ class Parser:
|
||||
"""
|
||||
target: nodes.Expr
|
||||
|
||||
if with_namespace and self.stream.look().type == "dot":
|
||||
token = self.stream.expect("name")
|
||||
next(self.stream) # dot
|
||||
attr = self.stream.expect("name")
|
||||
target = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
|
||||
elif name_only:
|
||||
if name_only:
|
||||
token = self.stream.expect("name")
|
||||
target = nodes.Name(token.value, "store", lineno=token.lineno)
|
||||
else:
|
||||
if with_tuple:
|
||||
target = self.parse_tuple(
|
||||
simplified=True, extra_end_rules=extra_end_rules
|
||||
simplified=True,
|
||||
extra_end_rules=extra_end_rules,
|
||||
with_namespace=with_namespace,
|
||||
)
|
||||
else:
|
||||
target = self.parse_primary()
|
||||
target = self.parse_primary(with_namespace=with_namespace)
|
||||
|
||||
target.set_ctx("store")
|
||||
|
||||
@ -524,7 +517,7 @@ class Parser:
|
||||
def parse_condexpr(self) -> nodes.Expr:
|
||||
lineno = self.stream.current.lineno
|
||||
expr1 = self.parse_or()
|
||||
expr3: t.Optional[nodes.Expr]
|
||||
expr3: nodes.Expr | None
|
||||
|
||||
while self.stream.skip_if("name:if"):
|
||||
expr2 = self.parse_or()
|
||||
@ -643,17 +636,25 @@ class Parser:
|
||||
node = self.parse_filter_expr(node)
|
||||
return node
|
||||
|
||||
def parse_primary(self) -> nodes.Expr:
|
||||
def parse_primary(self, with_namespace: bool = False) -> nodes.Expr:
|
||||
"""Parse a name or literal value. If ``with_namespace`` is enabled, also
|
||||
parse namespace attr refs, for use in assignments."""
|
||||
token = self.stream.current
|
||||
node: nodes.Expr
|
||||
if token.type == "name":
|
||||
next(self.stream)
|
||||
if token.value in ("true", "false", "True", "False"):
|
||||
node = nodes.Const(token.value in ("true", "True"), lineno=token.lineno)
|
||||
elif token.value in ("none", "None"):
|
||||
node = nodes.Const(None, lineno=token.lineno)
|
||||
elif with_namespace and self.stream.current.type == "dot":
|
||||
# If namespace attributes are allowed at this point, and the next
|
||||
# token is a dot, produce a namespace reference.
|
||||
next(self.stream)
|
||||
attr = self.stream.expect("name")
|
||||
node = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
|
||||
else:
|
||||
node = nodes.Name(token.value, "load", lineno=token.lineno)
|
||||
next(self.stream)
|
||||
elif token.type == "string":
|
||||
next(self.stream)
|
||||
buf = [token.value]
|
||||
@ -681,17 +682,19 @@ class Parser:
|
||||
self,
|
||||
simplified: bool = False,
|
||||
with_condexpr: bool = True,
|
||||
extra_end_rules: t.Optional[t.Tuple[str, ...]] = None,
|
||||
extra_end_rules: tuple[str, ...] | None = None,
|
||||
explicit_parentheses: bool = False,
|
||||
) -> t.Union[nodes.Tuple, nodes.Expr]:
|
||||
with_namespace: bool = False,
|
||||
) -> nodes.Tuple | nodes.Expr:
|
||||
"""Works like `parse_expression` but if multiple expressions are
|
||||
delimited by a comma a :class:`~jinja2.nodes.Tuple` node is created.
|
||||
This method could also return a regular expression instead of a tuple
|
||||
if no commas where found.
|
||||
|
||||
The default parsing mode is a full tuple. If `simplified` is `True`
|
||||
only names and literals are parsed. The `no_condexpr` parameter is
|
||||
forwarded to :meth:`parse_expression`.
|
||||
only names and literals are parsed; ``with_namespace`` allows namespace
|
||||
attr refs as well. The `no_condexpr` parameter is forwarded to
|
||||
:meth:`parse_expression`.
|
||||
|
||||
Because tuples do not require delimiters and may end in a bogus comma
|
||||
an extra hint is needed that marks the end of a tuple. For example
|
||||
@ -704,15 +707,16 @@ class Parser:
|
||||
"""
|
||||
lineno = self.stream.current.lineno
|
||||
if simplified:
|
||||
parse = self.parse_primary
|
||||
elif with_condexpr:
|
||||
parse = self.parse_expression
|
||||
|
||||
def parse() -> nodes.Expr:
|
||||
return self.parse_primary(with_namespace=with_namespace)
|
||||
|
||||
else:
|
||||
|
||||
def parse() -> nodes.Expr:
|
||||
return self.parse_expression(with_condexpr=False)
|
||||
return self.parse_expression(with_condexpr=with_condexpr)
|
||||
|
||||
args: t.List[nodes.Expr] = []
|
||||
args: list[nodes.Expr] = []
|
||||
is_tuple = False
|
||||
|
||||
while True:
|
||||
@ -745,7 +749,7 @@ class Parser:
|
||||
|
||||
def parse_list(self) -> nodes.List:
|
||||
token = self.stream.expect("lbracket")
|
||||
items: t.List[nodes.Expr] = []
|
||||
items: list[nodes.Expr] = []
|
||||
while self.stream.current.type != "rbracket":
|
||||
if items:
|
||||
self.stream.expect("comma")
|
||||
@ -757,7 +761,7 @@ class Parser:
|
||||
|
||||
def parse_dict(self) -> nodes.Dict:
|
||||
token = self.stream.expect("lbrace")
|
||||
items: t.List[nodes.Pair] = []
|
||||
items: list[nodes.Pair] = []
|
||||
while self.stream.current.type != "rbrace":
|
||||
if items:
|
||||
self.stream.expect("comma")
|
||||
@ -798,9 +802,7 @@ class Parser:
|
||||
break
|
||||
return node
|
||||
|
||||
def parse_subscript(
|
||||
self, node: nodes.Expr
|
||||
) -> t.Union[nodes.Getattr, nodes.Getitem]:
|
||||
def parse_subscript(self, node: nodes.Expr) -> nodes.Getattr | nodes.Getitem:
|
||||
token = next(self.stream)
|
||||
arg: nodes.Expr
|
||||
|
||||
@ -816,7 +818,7 @@ class Parser:
|
||||
arg = nodes.Const(attr_token.value, lineno=attr_token.lineno)
|
||||
return nodes.Getitem(node, arg, "load", lineno=token.lineno)
|
||||
if token.type == "lbracket":
|
||||
args: t.List[nodes.Expr] = []
|
||||
args: list[nodes.Expr] = []
|
||||
while self.stream.current.type != "rbracket":
|
||||
if args:
|
||||
self.stream.expect("comma")
|
||||
@ -831,7 +833,7 @@ class Parser:
|
||||
|
||||
def parse_subscribed(self) -> nodes.Expr:
|
||||
lineno = self.stream.current.lineno
|
||||
args: t.List[t.Optional[nodes.Expr]]
|
||||
args: list[nodes.Expr | None]
|
||||
|
||||
if self.stream.current.type == "colon":
|
||||
next(self.stream)
|
||||
@ -863,11 +865,11 @@ class Parser:
|
||||
|
||||
def parse_call_args(
|
||||
self,
|
||||
) -> t.Tuple[
|
||||
t.List[nodes.Expr],
|
||||
t.List[nodes.Keyword],
|
||||
t.Union[nodes.Expr, None],
|
||||
t.Union[nodes.Expr, None],
|
||||
) -> tuple[
|
||||
list[nodes.Expr],
|
||||
list[nodes.Keyword],
|
||||
nodes.Expr | None,
|
||||
nodes.Expr | None,
|
||||
]:
|
||||
token = self.stream.expect("lparen")
|
||||
args = []
|
||||
@ -925,8 +927,8 @@ class Parser:
|
||||
return nodes.Call(node, args, kwargs, dyn_args, dyn_kwargs, lineno=token.lineno)
|
||||
|
||||
def parse_filter(
|
||||
self, node: t.Optional[nodes.Expr], start_inline: bool = False
|
||||
) -> t.Optional[nodes.Expr]:
|
||||
self, node: nodes.Expr | None, start_inline: bool = False
|
||||
) -> nodes.Expr | None:
|
||||
while self.stream.current.type == "pipe" or start_inline:
|
||||
if not start_inline:
|
||||
next(self.stream)
|
||||
@ -959,7 +961,7 @@ class Parser:
|
||||
next(self.stream)
|
||||
name += "." + self.stream.expect("name").value
|
||||
dyn_args = dyn_kwargs = None
|
||||
kwargs: t.List[nodes.Keyword] = []
|
||||
kwargs: list[nodes.Keyword] = []
|
||||
if self.stream.current.type == "lparen":
|
||||
args, kwargs, dyn_args, dyn_kwargs = self.parse_call_args()
|
||||
elif self.stream.current.type in {
|
||||
@ -985,11 +987,9 @@ class Parser:
|
||||
node = nodes.Not(node, lineno=token.lineno)
|
||||
return node
|
||||
|
||||
def subparse(
|
||||
self, end_tokens: t.Optional[t.Tuple[str, ...]] = None
|
||||
) -> t.List[nodes.Node]:
|
||||
body: t.List[nodes.Node] = []
|
||||
data_buffer: t.List[nodes.Node] = []
|
||||
def subparse(self, end_tokens: tuple[str, ...] | None = None) -> list[nodes.Node]:
|
||||
body: list[nodes.Node] = []
|
||||
data_buffer: list[nodes.Node] = []
|
||||
add_data = data_buffer.append
|
||||
|
||||
if end_tokens is not None:
|
||||
|
||||
@ -92,12 +92,12 @@ def str_join(seq: t.Iterable[t.Any]) -> str:
|
||||
|
||||
def new_context(
|
||||
environment: "Environment",
|
||||
template_name: t.Optional[str],
|
||||
blocks: t.Dict[str, t.Callable[["Context"], t.Iterator[str]]],
|
||||
vars: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
template_name: str | None,
|
||||
blocks: dict[str, t.Callable[["Context"], t.Iterator[str]]],
|
||||
vars: dict[str, t.Any] | None = None,
|
||||
shared: bool = False,
|
||||
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||
locals: t.Optional[t.Mapping[str, t.Any]] = None,
|
||||
globals: t.MutableMapping[str, t.Any] | None = None,
|
||||
locals: t.Mapping[str, t.Any] | None = None,
|
||||
) -> "Context":
|
||||
"""Internal helper for context creation."""
|
||||
if vars is None:
|
||||
@ -165,16 +165,16 @@ class Context:
|
||||
def __init__(
|
||||
self,
|
||||
environment: "Environment",
|
||||
parent: t.Dict[str, t.Any],
|
||||
name: t.Optional[str],
|
||||
blocks: t.Dict[str, t.Callable[["Context"], t.Iterator[str]]],
|
||||
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||
parent: dict[str, t.Any],
|
||||
name: str | None,
|
||||
blocks: dict[str, t.Callable[["Context"], t.Iterator[str]]],
|
||||
globals: t.MutableMapping[str, t.Any] | None = None,
|
||||
):
|
||||
self.parent = parent
|
||||
self.vars: t.Dict[str, t.Any] = {}
|
||||
self.environment: "Environment" = environment
|
||||
self.vars: dict[str, t.Any] = {}
|
||||
self.environment: Environment = environment
|
||||
self.eval_ctx = EvalContext(self.environment, name)
|
||||
self.exported_vars: t.Set[str] = set()
|
||||
self.exported_vars: set[str] = set()
|
||||
self.name = name
|
||||
self.globals_keys = set() if globals is None else set(globals)
|
||||
|
||||
@ -244,11 +244,11 @@ class Context:
|
||||
|
||||
return missing
|
||||
|
||||
def get_exported(self) -> t.Dict[str, t.Any]:
|
||||
def get_exported(self) -> dict[str, t.Any]:
|
||||
"""Get a new dict with the exported variables."""
|
||||
return {k: self.vars[k] for k in self.exported_vars}
|
||||
|
||||
def get_all(self) -> t.Dict[str, t.Any]:
|
||||
def get_all(self) -> dict[str, t.Any]:
|
||||
"""Return the complete context as dict including the exported
|
||||
variables. For optimizations reasons this might not return an
|
||||
actual copy so be careful with using it.
|
||||
@ -307,7 +307,7 @@ class Context:
|
||||
" StopIteration exception"
|
||||
)
|
||||
|
||||
def derived(self, locals: t.Optional[t.Dict[str, t.Any]] = None) -> "Context":
|
||||
def derived(self, locals: dict[str, t.Any] | None = None) -> "Context":
|
||||
"""Internal helper function to create a derived context. This is
|
||||
used in situations where the system needs a new context in the same
|
||||
template that is independent.
|
||||
@ -348,7 +348,7 @@ class BlockReference:
|
||||
self,
|
||||
name: str,
|
||||
context: "Context",
|
||||
stack: t.List[t.Callable[["Context"], t.Iterator[str]]],
|
||||
stack: list[t.Callable[["Context"], t.Iterator[str]]],
|
||||
depth: int,
|
||||
) -> None:
|
||||
self.name = name
|
||||
@ -367,7 +367,7 @@ class BlockReference:
|
||||
|
||||
@internalcode
|
||||
async def _async_call(self) -> str:
|
||||
rv = concat(
|
||||
rv = self._context.environment.concat( # type: ignore
|
||||
[x async for x in self._stack[self._depth](self._context)] # type: ignore
|
||||
)
|
||||
|
||||
@ -381,7 +381,9 @@ class BlockReference:
|
||||
if self._context.environment.is_async:
|
||||
return self._async_call() # type: ignore
|
||||
|
||||
rv = concat(self._stack[self._depth](self._context))
|
||||
rv = self._context.environment.concat( # type: ignore
|
||||
self._stack[self._depth](self._context)
|
||||
)
|
||||
|
||||
if self._context.eval_ctx.autoescape:
|
||||
return Markup(rv)
|
||||
@ -397,7 +399,7 @@ class LoopContext:
|
||||
#: Current iteration of the loop, starting at 0.
|
||||
index0 = -1
|
||||
|
||||
_length: t.Optional[int] = None
|
||||
_length: int | None = None
|
||||
_after: t.Any = missing
|
||||
_current: t.Any = missing
|
||||
_before: t.Any = missing
|
||||
@ -406,7 +408,7 @@ class LoopContext:
|
||||
def __init__(
|
||||
self,
|
||||
iterable: t.Iterable[V],
|
||||
undefined: t.Type["Undefined"],
|
||||
undefined: type["Undefined"],
|
||||
recurse: t.Optional["LoopRenderFunc"] = None,
|
||||
depth0: int = 0,
|
||||
) -> None:
|
||||
@ -556,7 +558,7 @@ class LoopContext:
|
||||
def __iter__(self) -> "LoopContext":
|
||||
return self
|
||||
|
||||
def __next__(self) -> t.Tuple[t.Any, "LoopContext"]:
|
||||
def __next__(self) -> tuple[t.Any, "LoopContext"]:
|
||||
if self._after is not missing:
|
||||
rv = self._after
|
||||
self._after = missing
|
||||
@ -591,7 +593,7 @@ class AsyncLoopContext(LoopContext):
|
||||
|
||||
@staticmethod
|
||||
def _to_iterator( # type: ignore
|
||||
iterable: t.Union[t.Iterable[V], t.AsyncIterable[V]],
|
||||
iterable: t.Iterable[V] | t.AsyncIterable[V],
|
||||
) -> t.AsyncIterator[V]:
|
||||
return auto_aiter(iterable)
|
||||
|
||||
@ -644,7 +646,7 @@ class AsyncLoopContext(LoopContext):
|
||||
def __aiter__(self) -> "AsyncLoopContext":
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> t.Tuple[t.Any, "AsyncLoopContext"]:
|
||||
async def __anext__(self) -> tuple[t.Any, "AsyncLoopContext"]:
|
||||
if self._after is not missing:
|
||||
rv = self._after
|
||||
self._after = missing
|
||||
@ -665,11 +667,11 @@ class Macro:
|
||||
environment: "Environment",
|
||||
func: t.Callable[..., str],
|
||||
name: str,
|
||||
arguments: t.List[str],
|
||||
arguments: list[str],
|
||||
catch_kwargs: bool,
|
||||
catch_varargs: bool,
|
||||
caller: bool,
|
||||
default_autoescape: t.Optional[bool] = None,
|
||||
default_autoescape: bool | None = None,
|
||||
):
|
||||
self._environment = environment
|
||||
self._func = func
|
||||
@ -767,7 +769,7 @@ class Macro:
|
||||
|
||||
return self._invoke(arguments, autoescape)
|
||||
|
||||
async def _async_invoke(self, arguments: t.List[t.Any], autoescape: bool) -> str:
|
||||
async def _async_invoke(self, arguments: list[t.Any], autoescape: bool) -> str:
|
||||
rv = await self._func(*arguments) # type: ignore
|
||||
|
||||
if autoescape:
|
||||
@ -775,7 +777,7 @@ class Macro:
|
||||
|
||||
return rv # type: ignore
|
||||
|
||||
def _invoke(self, arguments: t.List[t.Any], autoescape: bool) -> str:
|
||||
def _invoke(self, arguments: list[t.Any], autoescape: bool) -> str:
|
||||
if self._environment.is_async:
|
||||
return self._async_invoke(arguments, autoescape) # type: ignore
|
||||
|
||||
@ -792,8 +794,8 @@ class Macro:
|
||||
|
||||
|
||||
class Undefined:
|
||||
"""The default undefined type. This undefined type can be printed and
|
||||
iterated over, but every other access will raise an :exc:`UndefinedError`:
|
||||
"""The default undefined type. This can be printed, iterated, and treated as
|
||||
a boolean. Any other operation will raise an :exc:`UndefinedError`.
|
||||
|
||||
>>> foo = Undefined(name='foo')
|
||||
>>> str(foo)
|
||||
@ -815,10 +817,10 @@ class Undefined:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hint: t.Optional[str] = None,
|
||||
hint: str | None = None,
|
||||
obj: t.Any = missing,
|
||||
name: t.Optional[str] = None,
|
||||
exc: t.Type[TemplateRuntimeError] = UndefinedError,
|
||||
name: str | None = None,
|
||||
exc: type[TemplateRuntimeError] = UndefinedError,
|
||||
) -> None:
|
||||
self._undefined_hint = hint
|
||||
self._undefined_obj = obj
|
||||
@ -858,7 +860,11 @@ class Undefined:
|
||||
|
||||
@internalcode
|
||||
def __getattr__(self, name: str) -> t.Any:
|
||||
if name[:2] == "__":
|
||||
# Raise AttributeError on requests for names that appear to be unimplemented
|
||||
# dunder methods to keep Python's internal protocol probing behaviors working
|
||||
# properly in cases where another exception type could cause unexpected or
|
||||
# difficult-to-diagnose failures.
|
||||
if name[:2] == "__" and name[-2:] == "__":
|
||||
raise AttributeError(name)
|
||||
|
||||
return self._fail_with_undefined_error()
|
||||
@ -904,8 +910,8 @@ class Undefined:
|
||||
|
||||
|
||||
def make_logging_undefined(
|
||||
logger: t.Optional["logging.Logger"] = None, base: t.Type[Undefined] = Undefined
|
||||
) -> t.Type[Undefined]:
|
||||
logger: t.Optional["logging.Logger"] = None, base: type[Undefined] = Undefined
|
||||
) -> type[Undefined]:
|
||||
"""Given a logger object this returns a new undefined class that will
|
||||
log certain failures. It will log iterations and printing. If no
|
||||
logger is given a default logger is created.
|
||||
@ -982,10 +988,20 @@ class ChainableUndefined(Undefined):
|
||||
def __html__(self) -> str:
|
||||
return str(self)
|
||||
|
||||
def __getattr__(self, _: str) -> "ChainableUndefined":
|
||||
def __getattr__(self, name: str) -> "ChainableUndefined":
|
||||
# Raise AttributeError on requests for names that appear to be unimplemented
|
||||
# dunder methods to avoid confusing Python with truthy non-method objects that
|
||||
# do not implement the protocol being probed for. e.g., copy.copy(Undefined())
|
||||
# fails spectacularly if getattr(Undefined(), '__setstate__') returns an
|
||||
# Undefined object instead of raising AttributeError to signal that it does not
|
||||
# support that style of object initialization.
|
||||
if name[:2] == "__" and name[-2:] == "__":
|
||||
raise AttributeError(name)
|
||||
|
||||
return self
|
||||
|
||||
__getitem__ = __getattr__ # type: ignore
|
||||
def __getitem__(self, _name: str) -> "ChainableUndefined": # type: ignore[override]
|
||||
return self
|
||||
|
||||
|
||||
class DebugUndefined(Undefined):
|
||||
@ -1044,13 +1060,3 @@ class StrictUndefined(Undefined):
|
||||
__iter__ = __str__ = __len__ = Undefined._fail_with_undefined_error
|
||||
__eq__ = __ne__ = __bool__ = __hash__ = Undefined._fail_with_undefined_error
|
||||
__contains__ = Undefined._fail_with_undefined_error
|
||||
|
||||
|
||||
# Remove slots attributes, after the metaclass is applied they are
|
||||
# unneeded and contain wrong data for subclasses.
|
||||
del (
|
||||
Undefined.__slots__,
|
||||
ChainableUndefined.__slots__,
|
||||
DebugUndefined.__slots__,
|
||||
StrictUndefined.__slots__,
|
||||
)
|
||||
|
||||
@ -5,11 +5,12 @@ Useful when the template itself comes from an untrusted source.
|
||||
import operator
|
||||
import types
|
||||
import typing as t
|
||||
from _string import formatter_field_name_split # type: ignore
|
||||
from collections import abc
|
||||
from collections import deque
|
||||
from functools import update_wrapper
|
||||
from string import Formatter
|
||||
|
||||
from _string import formatter_field_name_split # type: ignore
|
||||
from markupsafe import EscapeFormatter
|
||||
from markupsafe import Markup
|
||||
|
||||
@ -24,10 +25,10 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||
MAX_RANGE = 100000
|
||||
|
||||
#: Unsafe function attributes.
|
||||
UNSAFE_FUNCTION_ATTRIBUTES: t.Set[str] = set()
|
||||
UNSAFE_FUNCTION_ATTRIBUTES: set[str] = set()
|
||||
|
||||
#: Unsafe method attributes. Function attributes are unsafe for methods too.
|
||||
UNSAFE_METHOD_ATTRIBUTES: t.Set[str] = set()
|
||||
UNSAFE_METHOD_ATTRIBUTES: set[str] = set()
|
||||
|
||||
#: unsafe generator attributes.
|
||||
UNSAFE_GENERATOR_ATTRIBUTES = {"gi_frame", "gi_code"}
|
||||
@ -38,7 +39,7 @@ UNSAFE_COROUTINE_ATTRIBUTES = {"cr_frame", "cr_code"}
|
||||
#: unsafe attributes on async generators
|
||||
UNSAFE_ASYNC_GENERATOR_ATTRIBUTES = {"ag_code", "ag_frame"}
|
||||
|
||||
_mutable_spec: t.Tuple[t.Tuple[t.Type[t.Any], t.FrozenSet[str]], ...] = (
|
||||
_mutable_spec: tuple[tuple[type[t.Any], frozenset[str]], ...] = (
|
||||
(
|
||||
abc.MutableSet,
|
||||
frozenset(
|
||||
@ -60,7 +61,9 @@ _mutable_spec: t.Tuple[t.Tuple[t.Type[t.Any], t.FrozenSet[str]], ...] = (
|
||||
),
|
||||
(
|
||||
abc.MutableSequence,
|
||||
frozenset(["append", "reverse", "insert", "sort", "extend", "remove"]),
|
||||
frozenset(
|
||||
["append", "clear", "pop", "reverse", "insert", "sort", "extend", "remove"]
|
||||
),
|
||||
),
|
||||
(
|
||||
deque,
|
||||
@ -81,20 +84,6 @@ _mutable_spec: t.Tuple[t.Tuple[t.Type[t.Any], t.FrozenSet[str]], ...] = (
|
||||
)
|
||||
|
||||
|
||||
def inspect_format_method(callable: t.Callable[..., t.Any]) -> t.Optional[str]:
|
||||
if not isinstance(
|
||||
callable, (types.MethodType, types.BuiltinMethodType)
|
||||
) or callable.__name__ not in ("format", "format_map"):
|
||||
return None
|
||||
|
||||
obj = callable.__self__
|
||||
|
||||
if isinstance(obj, str):
|
||||
return obj
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def safe_range(*args: int) -> range:
|
||||
"""A range that can't generate ranges with a length of more than
|
||||
MAX_RANGE items.
|
||||
@ -201,7 +190,7 @@ class SandboxedEnvironment(Environment):
|
||||
#: default callback table for the binary operators. A copy of this is
|
||||
#: available on each instance of a sandboxed environment as
|
||||
#: :attr:`binop_table`
|
||||
default_binop_table: t.Dict[str, t.Callable[[t.Any, t.Any], t.Any]] = {
|
||||
default_binop_table: dict[str, t.Callable[[t.Any, t.Any], t.Any]] = {
|
||||
"+": operator.add,
|
||||
"-": operator.sub,
|
||||
"*": operator.mul,
|
||||
@ -214,7 +203,7 @@ class SandboxedEnvironment(Environment):
|
||||
#: default callback table for the unary operators. A copy of this is
|
||||
#: available on each instance of a sandboxed environment as
|
||||
#: :attr:`unop_table`
|
||||
default_unop_table: t.Dict[str, t.Callable[[t.Any], t.Any]] = {
|
||||
default_unop_table: dict[str, t.Callable[[t.Any], t.Any]] = {
|
||||
"+": operator.pos,
|
||||
"-": operator.neg,
|
||||
}
|
||||
@ -233,7 +222,7 @@ class SandboxedEnvironment(Environment):
|
||||
#: interested in.
|
||||
#:
|
||||
#: .. versionadded:: 2.6
|
||||
intercepted_binops: t.FrozenSet[str] = frozenset()
|
||||
intercepted_binops: frozenset[str] = frozenset()
|
||||
|
||||
#: a set of unary operators that should be intercepted. Each operator
|
||||
#: that is added to this set (empty by default) is delegated to the
|
||||
@ -248,7 +237,7 @@ class SandboxedEnvironment(Environment):
|
||||
#: interested in.
|
||||
#:
|
||||
#: .. versionadded:: 2.6
|
||||
intercepted_unops: t.FrozenSet[str] = frozenset()
|
||||
intercepted_unops: frozenset[str] = frozenset()
|
||||
|
||||
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -296,9 +285,7 @@ class SandboxedEnvironment(Environment):
|
||||
"""
|
||||
return self.unop_table[operator](arg)
|
||||
|
||||
def getitem(
|
||||
self, obj: t.Any, argument: t.Union[str, t.Any]
|
||||
) -> t.Union[t.Any, Undefined]:
|
||||
def getitem(self, obj: t.Any, argument: str | t.Any) -> t.Any | Undefined:
|
||||
"""Subscribe an object from sandboxed code."""
|
||||
try:
|
||||
return obj[argument]
|
||||
@ -314,12 +301,15 @@ class SandboxedEnvironment(Environment):
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
fmt = self.wrap_str_format(value)
|
||||
if fmt is not None:
|
||||
return fmt
|
||||
if self.is_safe_attribute(obj, argument, value):
|
||||
return value
|
||||
return self.unsafe_undefined(obj, argument)
|
||||
return self.undefined(obj=obj, name=argument)
|
||||
|
||||
def getattr(self, obj: t.Any, attribute: str) -> t.Union[t.Any, Undefined]:
|
||||
def getattr(self, obj: t.Any, attribute: str) -> t.Any | Undefined:
|
||||
"""Subscribe an object from sandboxed code and prefer the
|
||||
attribute. The attribute passed *must* be a bytestring.
|
||||
"""
|
||||
@ -331,6 +321,9 @@ class SandboxedEnvironment(Environment):
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
else:
|
||||
fmt = self.wrap_str_format(value)
|
||||
if fmt is not None:
|
||||
return fmt
|
||||
if self.is_safe_attribute(obj, attribute, value):
|
||||
return value
|
||||
return self.unsafe_undefined(obj, attribute)
|
||||
@ -346,34 +339,49 @@ class SandboxedEnvironment(Environment):
|
||||
exc=SecurityError,
|
||||
)
|
||||
|
||||
def format_string(
|
||||
self,
|
||||
s: str,
|
||||
args: t.Tuple[t.Any, ...],
|
||||
kwargs: t.Dict[str, t.Any],
|
||||
format_func: t.Optional[t.Callable[..., t.Any]] = None,
|
||||
) -> str:
|
||||
"""If a format call is detected, then this is routed through this
|
||||
method so that our safety sandbox can be used for it.
|
||||
def wrap_str_format(self, value: t.Any) -> t.Callable[..., str] | None:
|
||||
"""If the given value is a ``str.format`` or ``str.format_map`` method,
|
||||
return a new function than handles sandboxing. This is done at access
|
||||
rather than in :meth:`call`, so that calls made without ``call`` are
|
||||
also sandboxed.
|
||||
"""
|
||||
if not isinstance(
|
||||
value, (types.MethodType, types.BuiltinMethodType)
|
||||
) or value.__name__ not in ("format", "format_map"):
|
||||
return None
|
||||
|
||||
f_self: t.Any = value.__self__
|
||||
|
||||
if not isinstance(f_self, str):
|
||||
return None
|
||||
|
||||
str_type: type[str] = type(f_self)
|
||||
is_format_map = value.__name__ == "format_map"
|
||||
formatter: SandboxedFormatter
|
||||
if isinstance(s, Markup):
|
||||
formatter = SandboxedEscapeFormatter(self, escape=s.escape)
|
||||
|
||||
if isinstance(f_self, Markup):
|
||||
formatter = SandboxedEscapeFormatter(self, escape=f_self.escape)
|
||||
else:
|
||||
formatter = SandboxedFormatter(self)
|
||||
|
||||
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"
|
||||
f" {len(args) + (kwargs is not None)} given"
|
||||
)
|
||||
vformat = formatter.vformat
|
||||
|
||||
kwargs = args[0]
|
||||
args = ()
|
||||
def wrapper(*args: t.Any, **kwargs: t.Any) -> str:
|
||||
if is_format_map:
|
||||
if kwargs:
|
||||
raise TypeError("format_map() takes no keyword arguments")
|
||||
|
||||
rv = formatter.vformat(s, args, kwargs)
|
||||
return type(s)(rv)
|
||||
if len(args) != 1:
|
||||
raise TypeError(
|
||||
f"format_map() takes exactly one argument ({len(args)} given)"
|
||||
)
|
||||
|
||||
kwargs = args[0]
|
||||
args = ()
|
||||
|
||||
return str_type(vformat(f_self, args, kwargs))
|
||||
|
||||
return update_wrapper(wrapper, value)
|
||||
|
||||
def call(
|
||||
__self, # noqa: B902
|
||||
@ -383,9 +391,6 @@ class SandboxedEnvironment(Environment):
|
||||
**kwargs: t.Any,
|
||||
) -> t.Any:
|
||||
"""Call an object from sandboxed code."""
|
||||
fmt = inspect_format_method(__obj)
|
||||
if fmt is not None:
|
||||
return __self.format_string(fmt, args, kwargs, __obj)
|
||||
|
||||
# the double prefixes are to avoid double keyword argument
|
||||
# errors when proxying the call.
|
||||
@ -414,7 +419,7 @@ class SandboxedFormatter(Formatter):
|
||||
|
||||
def get_field(
|
||||
self, field_name: str, args: t.Sequence[t.Any], kwargs: t.Mapping[str, t.Any]
|
||||
) -> t.Tuple[t.Any, str]:
|
||||
) -> tuple[t.Any, str]:
|
||||
first, rest = formatter_field_name_split(field_name)
|
||||
obj = self.get_value(first, args, kwargs)
|
||||
for is_attr, i in rest:
|
||||
|
||||
@ -18,8 +18,17 @@ if t.TYPE_CHECKING:
|
||||
|
||||
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||
|
||||
# special singleton representing missing values for the runtime
|
||||
missing: t.Any = type("MissingType", (), {"__repr__": lambda x: "missing"})()
|
||||
|
||||
class _MissingType:
|
||||
def __repr__(self) -> str:
|
||||
return "missing"
|
||||
|
||||
def __reduce__(self) -> str:
|
||||
return "missing"
|
||||
|
||||
|
||||
missing: t.Any = _MissingType()
|
||||
"""Special singleton representing missing values for the runtime."""
|
||||
|
||||
internal_code: t.MutableSet[CodeType] = set()
|
||||
|
||||
@ -152,7 +161,7 @@ def import_string(import_name: str, silent: bool = False) -> t.Any:
|
||||
raise
|
||||
|
||||
|
||||
def open_if_exists(filename: str, mode: str = "rb") -> t.Optional[t.IO[t.Any]]:
|
||||
def open_if_exists(filename: str, mode: str = "rb") -> t.IO[t.Any] | None:
|
||||
"""Returns a file descriptor for the filename if that file exists,
|
||||
otherwise ``None``.
|
||||
"""
|
||||
@ -220,10 +229,10 @@ _email_re = re.compile(r"^\S+@\w[\w.-]*\.\w+$")
|
||||
|
||||
def urlize(
|
||||
text: str,
|
||||
trim_url_limit: t.Optional[int] = None,
|
||||
rel: t.Optional[str] = None,
|
||||
target: t.Optional[str] = None,
|
||||
extra_schemes: t.Optional[t.Iterable[str]] = None,
|
||||
trim_url_limit: int | None = None,
|
||||
rel: str | None = None,
|
||||
target: str | None = None,
|
||||
extra_schemes: t.Iterable[str] | None = None,
|
||||
) -> str:
|
||||
"""Convert URLs in text into clickable links.
|
||||
|
||||
@ -324,6 +333,8 @@ def urlize(
|
||||
elif (
|
||||
"@" in middle
|
||||
and not middle.startswith("www.")
|
||||
# ignore values like `@a@b`
|
||||
and not middle.startswith("@")
|
||||
and ":" not in middle
|
||||
and _email_re.match(middle)
|
||||
):
|
||||
@ -427,8 +438,8 @@ class LRUCache:
|
||||
|
||||
def __init__(self, capacity: int) -> None:
|
||||
self.capacity = capacity
|
||||
self._mapping: t.Dict[t.Any, t.Any] = {}
|
||||
self._queue: "te.Deque[t.Any]" = deque()
|
||||
self._mapping: dict[t.Any, t.Any] = {}
|
||||
self._queue: deque[t.Any] = deque()
|
||||
self._postinit()
|
||||
|
||||
def _postinit(self) -> None:
|
||||
@ -450,10 +461,10 @@ class LRUCache:
|
||||
self.__dict__.update(d)
|
||||
self._postinit()
|
||||
|
||||
def __getnewargs__(self) -> t.Tuple[t.Any, ...]:
|
||||
def __getnewargs__(self) -> tuple[t.Any, ...]:
|
||||
return (self.capacity,)
|
||||
|
||||
def copy(self) -> "LRUCache":
|
||||
def copy(self) -> "te.Self":
|
||||
"""Return a shallow copy of the instance."""
|
||||
rv = self.__class__(self.capacity)
|
||||
rv._mapping.update(self._mapping)
|
||||
@ -541,7 +552,7 @@ class LRUCache:
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def items(self) -> t.Iterable[t.Tuple[t.Any, t.Any]]:
|
||||
def items(self) -> t.Iterable[tuple[t.Any, t.Any]]:
|
||||
"""Return a list of items."""
|
||||
result = [(key, self._mapping[key]) for key in list(self._queue)]
|
||||
result.reverse()
|
||||
@ -572,7 +583,7 @@ def select_autoescape(
|
||||
disabled_extensions: t.Collection[str] = (),
|
||||
default_for_string: bool = True,
|
||||
default: bool = False,
|
||||
) -> t.Callable[[t.Optional[str]], bool]:
|
||||
) -> t.Callable[[str | None], bool]:
|
||||
"""Intelligently sets the initial value of autoescaping based on the
|
||||
filename of the template. This is the recommended way to configure
|
||||
autoescaping if you do not want to write a custom function yourself.
|
||||
@ -610,7 +621,7 @@ def select_autoescape(
|
||||
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: t.Optional[str]) -> bool:
|
||||
def autoescape(template_name: str | None) -> bool:
|
||||
if template_name is None:
|
||||
return default_for_string
|
||||
template_name = template_name.lower()
|
||||
@ -624,7 +635,7 @@ def select_autoescape(
|
||||
|
||||
|
||||
def htmlsafe_json_dumps(
|
||||
obj: t.Any, dumps: t.Optional[t.Callable[..., str]] = None, **kwargs: t.Any
|
||||
obj: t.Any, dumps: t.Callable[..., str] | None = None, **kwargs: t.Any
|
||||
) -> markupsafe.Markup:
|
||||
"""Serialize an object to a string of JSON with :func:`json.dumps`,
|
||||
then replace HTML-unsafe characters with Unicode escapes and mark
|
||||
|
||||
@ -25,7 +25,7 @@ class NodeVisitor:
|
||||
(return value `None`) the `generic_visit` visitor is used instead.
|
||||
"""
|
||||
|
||||
def get_visitor(self, node: Node) -> "t.Optional[VisitCallable]":
|
||||
def get_visitor(self, node: Node) -> "VisitCallable | None":
|
||||
"""Return the visitor function for this node or `None` if no visitor
|
||||
exists for this node. In that case the generic visit function is
|
||||
used instead.
|
||||
@ -80,7 +80,7 @@ class NodeTransformer(NodeVisitor):
|
||||
setattr(node, field, new_node)
|
||||
return node
|
||||
|
||||
def visit_list(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.List[Node]:
|
||||
def visit_list(self, node: Node, *args: t.Any, **kwargs: t.Any) -> list[Node]:
|
||||
"""As transformers may return lists in some places this method
|
||||
can be used to enforce a list as return value.
|
||||
"""
|
||||
|
||||
@ -1,11 +1,22 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import trio
|
||||
|
||||
from jinja2 import loaders
|
||||
from jinja2.environment import Environment
|
||||
|
||||
|
||||
def _asyncio_run(async_fn, *args):
|
||||
return asyncio.run(async_fn(*args))
|
||||
|
||||
|
||||
@pytest.fixture(params=[_asyncio_run, trio.run], ids=["asyncio", "trio"])
|
||||
def run_async_fn(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env():
|
||||
"""returns a new environment."""
|
||||
|
||||
@ -323,8 +323,6 @@ class TestUndefined:
|
||||
assert und1 == und2
|
||||
assert und1 != 42
|
||||
assert hash(und1) == hash(und2) == hash(Undefined())
|
||||
with pytest.raises(AttributeError):
|
||||
getattr(Undefined, "__slots__") # noqa: B009
|
||||
|
||||
def test_chainable_undefined(self):
|
||||
env = Environment(undefined=ChainableUndefined)
|
||||
@ -335,8 +333,6 @@ class TestUndefined:
|
||||
assert env.from_string("{{ foo.missing }}").render(foo=42) == ""
|
||||
assert env.from_string("{{ not missing }}").render() == "True"
|
||||
pytest.raises(UndefinedError, env.from_string("{{ missing - 1}}").render)
|
||||
with pytest.raises(AttributeError):
|
||||
getattr(ChainableUndefined, "__slots__") # noqa: B009
|
||||
|
||||
# The following tests ensure subclass functionality works as expected
|
||||
assert env.from_string('{{ missing.bar["baz"] }}').render() == ""
|
||||
@ -368,8 +364,6 @@ class TestUndefined:
|
||||
str(DebugUndefined(hint=undefined_hint))
|
||||
== f"{{{{ undefined value printed: {undefined_hint} }}}}"
|
||||
)
|
||||
with pytest.raises(AttributeError):
|
||||
getattr(DebugUndefined, "__slots__") # noqa: B009
|
||||
|
||||
def test_strict_undefined(self):
|
||||
env = Environment(undefined=StrictUndefined)
|
||||
@ -386,8 +380,6 @@ class TestUndefined:
|
||||
env.from_string('{{ missing|default("default", true) }}').render()
|
||||
== "default"
|
||||
)
|
||||
with pytest.raises(AttributeError):
|
||||
getattr(StrictUndefined, "__slots__") # noqa: B009
|
||||
assert env.from_string('{{ "foo" if false }}').render() == ""
|
||||
|
||||
def test_indexing_gives_undefined(self):
|
||||
@ -433,3 +425,11 @@ class TestLowLevel:
|
||||
env = CustomEnvironment()
|
||||
tmpl = env.from_string("{{ foo }}")
|
||||
assert tmpl.render() == "resolve-foo"
|
||||
|
||||
|
||||
def test_overlay_enable_async(env):
|
||||
assert not env.is_async
|
||||
assert not env.overlay().is_async
|
||||
env_async = env.overlay(enable_async=True)
|
||||
assert env_async.is_async
|
||||
assert not env_async.overlay(enable_async=False).is_async
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
import trio
|
||||
|
||||
from jinja2 import ChainableUndefined
|
||||
from jinja2 import DictLoader
|
||||
@ -14,15 +11,6 @@ from jinja2.exceptions import UndefinedError
|
||||
from jinja2.nativetypes import NativeEnvironment
|
||||
|
||||
|
||||
def _asyncio_run(async_fn, *args):
|
||||
return asyncio.run(async_fn(*args))
|
||||
|
||||
|
||||
@pytest.fixture(params=[_asyncio_run, trio.run], ids=["asyncio", "trio"])
|
||||
def run_async_fn(request):
|
||||
return request.param
|
||||
|
||||
|
||||
def test_basic_async(run_async_fn):
|
||||
t = Template(
|
||||
"{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True
|
||||
@ -461,9 +449,7 @@ class TestAsyncForLoop:
|
||||
|
||||
def test_reversed_bug(self, test_env_async):
|
||||
tmpl = test_env_async.from_string(
|
||||
"{% for i in items %}{{ i }}"
|
||||
"{% if not loop.last %}"
|
||||
",{% endif %}{% endfor %}"
|
||||
"{% for i in items %}{{ i }}{% if not loop.last %},{% endif %}{% endfor %}"
|
||||
)
|
||||
assert tmpl.render(items=reversed([3, 2, 1])) == "1,2,3"
|
||||
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
import trio
|
||||
from markupsafe import Markup
|
||||
|
||||
from jinja2 import Environment
|
||||
@ -29,15 +27,6 @@ def env_async():
|
||||
return Environment(enable_async=True)
|
||||
|
||||
|
||||
def _asyncio_run(async_fn, *args):
|
||||
return asyncio.run(async_fn(*args))
|
||||
|
||||
|
||||
@pytest.fixture(params=[_asyncio_run, trio.run], ids=["asyncio", "trio"])
|
||||
def run_async_fn(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def closing_factory():
|
||||
async with contextlib.AsyncExitStack() as stack:
|
||||
@ -277,6 +266,13 @@ def test_slice(env_async, items):
|
||||
)
|
||||
|
||||
|
||||
def test_unique_with_async_gen(env_async):
|
||||
items = ["a", "b", "c", "c", "a", "d", "z"]
|
||||
tmpl = env_async.from_string("{{ items|reject('==', 'z')|unique|list }}")
|
||||
out = tmpl.render(items=items)
|
||||
assert out == "['a', 'b', 'c', 'd']"
|
||||
|
||||
|
||||
def test_custom_async_filter(env_async, run_async_fn):
|
||||
async def customfilter(val):
|
||||
return str(val)
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from jinja2 import UndefinedError
|
||||
from jinja2.environment import Environment
|
||||
from jinja2.loaders import DictLoader
|
||||
|
||||
@ -26,3 +29,80 @@ def test_import_as_with_context_deterministic(tmp_path):
|
||||
expect = [f"'bar{i}': " for i in range(10)]
|
||||
found = re.findall(r"'bar\d': ", content)[:10]
|
||||
assert found == expect
|
||||
|
||||
|
||||
def test_top_level_set_vars_unpacking_deterministic(tmp_path):
|
||||
src = "\n".join(f"{{% set a{i}, b{i}, c{i} = tuple_var{i} %}}" for i in range(10))
|
||||
env = Environment(loader=DictLoader({"foo": src}))
|
||||
env.compile_templates(tmp_path, zip=None)
|
||||
name = os.listdir(tmp_path)[0]
|
||||
content = (tmp_path / name).read_text("utf8")
|
||||
expect = [
|
||||
f"context.vars.update({{'a{i}': l_0_a{i}, 'b{i}': l_0_b{i}, 'c{i}': l_0_c{i}}})"
|
||||
for i in range(10)
|
||||
]
|
||||
found = re.findall(
|
||||
r"context\.vars\.update\(\{'a\d': l_0_a\d, 'b\d': l_0_b\d, 'c\d': l_0_c\d\}\)",
|
||||
content,
|
||||
)[:10]
|
||||
assert found == expect
|
||||
expect = [
|
||||
f"context.exported_vars.update(('a{i}', 'b{i}', 'c{i}'))" for i in range(10)
|
||||
]
|
||||
found = re.findall(
|
||||
r"context\.exported_vars\.update\(\('a\d', 'b\d', 'c\d'\)\)",
|
||||
content,
|
||||
)[:10]
|
||||
assert found == expect
|
||||
|
||||
|
||||
def test_loop_set_vars_unpacking_deterministic(tmp_path):
|
||||
src = "\n".join(f" {{% set a{i}, b{i}, c{i} = tuple_var{i} %}}" for i in range(10))
|
||||
src = f"{{% for i in seq %}}\n{src}\n{{% endfor %}}"
|
||||
env = Environment(loader=DictLoader({"foo": src}))
|
||||
env.compile_templates(tmp_path, zip=None)
|
||||
name = os.listdir(tmp_path)[0]
|
||||
content = (tmp_path / name).read_text("utf8")
|
||||
expect = [
|
||||
f"_loop_vars.update({{'a{i}': l_1_a{i}, 'b{i}': l_1_b{i}, 'c{i}': l_1_c{i}}})"
|
||||
for i in range(10)
|
||||
]
|
||||
found = re.findall(
|
||||
r"_loop_vars\.update\(\{'a\d': l_1_a\d, 'b\d': l_1_b\d, 'c\d': l_1_c\d\}\)",
|
||||
content,
|
||||
)[:10]
|
||||
assert found == expect
|
||||
|
||||
|
||||
def test_block_set_vars_unpacking_deterministic(tmp_path):
|
||||
src = "\n".join(f" {{% set a{i}, b{i}, c{i} = tuple_var{i} %}}" for i in range(10))
|
||||
src = f"{{% block test %}}\n{src}\n{{% endblock test %}}"
|
||||
env = Environment(loader=DictLoader({"foo": src}))
|
||||
env.compile_templates(tmp_path, zip=None)
|
||||
name = os.listdir(tmp_path)[0]
|
||||
content = (tmp_path / name).read_text("utf8")
|
||||
expect = [
|
||||
f"_block_vars.update({{'a{i}': l_0_a{i}, 'b{i}': l_0_b{i}, 'c{i}': l_0_c{i}}})"
|
||||
for i in range(10)
|
||||
]
|
||||
found = re.findall(
|
||||
r"_block_vars\.update\(\{'a\d': l_0_a\d, 'b\d': l_0_b\d, 'c\d': l_0_c\d\}\)",
|
||||
content,
|
||||
)[:10]
|
||||
assert found == expect
|
||||
|
||||
|
||||
def test_undefined_import_curly_name():
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"{bad}": "{% from 'macro' import m %}{{ m() }}",
|
||||
"macro": "",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Must not raise `NameError: 'bad' is not defined`, as that would indicate
|
||||
# that `{bad}` is being interpreted as an f-string. It must be escaped.
|
||||
with pytest.raises(UndefinedError):
|
||||
env.get_template("{bad}").render()
|
||||
|
||||
@ -191,9 +191,7 @@ class TestForLoop:
|
||||
|
||||
def test_reversed_bug(self, env):
|
||||
tmpl = env.from_string(
|
||||
"{% for i in items %}{{ i }}"
|
||||
"{% if not loop.last %}"
|
||||
",{% endif %}{% endfor %}"
|
||||
"{% for i in items %}{{ i }}{% if not loop.last %},{% endif %}{% endfor %}"
|
||||
)
|
||||
assert tmpl.render(items=reversed([3, 2, 1])) == "1,2,3"
|
||||
|
||||
@ -538,6 +536,14 @@ class TestSet:
|
||||
)
|
||||
assert tmpl.render() == "13|37"
|
||||
|
||||
def test_namespace_set_tuple(self, env_trim):
|
||||
tmpl = env_trim.from_string(
|
||||
"{% set ns = namespace(a=12, b=36) %}"
|
||||
"{% set ns.a, ns.b = ns.a + 1, ns.b + 1 %}"
|
||||
"{{ ns.a }}|{{ ns.b }}"
|
||||
)
|
||||
assert tmpl.render() == "13|37"
|
||||
|
||||
def test_block_escaping_filtered(self):
|
||||
env = Environment(autoescape=True)
|
||||
tmpl = env.from_string(
|
||||
|
||||
@ -23,9 +23,9 @@ class TestDebug:
|
||||
|
||||
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
|
||||
), f"Traceback did not match:\n\n{''.join(tb)}\nexpected:\n{expected_tb}"
|
||||
assert m is not None, (
|
||||
f"Traceback did not match:\n\n{''.join(tb)}\nexpected:\n{expected_tb}"
|
||||
)
|
||||
|
||||
def test_runtime_error(self, fs_env):
|
||||
def test():
|
||||
|
||||
@ -554,8 +554,7 @@ class TestNewstyleInternationalization:
|
||||
newstyle=True,
|
||||
)
|
||||
t = env.from_string(
|
||||
'{% autoescape ae %}{{ gettext("foo", name='
|
||||
'"<test>") }}{% endautoescape %}'
|
||||
'{% autoescape ae %}{{ gettext("foo", name="<test>") }}{% endautoescape %}'
|
||||
)
|
||||
assert t.render(ae=True) == "<strong>Wert: <test></strong>"
|
||||
assert t.render(ae=False) == "<strong>Wert: <test></strong>"
|
||||
|
||||
@ -196,6 +196,7 @@ class TestFilter:
|
||||
("abc", "0"),
|
||||
("32.32", "32"),
|
||||
("12345678901234567890", "12345678901234567890"),
|
||||
("1e10000", "0"),
|
||||
),
|
||||
)
|
||||
def test_int(self, env, value, expect):
|
||||
@ -356,7 +357,7 @@ class TestFilter:
|
||||
def test_urlize(self, env):
|
||||
tmpl = env.from_string('{{ "foo example.org bar"|urlize }}')
|
||||
assert tmpl.render() == (
|
||||
'foo <a href="https://example.org" rel="noopener">' "example.org</a> bar"
|
||||
'foo <a href="https://example.org" rel="noopener">example.org</a> bar'
|
||||
)
|
||||
tmpl = env.from_string('{{ "foo http://www.example.com/ bar"|urlize }}')
|
||||
assert tmpl.render() == (
|
||||
|
||||
@ -43,8 +43,7 @@ class TestTokenStream:
|
||||
class TestLexer:
|
||||
def test_raw1(self, env):
|
||||
tmpl = env.from_string(
|
||||
"{% raw %}foo{% endraw %}|"
|
||||
"{%raw%}{{ bar }}|{% baz %}{% endraw %}"
|
||||
"{% raw %}foo{% endraw %}|{%raw%}{{ bar }}|{% baz %}{% endraw %}"
|
||||
)
|
||||
assert tmpl.render() == "foo|{{ bar }}|{% baz %}"
|
||||
|
||||
|
||||
@ -179,6 +179,24 @@ class TestFileSystemLoader:
|
||||
t = e.get_template("foo/test.html")
|
||||
assert t.filename == str(self.searchpath / "foo" / "test.html")
|
||||
|
||||
def test_error_includes_paths(self, env, filesystem_loader):
|
||||
env.loader = filesystem_loader
|
||||
|
||||
with pytest.raises(TemplateNotFound) as info:
|
||||
env.get_template("missing")
|
||||
|
||||
e_str = str(info.value)
|
||||
assert e_str.startswith("'missing' not found in search path: ")
|
||||
|
||||
filesystem_loader.searchpath.append("other")
|
||||
|
||||
with pytest.raises(TemplateNotFound) as info:
|
||||
env.get_template("missing")
|
||||
|
||||
e_str = str(info.value)
|
||||
assert e_str.startswith("'missing' not found in search paths: ")
|
||||
assert ", 'other'" in e_str
|
||||
|
||||
|
||||
class TestModuleLoader:
|
||||
archive = None
|
||||
@ -363,7 +381,7 @@ def test_package_zip_source(package_zip_loader, template, expect):
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
sys.implementation.name == "pypy" or sys.version_info > (3, 13),
|
||||
sys.implementation.name == "pypy",
|
||||
reason="zipimporter doesn't have a '_files' attribute",
|
||||
raises=TypeError,
|
||||
)
|
||||
@ -411,3 +429,8 @@ def test_pep_451_import_hook():
|
||||
assert "test.html" in package_loader.list_templates()
|
||||
finally:
|
||||
sys.meta_path[:] = before
|
||||
|
||||
|
||||
def test_package_loader_no_dir() -> None:
|
||||
with pytest.raises(ValueError, match="could not find a 'templates' directory"):
|
||||
PackageLoader("jinja2")
|
||||
|
||||
@ -13,6 +13,11 @@ def env():
|
||||
return NativeEnvironment()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def async_native_env():
|
||||
return NativeEnvironment(enable_async=True)
|
||||
|
||||
|
||||
def test_is_defined_native_return(env):
|
||||
t = env.from_string("{{ missing is defined }}")
|
||||
assert not t.render()
|
||||
@ -122,6 +127,18 @@ def test_string_top_level(env):
|
||||
assert result == "Jinja"
|
||||
|
||||
|
||||
def test_string_concatenation(async_native_env, run_async_fn):
|
||||
async def async_render():
|
||||
t = async_native_env.from_string(
|
||||
"{%- macro x(y) -%}{{ y }}{%- endmacro -%}{{- x('not') }} {{ x('bad') -}}"
|
||||
)
|
||||
result = await t.render_async()
|
||||
assert isinstance(result, str)
|
||||
assert result == "not bad"
|
||||
|
||||
run_async_fn(async_render)
|
||||
|
||||
|
||||
def test_tuple_of_variable_strings(env):
|
||||
t = env.from_string("'{{ a }}', 'data', '{{ b }}', b'{{ c }}'")
|
||||
result = t.render(a=1, b=2, c="bytes")
|
||||
@ -160,3 +177,13 @@ def test_macro(env):
|
||||
result = t.render()
|
||||
assert result == 2
|
||||
assert isinstance(result, int)
|
||||
|
||||
|
||||
def test_block(env):
|
||||
t = env.from_string(
|
||||
"{% block b %}{% for i in range(1) %}{{ loop.index }}{% endfor %}"
|
||||
"{% endblock %}{{ self.b() }}"
|
||||
)
|
||||
result = t.render()
|
||||
assert result == 11
|
||||
assert isinstance(result, int)
|
||||
|
||||
@ -737,6 +737,28 @@ End"""
|
||||
)
|
||||
assert tmpl.render() == "hellohellohello"
|
||||
|
||||
def test_pass_context_with_select(self, env):
|
||||
@pass_context
|
||||
def is_foo(ctx, s):
|
||||
assert ctx is not None
|
||||
return s == "foo"
|
||||
|
||||
env.tests["foo"] = is_foo
|
||||
tmpl = env.from_string(
|
||||
"{% for x in ['one', 'foo'] | select('foo') %}{{ x }}{% endfor %}"
|
||||
)
|
||||
assert tmpl.render() == "foo"
|
||||
|
||||
|
||||
def test_load_parameter_when_set_in_all_if_branches(env):
|
||||
tmpl = env.from_string(
|
||||
"{% if True %}{{ a.b }}{% set a = 1 %}"
|
||||
"{% elif False %}{% set a = 2 %}"
|
||||
"{% else %}{% set a = 3 %}{% endif %}"
|
||||
"{{ a }}"
|
||||
)
|
||||
assert tmpl.render(a={"b": 0}) == "01"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unicode_char", ["\N{FORM FEED}", "\x85"])
|
||||
def test_unicode_whitespace(env, unicode_char):
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
import copy
|
||||
import itertools
|
||||
import pickle
|
||||
|
||||
import pytest
|
||||
|
||||
from jinja2 import ChainableUndefined
|
||||
from jinja2 import DebugUndefined
|
||||
from jinja2 import StrictUndefined
|
||||
from jinja2 import Template
|
||||
from jinja2 import TemplateRuntimeError
|
||||
from jinja2 import Undefined
|
||||
from jinja2.runtime import LoopContext
|
||||
|
||||
TEST_IDX_TEMPLATE_STR_1 = (
|
||||
@ -73,3 +82,44 @@ def test_mock_not_pass_arg_marker():
|
||||
out = t.render(calc=Calc())
|
||||
# Would be "1" if context argument was passed.
|
||||
assert out == "0"
|
||||
|
||||
|
||||
_undefined_types = (Undefined, ChainableUndefined, DebugUndefined, StrictUndefined)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("undefined_type", _undefined_types)
|
||||
def test_undefined_copy(undefined_type):
|
||||
undef = undefined_type("a hint", ["foo"], "a name", TemplateRuntimeError)
|
||||
copied = copy.copy(undef)
|
||||
|
||||
assert copied is not undef
|
||||
assert copied._undefined_hint is undef._undefined_hint
|
||||
assert copied._undefined_obj is undef._undefined_obj
|
||||
assert copied._undefined_name is undef._undefined_name
|
||||
assert copied._undefined_exception is undef._undefined_exception
|
||||
|
||||
|
||||
@pytest.mark.parametrize("undefined_type", _undefined_types)
|
||||
def test_undefined_deepcopy(undefined_type):
|
||||
undef = undefined_type("a hint", ["foo"], "a name", TemplateRuntimeError)
|
||||
copied = copy.deepcopy(undef)
|
||||
|
||||
assert copied._undefined_hint is undef._undefined_hint
|
||||
assert copied._undefined_obj is not undef._undefined_obj
|
||||
assert copied._undefined_obj == undef._undefined_obj
|
||||
assert copied._undefined_name is undef._undefined_name
|
||||
assert copied._undefined_exception is undef._undefined_exception
|
||||
|
||||
|
||||
@pytest.mark.parametrize("undefined_type", _undefined_types)
|
||||
def test_undefined_pickle(undefined_type):
|
||||
undef = undefined_type("a hint", ["foo"], "a name", TemplateRuntimeError)
|
||||
copied = pickle.loads(pickle.dumps(undef))
|
||||
|
||||
assert copied._undefined_hint is not undef._undefined_hint
|
||||
assert copied._undefined_hint == undef._undefined_hint
|
||||
assert copied._undefined_obj is not undef._undefined_obj
|
||||
assert copied._undefined_obj == undef._undefined_obj
|
||||
assert copied._undefined_name is not undef._undefined_name
|
||||
assert copied._undefined_name == undef._undefined_name
|
||||
assert copied._undefined_exception is undef._undefined_exception
|
||||
|
||||
@ -58,6 +58,8 @@ class TestSandbox:
|
||||
def test_immutable_environment(self, env):
|
||||
env = ImmutableSandboxedEnvironment()
|
||||
pytest.raises(SecurityError, env.from_string("{{ [].append(23) }}").render)
|
||||
pytest.raises(SecurityError, env.from_string("{{ [].clear() }}").render)
|
||||
pytest.raises(SecurityError, env.from_string("{{ [1].pop() }}").render)
|
||||
pytest.raises(SecurityError, env.from_string("{{ {1:2}.clear() }}").render)
|
||||
|
||||
def test_restricted(self, env):
|
||||
@ -171,3 +173,30 @@ class TestStringFormatMap:
|
||||
'{{ ("a{x.foo}b{y}"|safe).format_map({"x":{"foo": 42}, "y":"<foo>"}) }}'
|
||||
)
|
||||
assert t.render() == "a42b<foo>"
|
||||
|
||||
def test_indirect_call(self):
|
||||
def run(value, arg):
|
||||
return value.run(arg)
|
||||
|
||||
env = SandboxedEnvironment()
|
||||
env.filters["run"] = run
|
||||
t = env.from_string(
|
||||
"""{% set
|
||||
ns = namespace(run="{0.__call__.__builtins__[__import__]}".format)
|
||||
%}
|
||||
{{ ns | run(not_here) }}
|
||||
"""
|
||||
)
|
||||
|
||||
with pytest.raises(SecurityError):
|
||||
t.render()
|
||||
|
||||
def test_attr_filter(self) -> None:
|
||||
env = SandboxedEnvironment()
|
||||
t = env.from_string(
|
||||
"""{{ "{0.__call__.__builtins__[__import__]}"
|
||||
| attr("format")(not_here) }}"""
|
||||
)
|
||||
|
||||
with pytest.raises(SecurityError):
|
||||
t.render()
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import copy
|
||||
import pickle
|
||||
import random
|
||||
from collections import deque
|
||||
@ -141,6 +142,14 @@ class TestEscapeUrlizeTarget:
|
||||
"http://example.org</a>"
|
||||
)
|
||||
|
||||
def test_urlize_mail_mastodon(self):
|
||||
fr = "nabijaczleweli@nabijaczleweli.xyz\n@eater@cijber.social\n"
|
||||
to = (
|
||||
'<a href="mailto:nabijaczleweli@nabijaczleweli.xyz">'
|
||||
"nabijaczleweli@nabijaczleweli.xyz</a>\n@eater@cijber.social\n"
|
||||
)
|
||||
assert urlize(fr) == to
|
||||
|
||||
|
||||
class TestLoremIpsum:
|
||||
def test_lorem_ipsum_markup(self):
|
||||
@ -183,3 +192,14 @@ def test_consume():
|
||||
consume(x)
|
||||
with pytest.raises(StopIteration):
|
||||
next(x)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1))
|
||||
def test_pickle_missing(protocol: int) -> None:
|
||||
"""Test that missing can be pickled while remaining a singleton."""
|
||||
assert pickle.loads(pickle.dumps(missing, protocol)) is missing
|
||||
|
||||
|
||||
def test_copy_missing() -> None:
|
||||
"""Test that missing can be copied while remaining a singleton."""
|
||||
assert copy.copy(missing) is missing
|
||||
|
||||
58
tox.ini
58
tox.ini
@ -1,58 +0,0 @@
|
||||
[tox]
|
||||
envlist =
|
||||
py3{13,12,11,10,9,8}
|
||||
pypy310
|
||||
style
|
||||
typing
|
||||
docs
|
||||
skip_missing_interpreters = true
|
||||
|
||||
[testenv]
|
||||
package = wheel
|
||||
wheel_build_env = .pkg
|
||||
constrain_package_deps = true
|
||||
use_frozen_constraints = true
|
||||
deps = -r requirements/tests.txt
|
||||
commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs}
|
||||
|
||||
[testenv:py37,py3.7]
|
||||
deps = -r requirements/tests37.txt
|
||||
|
||||
[testenv:style]
|
||||
deps = pre-commit
|
||||
skip_install = true
|
||||
commands = pre-commit run --all-files
|
||||
|
||||
[testenv:typing]
|
||||
deps = -r requirements/typing.txt
|
||||
commands = mypy
|
||||
|
||||
[testenv:docs]
|
||||
deps = -r requirements/docs.txt
|
||||
commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml
|
||||
|
||||
[testenv:update-pre_commit]
|
||||
labels = update
|
||||
deps = pre-commit
|
||||
skip_install = true
|
||||
commands = pre-commit autoupdate -j4
|
||||
|
||||
[testenv:update-requirements]
|
||||
labels = update
|
||||
deps = pip-tools
|
||||
skip_install = true
|
||||
change_dir = requirements
|
||||
commands =
|
||||
pip-compile build.in -q {posargs:-U}
|
||||
pip-compile docs.in -q {posargs:-U}
|
||||
pip-compile tests.in -q {posargs:-U}
|
||||
pip-compile typing.in -q {posargs:-U}
|
||||
pip-compile dev.in -q {posargs:-U}
|
||||
|
||||
[testenv:update-requirements37]
|
||||
base_python = 3.7
|
||||
labels = update
|
||||
deps = pip-tools
|
||||
skip_install = true
|
||||
change_dir = requirements
|
||||
commands = pip-compile tests37.in -q {posargs:-U}
|
||||
Loading…
Reference in New Issue
Block a user