Compare commits
347 Commits
| 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 | ||
|
|
8710cabd4f | ||
|
|
90750800d4 | ||
|
|
8a8e2bc4d7 | ||
|
|
679af7f816 | ||
|
|
b002d9c6c3 | ||
|
|
e82013c399 | ||
|
|
004476c22b | ||
|
|
1655128cfc | ||
|
|
079e8312c3 | ||
|
|
5bc613ec45 | ||
|
|
2fcabb529f | ||
|
|
a516a99bab | ||
|
|
a89ed5fe0f | ||
|
|
11550f9df9 | ||
|
|
6e7b0face6 | ||
|
|
dd4a8b5466 | ||
|
|
0668239dc6 | ||
|
|
bbd5bcee7b | ||
|
|
d655030770 | ||
|
|
a7863ba9d3 | ||
|
|
b5c98e78c2 | ||
|
|
c6dd4bac24 | ||
|
|
6fcf463011 | ||
|
|
27ea85b001 | ||
|
|
2e8bbca767 | ||
|
|
4a7a153a48 | ||
|
|
2a17038fca | ||
|
|
c8aca74587 | ||
|
|
9b33637538 | ||
|
|
2e3e3774a9 | ||
|
|
6d6a6c2546 | ||
|
|
a2438d20b0 | ||
|
|
fcd3d3bbf3 | ||
|
|
de6131232a | ||
|
|
da3a9f0b80 | ||
|
|
0ee5eb41d1 | ||
|
|
20477c6357 | ||
|
|
e491223739 | ||
|
|
3fd91e4d11 | ||
|
|
36f98854c7 | ||
|
|
a0e864ec0f | ||
|
|
d9de4bb215 | ||
|
|
50124e1656 | ||
|
|
9ea7222ef3 | ||
|
|
da703f7aae | ||
|
|
bce1746925 | ||
|
|
7f8fb54782 | ||
|
|
7277d8068b | ||
|
|
5c8a105224 | ||
|
|
19a55db3b4 | ||
|
|
716795349a | ||
|
|
7dd3680e6e | ||
|
|
d594969d72 | ||
|
|
ec22f25312 | ||
|
|
21fa43ca01 | ||
|
|
938e7ca5bb | ||
|
|
f0685845e1 | ||
|
|
fcafd5087b | ||
|
|
86f28a9df0 | ||
|
|
f272b6d8b6 | ||
|
|
9db787b566 | ||
|
|
f575dc7385 | ||
|
|
d84a1743e4 | ||
|
|
4504beba06 | ||
|
|
31b764ea83 | ||
|
|
ac57ea048d | ||
|
|
2a2bfb7f95 | ||
|
|
ec2649688d | ||
|
|
859039244a | ||
|
|
890c8a9519 | ||
|
|
505effc995 | ||
|
|
956b1f1ce9 | ||
|
|
c4b8d066d6 | ||
|
|
5258c9d27d | ||
|
|
534c9e13ed | ||
|
|
8310b2bbef | ||
|
|
fed2d0808f | ||
|
|
d80f186832 | ||
|
|
c0e4f69ff3 | ||
|
|
8d0ea74289 | ||
|
|
fd5128f864 | ||
|
|
02df0dad29 | ||
|
|
7b48764688 | ||
|
|
4b18fd4f1f | ||
|
|
3914664578 | ||
|
|
81a23847cd | ||
|
|
ae312b3065 | ||
|
|
051df10c7b | ||
|
|
37f5b058ee | ||
|
|
23aab8330c | ||
|
|
7e03bef475 | ||
|
|
b364f26a11 | ||
|
|
bfceede8ad | ||
|
|
7e691a0862 | ||
|
|
235ecaf576 | ||
|
|
a24a4b1574 | ||
|
|
953acd65b2 | ||
|
|
c5685b6dc4 | ||
|
|
7018e3fc76 | ||
|
|
2cd9ed2ad9 | ||
|
|
1e357f34ff | ||
|
|
055bbfd1fe | ||
|
|
090a5a9e22 | ||
|
|
6fef24ce45 | ||
|
|
c8c3c846d5 | ||
|
|
85e5ad85d1 | ||
|
|
3067df7261 | ||
|
|
23e5cec554 | ||
|
|
c48f131143 | ||
|
|
4863d6534f | ||
|
|
80f4e5586a | ||
|
|
be0bcb61f9 | ||
|
|
832bdaadfc | ||
|
|
f39ffa0d8a | ||
|
|
9dad679695 | ||
|
|
47957d571c | ||
|
|
7b3cb76e71 | ||
|
|
02e058df6c | ||
|
|
b7cb6ee667 | ||
|
|
3e07d14a0f | ||
|
|
291dfe27d5 | ||
|
|
42b3a35410 | ||
|
|
37561cead6 | ||
|
|
56f7c4e083 | ||
|
|
c01f51b345 | ||
|
|
1cc0b63e2f | ||
|
|
96a83e9014 | ||
|
|
795ab3db02 | ||
|
|
50a5fd4fb2 | ||
|
|
102ba5d688 | ||
|
|
05a5f8120a | ||
|
|
8ed8e1d0ed | ||
|
|
614b045fab | ||
|
|
a9c8111d24 | ||
|
|
048a068697 | ||
|
|
89eec1c5ee | ||
|
|
495b889b7c | ||
|
|
623df9b3f2 | ||
|
|
782151081d | ||
|
|
80e7a83235 | ||
|
|
e46be11e3e | ||
|
|
45c23ea56d | ||
|
|
e026c72c19 | ||
|
|
36b601f24b | ||
|
|
3fadee01b7 | ||
|
|
5b13cea00a | ||
|
|
522391c5bd | ||
|
|
e0486050d3 | ||
|
|
7e691ed15b | ||
|
|
6a8246be1e | ||
|
|
ae53ea5350 | ||
|
|
46298e0c6b | ||
|
|
1746bcdfd5 | ||
|
|
3ccc61b0bd | ||
|
|
5d9ece6d65 | ||
|
|
bf251517c1 | ||
|
|
b1bb29d292 | ||
|
|
0a10079f33 | ||
|
|
e740cc65d5 | ||
|
|
c436c9f18f | ||
|
|
9fde7eb820 | ||
|
|
15e4959a2e | ||
|
|
997f7f5243 | ||
|
|
c3fdbac68c | ||
|
|
9e1895f0e2 | ||
|
|
7f936deac0 | ||
|
|
769921b12b | ||
|
|
f07a7b5229 | ||
|
|
c4bff4efee | ||
|
|
4bfc33a276 | ||
|
|
5e636989f0 | ||
|
|
7fb13bf944 | ||
|
|
32708a5ce8 | ||
|
|
94fd2d9241 | ||
|
|
e9cb0a5fb5 | ||
|
|
8fec9b4220 | ||
|
|
fcefbe7472 | ||
|
|
dd22b7a10e | ||
|
|
6089d142c1 | ||
|
|
033bfd12d7 | ||
|
|
bd07dfbf18 | ||
|
|
b8b2c6b445 | ||
|
|
0d177809e2 | ||
|
|
5cede152e8 | ||
|
|
c9593aa388 | ||
|
|
47493d082c | ||
|
|
1b714c7e82 | ||
|
|
b08cd4bc64 | ||
|
|
1e68ba8617 | ||
|
|
52843b5cbf | ||
|
|
8efee35092 | ||
|
|
a24df26d54 | ||
|
|
9faee281ea | ||
|
|
b802b5a6ad | ||
|
|
746bb95780 | ||
|
|
a0dd7753d0 | ||
|
|
466a200ea4 | ||
|
|
990602f719 | ||
|
|
5d3d241471 | ||
|
|
21da8f5298 | ||
|
|
ea69e41db3 | ||
|
|
c3a61d6ef6 | ||
|
|
9b521347aa | ||
|
|
8fd4b28a22 | ||
|
|
2c8e84db29 | ||
|
|
4f5630f43b | ||
|
|
d6b4900742 | ||
|
|
a7eedafa2a | ||
|
|
6f79daafe8 | ||
|
|
e542e10002 |
17
.devcontainer/devcontainer.json
Normal file
17
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "pallets/jinja",
|
||||
"image": "mcr.microsoft.com/devcontainers/python:3",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": "${workspaceFolder}/.venv",
|
||||
"python.terminal.activateEnvInCurrentTerminal": true,
|
||||
"python.terminal.launchArgs": [
|
||||
"-X",
|
||||
"dev"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"onCreateCommand": ".devcontainer/on-create-command.sh"
|
||||
}
|
||||
17
.devcontainer/on-create-command.sh
Executable file
17
.devcontainer/on-create-command.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
set -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
|
||||
@ -9,5 +9,5 @@ end_of_line = lf
|
||||
charset = utf-8
|
||||
max_line_length = 88
|
||||
|
||||
[*.{yml,yaml,json,js,css,html}]
|
||||
[*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}]
|
||||
indent_size = 2
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug-report.md
vendored
4
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -4,8 +4,8 @@ about: Report a bug in Jinja (not other projects which depend on Jinja)
|
||||
---
|
||||
|
||||
<!--
|
||||
This issue tracker is a tool to address bugs in Jinja itself. Please
|
||||
use Pallets Discord or Stack Overflow for questions about your own code.
|
||||
This issue tracker is a tool to address bugs in Jinja itself. Please use
|
||||
GitHub Discussions or the Pallets Discord for questions about your own code.
|
||||
|
||||
Replace this comment with a clear outline of what the bug is.
|
||||
-->
|
||||
|
||||
13
.github/ISSUE_TEMPLATE/config.yml
vendored
13
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,11 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Security issue
|
||||
url: security@palletsprojects.com
|
||||
about: Do not report security issues publicly. Email our security contact.
|
||||
- name: Questions
|
||||
url: https://stackoverflow.com/questions/tagged/Jinja?tab=Frequent
|
||||
about: Search for and ask questions about your code on Stack Overflow.
|
||||
- name: Questions and discussions
|
||||
- name: Questions on Discussions
|
||||
url: https://github.com/pallets/jinja/discussions/
|
||||
about: Ask questions about your own code on the Discussions tab.
|
||||
- name: Questions on Chat
|
||||
url: https://discord.gg/pallets
|
||||
about: Discuss questions about your code on our Discord chat.
|
||||
about: Ask questions about your own code on our Discord chat.
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/feature-request.md
vendored
6
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@ -5,11 +5,11 @@ about: Suggest a new feature for Jinja
|
||||
|
||||
<!--
|
||||
Replace this comment with a description of what the feature should do.
|
||||
Include details such as links relevant specs or previous discussions.
|
||||
Include details such as links to relevant specs or previous discussions.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Replace this comment with an example of the problem which this feature
|
||||
would resolve. Is this problem solvable without changes to Jinja,
|
||||
such as by subclassing or using an extension?
|
||||
would resolve. Is this problem solvable without changes to Jinja, such
|
||||
as by subclassing or using an extension?
|
||||
-->
|
||||
|
||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@ -1,9 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
day: "monday"
|
||||
time: "16:00"
|
||||
timezone: "UTC"
|
||||
31
.github/pull_request_template.md
vendored
31
.github/pull_request_template.md
vendored
@ -1,26 +1,25 @@
|
||||
<!--
|
||||
Before opening a PR, open a ticket describing the issue or feature the PR will address. Follow the steps in CONTRIBUTING.rst.
|
||||
Before opening a PR, open a ticket describing the issue or feature the
|
||||
PR will address. An issue is not required for fixing typos in
|
||||
documentation, or other simple non-code changes.
|
||||
|
||||
Replace this comment with a description of the change. Describe how it addresses the linked ticket.
|
||||
Replace this comment with a description of the change. Describe how it
|
||||
addresses the linked ticket.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Link to relevant issues or previous PRs, one per line. Use "fixes" to automatically close an issue.
|
||||
-->
|
||||
Link to relevant issues or previous PRs, one per line. Use "fixes" to
|
||||
automatically close an issue.
|
||||
|
||||
- fixes #<issue number>
|
||||
fixes #<issue number>
|
||||
-->
|
||||
|
||||
<!--
|
||||
Ensure each step in CONTRIBUTING.rst is complete by adding an "x" to each box below.
|
||||
Ensure each step in CONTRIBUTING.rst is complete, especially the following:
|
||||
|
||||
If only docs were changed, these aren't relevant and can be removed.
|
||||
- Add tests that demonstrate the correct behavior of the change. Tests
|
||||
should fail without the change.
|
||||
- Add or update relevant docs, in the docs folder and in code.
|
||||
- Add an entry in CHANGES.rst summarizing the change and linking to the issue.
|
||||
- Add `.. versionchanged::` entries in any relevant code docs.
|
||||
-->
|
||||
|
||||
Checklist:
|
||||
|
||||
- [ ] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
|
||||
- [ ] Add or update relevant docs, in the docs folder and in code.
|
||||
- [ ] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
|
||||
- [ ] Add `.. versionchanged::` entries in any relevant code docs.
|
||||
- [ ] Run `pre-commit` hooks and fix any issues.
|
||||
- [ ] Run `pytest` and `tox`, no tests failed.
|
||||
|
||||
17
.github/workflows/lock.yaml
vendored
17
.github/workflows/lock.yaml
vendored
@ -1,15 +1,24 @@
|
||||
name: 'Lock threads'
|
||||
name: Lock inactive closed issues
|
||||
# Lock closed issues that have not received any further activity for two weeks.
|
||||
# This does not close open issues, only humans may do that. It is easier to
|
||||
# respond to new issues with fresh examples rather than continuing discussions
|
||||
# on old issues.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
concurrency:
|
||||
group: lock
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v3
|
||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: 14
|
||||
pr-inactive-days: 14
|
||||
discussion-inactive-days: 14
|
||||
|
||||
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() }}
|
||||
47
.github/workflows/publish.yaml
vendored
Normal file
47
.github/workflows/publish.yaml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
name: Publish
|
||||
on:
|
||||
push:
|
||||
tags: ['*']
|
||||
jobs:
|
||||
build:
|
||||
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
|
||||
with:
|
||||
python-version-file: pyproject.toml
|
||||
- run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
|
||||
- run: uv build
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
path: ./dist
|
||||
create-release:
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
- name: create release
|
||||
run: >
|
||||
gh release create --draft --repo ${{ github.repository }}
|
||||
${{ github.ref_name }} artifact/*
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
publish-pypi:
|
||||
needs: [build]
|
||||
environment:
|
||||
name: publish
|
||||
url: https://pypi.org/project/Jinja2/${{ github.ref_name }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
- uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
with:
|
||||
packages-dir: artifact/
|
||||
74
.github/workflows/tests.yaml
vendored
74
.github/workflows/tests.yaml
vendored
@ -1,55 +1,49 @@
|
||||
name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- '*.x'
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '*.rst'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- '*.x'
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '*.rst'
|
||||
paths-ignore: ['docs/**', 'README.md']
|
||||
push:
|
||||
branches: [main, stable]
|
||||
paths-ignore: ['docs/**', 'README.md']
|
||||
jobs:
|
||||
tests:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.name || matrix.python }}
|
||||
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- {name: Linux, python: '3.10', os: ubuntu-latest, tox: py310}
|
||||
- {name: Windows, python: '3.10', os: windows-latest, tox: py310}
|
||||
- {name: Mac, python: '3.10', os: macos-latest, tox: py310}
|
||||
- {name: '3.11-dev', python: '3.11-dev', os: ubuntu-latest, tox: py311}
|
||||
- {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39}
|
||||
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
|
||||
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
|
||||
- {name: 'PyPy', python: 'pypy-3.7', os: ubuntu-latest, tox: pypy37}
|
||||
- {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing}
|
||||
- {python: '3.13'}
|
||||
- {name: Windows, python: '3.13', os: windows-latest}
|
||||
- {name: Mac, python: '3.13', os: macos-latest}
|
||||
- {python: '3.12'}
|
||||
- {python: '3.11'}
|
||||
- {python: '3.10'}
|
||||
- {name: PyPy, python: 'pypy-3.11', tox: pypy3.11}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v3
|
||||
- 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 }}
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'requirements/*.txt'
|
||||
- name: update pip
|
||||
run: |
|
||||
pip install -U wheel
|
||||
pip install -U setuptools
|
||||
python -m pip install -U pip
|
||||
- run: uv run --locked tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }}
|
||||
typing:
|
||||
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
|
||||
with:
|
||||
python-version-file: pyproject.toml
|
||||
- name: cache mypy
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ./.mypy_cache
|
||||
key: mypy|${{ matrix.python }}|${{ hashFiles('setup.cfg') }}
|
||||
if: matrix.tox == 'typing'
|
||||
- run: pip install tox
|
||||
- run: tox -e ${{ matrix.tox }}
|
||||
key: mypy|${{ hashFiles('pyproject.toml') }}
|
||||
- run: uv run --locked tox run -e typing
|
||||
|
||||
27
.gitignore
vendored
27
.gitignore
vendored
@ -1,21 +1,8 @@
|
||||
*.so
|
||||
docs/_build/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.egg-info/
|
||||
*.egg
|
||||
build/
|
||||
dist/
|
||||
.DS_Store
|
||||
.tox/
|
||||
.cache/
|
||||
.idea/
|
||||
env/
|
||||
venv/
|
||||
venv-*/
|
||||
.coverage
|
||||
.coverage.*
|
||||
htmlcov
|
||||
.pytest_cache/
|
||||
/.vscode/
|
||||
.mypy_cache
|
||||
.vscode/
|
||||
__pycache__/
|
||||
dist/
|
||||
.coverage*
|
||||
htmlcov/
|
||||
.tox/
|
||||
docs/_build/
|
||||
|
||||
@ -1,34 +1,18 @@
|
||||
ci:
|
||||
autoupdate_branch: "3.0.x"
|
||||
autoupdate_schedule: monthly
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.31.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 76e47323a83cd9795e4ff9a1de1c0d2eef610f17 # frozen: v0.11.11
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ["--py37-plus"]
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v2.7.1
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||
rev: 648bdbfd6bb1a82f132ecc2c666e0d1b2e4b0d94 # frozen: 0.7.8
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ["--application-directories", "src"]
|
||||
additional_dependencies: ["setuptools>60.9"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-bugbear]
|
||||
- repo: https://github.com/peterdemin/pip-compile-multi
|
||||
rev: v2.4.3
|
||||
hooks:
|
||||
- id: pip-compile-multi-verify
|
||||
- id: uv-lock
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.1.0
|
||||
rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: debug-statements
|
||||
- id: fix-byte-order-marker
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
version: 2
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-24.04
|
||||
tools:
|
||||
python: "3.10"
|
||||
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
|
||||
|
||||
115
CHANGES.rst
115
CHANGES.rst
@ -1,5 +1,113 @@
|
||||
.. currentmodule:: jinja2
|
||||
|
||||
Version 3.2.0
|
||||
-------------
|
||||
|
||||
Unreleased
|
||||
|
||||
- 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
|
||||
-------------
|
||||
|
||||
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`
|
||||
- Return an ``aclose``-able ``AsyncGenerator`` from
|
||||
``Template.generate_async``. :pr:`1960`
|
||||
- Avoid leaving ``root_render_func()`` unclosed in
|
||||
``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
|
||||
-------------
|
||||
|
||||
Released 2024-05-05
|
||||
|
||||
- The ``xmlattr`` filter does not allow keys with ``/`` solidus, ``>``
|
||||
greater-than sign, or ``=`` equals sign, in addition to disallowing spaces.
|
||||
Regardless of any validation done by Jinja, user input should never be used
|
||||
as keys to this filter, or must be separately validated first.
|
||||
:ghsa:`h75v-3vvj-5mfj`
|
||||
|
||||
|
||||
Version 3.1.3
|
||||
-------------
|
||||
|
||||
Released 2024-01-10
|
||||
|
||||
- Fix compiler error when checking if required blocks in parent templates are
|
||||
empty. :pr:`1858`
|
||||
- ``xmlattr`` filter does not allow keys with spaces. :ghsa:`h5c8-rqwp-cp95`
|
||||
- Make error messages stemming from invalid nesting of ``{% trans %}`` blocks
|
||||
more helpful. :pr:`1918`
|
||||
|
||||
|
||||
Version 3.1.2
|
||||
-------------
|
||||
|
||||
Released 2022-04-28
|
||||
|
||||
- Add parameters to ``Environment.overlay`` to match ``__init__``.
|
||||
:issue:`1645`
|
||||
- Handle race condition in ``FileSystemBytecodeCache``. :issue:`1654`
|
||||
|
||||
|
||||
Version 3.1.1
|
||||
-------------
|
||||
|
||||
@ -98,9 +206,8 @@ Released 2021-05-18
|
||||
extensions shows more relevant context. :issue:`1429`
|
||||
- Fixed calling deprecated ``jinja2.Markup`` without an argument.
|
||||
Use ``markupsafe.Markup`` instead. :issue:`1438`
|
||||
- Calling sync ``render`` for an async template uses ``asyncio.run``
|
||||
on Python >= 3.7. This fixes a deprecation that Python 3.10
|
||||
introduces. :issue:`1443`
|
||||
- Calling sync ``render`` for an async template uses ``asyncio.new_event_loop``
|
||||
This fixes a deprecation that Python 3.10 introduces. :issue:`1443`
|
||||
|
||||
|
||||
Version 3.0.0
|
||||
@ -952,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
|
||||
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at report@palletsprojects.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
222
CONTRIBUTING.rst
222
CONTRIBUTING.rst
@ -1,222 +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
|
||||
|
||||
- Upgrade pip and setuptools.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ python -m pip install --upgrade pip setuptools
|
||||
|
||||
- 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
MANIFEST.in
10
MANIFEST.in
@ -1,10 +0,0 @@
|
||||
include CHANGES.rst
|
||||
include tox.ini
|
||||
include requirements/*.txt
|
||||
graft artwork
|
||||
graft docs
|
||||
prune docs/_build
|
||||
graft examples
|
||||
graft tests
|
||||
include src/jinja2/py.typed
|
||||
global-exclude *.pyc
|
||||
@ -1,5 +1,6 @@
|
||||
Jinja
|
||||
=====
|
||||
<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
|
||||
placeholders in the template allow writing code similar to Python
|
||||
@ -26,53 +27,33 @@ possible, it shouldn't make the template designer's job difficult by
|
||||
restricting functionality too much.
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
## In A Nutshell
|
||||
|
||||
Install and update using `pip`_:
|
||||
```jinja
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Members{% endblock %}
|
||||
{% block content %}
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ pip install -U Jinja2
|
||||
|
||||
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||
|
||||
|
||||
In A Nutshell
|
||||
-------------
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Members{% endblock %}
|
||||
{% block content %}
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
Donate
|
||||
------
|
||||
## Donate
|
||||
|
||||
The Pallets organization develops and supports Jinja and other popular
|
||||
packages. In order to grow the community of contributors and users, and
|
||||
allow the maintainers to devote more time to the projects, `please
|
||||
donate today`_.
|
||||
allow the maintainers to devote more time to the projects, [please
|
||||
donate today][].
|
||||
|
||||
.. _please donate today: https://palletsprojects.com/donate
|
||||
[please donate today]: https://palletsprojects.com/donate
|
||||
|
||||
## Contributing
|
||||
|
||||
Links
|
||||
-----
|
||||
See our [detailed contributing documentation][contrib] for many ways to
|
||||
contribute, including reporting issues, requesting features, asking or answering
|
||||
questions, and making PRs.
|
||||
|
||||
- Documentation: https://jinja.palletsprojects.com/
|
||||
- Changes: https://jinja.palletsprojects.com/changes/
|
||||
- PyPI Releases: https://pypi.org/project/Jinja2/
|
||||
- Source Code: https://github.com/pallets/jinja/
|
||||
- Issue Tracker: https://github.com/pallets/jinja/issues/
|
||||
- Website: https://palletsprojects.com/p/jinja/
|
||||
- Twitter: https://twitter.com/PalletsTeam
|
||||
- Chat: https://discord.gg/pallets
|
||||
[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 |
15
docs/api.rst
15
docs/api.rst
@ -515,9 +515,6 @@ environment to compile different code behind the scenes in order to
|
||||
handle async and sync code in an asyncio event loop. This has the
|
||||
following implications:
|
||||
|
||||
- Template rendering requires an event loop to be available to the
|
||||
current thread. :func:`asyncio.get_running_loop` must return an
|
||||
event loop.
|
||||
- The compiled code uses ``await`` for functions and attributes, and
|
||||
uses ``async for`` loops. In order to support using both async and
|
||||
sync functions in this context, a small wrapper is placed around
|
||||
@ -669,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
|
||||
@ -700,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:
|
||||
@ -751,8 +748,8 @@ Now it can be used in templates:
|
||||
{% endif %}
|
||||
|
||||
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
|
||||
being filtered the second argument.
|
||||
the test. The object is passed as the first argument, making the value
|
||||
being tested the second argument.
|
||||
|
||||
- :func:`pass_environment` passes the :class:`Environment`.
|
||||
- :func:`pass_eval_context` passes the :ref:`eval-context`.
|
||||
|
||||
28
docs/conf.py
28
docs/conf.py
@ -10,17 +10,25 @@ release, version = get_version("Jinja2")
|
||||
|
||||
# General --------------------------------------------------------------
|
||||
|
||||
master_doc = "index"
|
||||
default_role = "code"
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.extlinks",
|
||||
"sphinx.ext.intersphinx",
|
||||
"pallets_sphinx_themes",
|
||||
"sphinxcontrib.log_cabinet",
|
||||
"sphinx_issues",
|
||||
"pallets_sphinx_themes",
|
||||
]
|
||||
autodoc_member_order = "bysource"
|
||||
autodoc_typehints = "description"
|
||||
intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
|
||||
issues_github_path = "pallets/jinja"
|
||||
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/pallets/jinja/security/advisories/GHSA-%s", "GHSA-%s"),
|
||||
}
|
||||
intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3/", None),
|
||||
}
|
||||
|
||||
# HTML -----------------------------------------------------------------
|
||||
|
||||
@ -32,8 +40,6 @@ html_context = {
|
||||
ProjectLink("PyPI Releases", "https://pypi.org/project/Jinja2/"),
|
||||
ProjectLink("Source Code", "https://github.com/pallets/jinja/"),
|
||||
ProjectLink("Issue Tracker", "https://github.com/pallets/jinja/issues/"),
|
||||
ProjectLink("Website", "https://palletsprojects.com/p/jinja/"),
|
||||
ProjectLink("Twitter", "https://twitter.com/PalletsTeam"),
|
||||
ProjectLink("Chat", "https://discord.gg/pallets"),
|
||||
]
|
||||
}
|
||||
@ -43,11 +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
|
||||
|
||||
# LaTeX ----------------------------------------------------------------
|
||||
|
||||
latex_documents = [(master_doc, f"Jinja-{version}.tex", html_title, author, "manual")]
|
||||
|
||||
@ -5,7 +5,6 @@ from jinja2.ext import Extension
|
||||
from jinja2.lexer import count_newlines
|
||||
from jinja2.lexer import Token
|
||||
|
||||
|
||||
_outside_re = re.compile(r"\\?(gettext|_)\(")
|
||||
_inside_re = re.compile(r"\\?[()]")
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -91,4 +91,4 @@ this add this to ``config/environment.py``:
|
||||
|
||||
config['pylons.strict_c'] = True
|
||||
|
||||
.. _Pylons: https://pylonshq.com/
|
||||
.. _Pylons: https://pylonsproject.org/
|
||||
|
||||
@ -30,7 +30,7 @@ Installation
|
||||
------------
|
||||
|
||||
We recommend using the latest version of Python. Jinja supports Python
|
||||
3.7 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
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
BSD-3-Clause License
|
||||
====================
|
||||
|
||||
.. include:: ../LICENSE.rst
|
||||
.. literalinclude:: ../LICENSE.txt
|
||||
:language: text
|
||||
|
||||
@ -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
|
||||
---
|
||||
|
||||
|
||||
@ -84,7 +84,7 @@ In Django, the special variable for the loop context is called
|
||||
.. code-block:: django
|
||||
|
||||
{% for item in items %}
|
||||
{{ item }}
|
||||
{{ forloop.counter }}. {{ item }}
|
||||
{% empty %}
|
||||
No items!
|
||||
{% endfor %}
|
||||
@ -95,7 +95,7 @@ and the ``else`` block is used for no loop items.
|
||||
.. code-block:: jinja
|
||||
|
||||
{% for item in items %}
|
||||
{{ loop.index}}. {{ item }}
|
||||
{{ loop.index }}. {{ item }}
|
||||
{% else %}
|
||||
No items!
|
||||
{% endfor %}
|
||||
|
||||
@ -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>
|
||||
@ -599,7 +605,8 @@ first and pass it in to ``render``.
|
||||
else:
|
||||
layout = env.get_template("layout.html")
|
||||
|
||||
user_detail = env.get_template("user/detail.html", layout=layout)
|
||||
user_detail = env.get_template("user/detail.html")
|
||||
return user_detail.render(layout=layout)
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
@ -1014,6 +1021,9 @@ template data. Just wrap the code in the special `filter` section::
|
||||
This text becomes uppercase
|
||||
{% endfilter %}
|
||||
|
||||
Filters that accept arguments can be called like this::
|
||||
|
||||
{% filter center(100) %}Center this{% endfilter %}
|
||||
|
||||
.. _assignments:
|
||||
|
||||
@ -1076,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:
|
||||
|
||||
@ -1168,7 +1178,7 @@ none of the templates exist.
|
||||
{% include ['special_sidebar.html', 'sidebar.html'] ignore missing %}
|
||||
|
||||
A variable, with either a template name or template object, can also be
|
||||
passed to the statment.
|
||||
passed to the statement.
|
||||
|
||||
.. _import:
|
||||
|
||||
@ -1402,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
|
||||
@ -1607,8 +1621,7 @@ The following functions are available in the global scope by default:
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
.. method:: current
|
||||
:property:
|
||||
.. property:: current
|
||||
|
||||
Return the current item. Equivalent to the item that will be
|
||||
returned next time :meth:`next` is called.
|
||||
@ -1665,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
|
||||
----------
|
||||
@ -1775,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() }}
|
||||
|
||||
211
pyproject.toml
Normal file
211
pyproject.toml
Normal file
@ -0,0 +1,211 @@
|
||||
[project]
|
||||
name = "Jinja2"
|
||||
version = "3.2.0.dev"
|
||||
description = "A very fast and expressive template engine."
|
||||
readme = "README.md"
|
||||
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",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||
"Topic :: Text Processing :: Markup :: HTML",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
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/page/changes/"
|
||||
Source = "https://github.com/pallets/jinja/"
|
||||
Chat = "https://discord.gg/pallets"
|
||||
|
||||
[project.optional-dependencies]
|
||||
i18n = ["Babel>=2.17"]
|
||||
|
||||
[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"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
|
||||
[tool.flit.module]
|
||||
name = "jinja2"
|
||||
|
||||
[tool.flit.sdist]
|
||||
include = [
|
||||
"docs/",
|
||||
"examples/",
|
||||
"tests/",
|
||||
"CHANGES.rst",
|
||||
"uv.lock"
|
||||
]
|
||||
exclude = [
|
||||
"docs/_build/",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
default-groups = ["dev", "pre-commit", "tests", "typing"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
filterwarnings = [
|
||||
"error",
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
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.10"
|
||||
files = ["src"]
|
||||
show_error_codes = true
|
||||
pretty = true
|
||||
strict = true
|
||||
|
||||
[tool.pyright]
|
||||
pythonVersion = "3.10"
|
||||
include = ["src"]
|
||||
typeCheckingMode = "standard"
|
||||
|
||||
[tool.ruff]
|
||||
src = ["src"]
|
||||
fix = true
|
||||
show-fixes = true
|
||||
output-format = "full"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"B", # flake8-bugbear
|
||||
"E", # pycodestyle error
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"W", # pycodestyle warning
|
||||
]
|
||||
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,6 +0,0 @@
|
||||
-r docs.in
|
||||
-r tests.in
|
||||
-r typing.in
|
||||
pip-compile-multi
|
||||
pre-commit
|
||||
tox
|
||||
@ -1,60 +0,0 @@
|
||||
# SHA1:54b5b77ec8c7a0064ffa93b2fd16cb0130ba177c
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile-multi
|
||||
#
|
||||
-r docs.txt
|
||||
-r tests.txt
|
||||
-r typing.txt
|
||||
cfgv==3.3.1
|
||||
# via pre-commit
|
||||
click==8.0.3
|
||||
# via
|
||||
# pip-compile-multi
|
||||
# pip-tools
|
||||
distlib==0.3.4
|
||||
# via virtualenv
|
||||
filelock==3.6.0
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
identify==2.4.10
|
||||
# via pre-commit
|
||||
nodeenv==1.6.0
|
||||
# via pre-commit
|
||||
pep517==0.12.0
|
||||
# via pip-tools
|
||||
pip-compile-multi==2.4.3
|
||||
# via -r requirements/dev.in
|
||||
pip-tools==6.5.1
|
||||
# via pip-compile-multi
|
||||
platformdirs==2.5.0
|
||||
# via virtualenv
|
||||
pre-commit==2.17.0
|
||||
# via -r requirements/dev.in
|
||||
pyyaml==6.0
|
||||
# via pre-commit
|
||||
six==1.16.0
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
toml==0.10.2
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
toposort==1.7
|
||||
# via pip-compile-multi
|
||||
tox==3.24.5
|
||||
# via -r requirements/dev.in
|
||||
virtualenv==20.13.1
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
wheel==0.37.1
|
||||
# via pip-tools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
# setuptools
|
||||
@ -1,4 +0,0 @@
|
||||
Pallets-Sphinx-Themes
|
||||
Sphinx
|
||||
sphinx-issues
|
||||
sphinxcontrib-log-cabinet
|
||||
@ -1,65 +0,0 @@
|
||||
# SHA1:45c590f97fe95b8bdc755eef796e91adf5fbe4ea
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile-multi
|
||||
#
|
||||
alabaster==0.7.12
|
||||
# via sphinx
|
||||
babel==2.9.1
|
||||
# via sphinx
|
||||
certifi==2021.10.8
|
||||
# via requests
|
||||
charset-normalizer==2.0.12
|
||||
# via requests
|
||||
docutils==0.17.1
|
||||
# via sphinx
|
||||
idna==3.3
|
||||
# via requests
|
||||
imagesize==1.3.0
|
||||
# via sphinx
|
||||
jinja2==3.0.3
|
||||
# via sphinx
|
||||
markupsafe==2.0.1
|
||||
# via jinja2
|
||||
packaging==21.3
|
||||
# via
|
||||
# pallets-sphinx-themes
|
||||
# sphinx
|
||||
pallets-sphinx-themes==2.0.2
|
||||
# via -r requirements/docs.in
|
||||
pygments==2.11.2
|
||||
# via sphinx
|
||||
pyparsing==3.0.7
|
||||
# via packaging
|
||||
pytz==2021.3
|
||||
# via babel
|
||||
requests==2.27.1
|
||||
# via sphinx
|
||||
snowballstemmer==2.2.0
|
||||
# via sphinx
|
||||
sphinx==4.4.0
|
||||
# via
|
||||
# -r requirements/docs.in
|
||||
# pallets-sphinx-themes
|
||||
# sphinx-issues
|
||||
# sphinxcontrib-log-cabinet
|
||||
sphinx-issues==3.0.1
|
||||
# via -r requirements/docs.in
|
||||
sphinxcontrib-applehelp==1.0.2
|
||||
# via sphinx
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
# via sphinx
|
||||
sphinxcontrib-htmlhelp==2.0.0
|
||||
# via sphinx
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
# via sphinx
|
||||
sphinxcontrib-log-cabinet==1.0.1
|
||||
# via -r requirements/docs.in
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
# via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.5
|
||||
# via sphinx
|
||||
urllib3==1.26.8
|
||||
# via requests
|
||||
@ -1 +0,0 @@
|
||||
pytest
|
||||
@ -1,23 +0,0 @@
|
||||
# SHA1:0eaa389e1fdb3a1917c0f987514bd561be5718ee
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile-multi
|
||||
#
|
||||
attrs==21.4.0
|
||||
# via pytest
|
||||
iniconfig==1.1.1
|
||||
# via pytest
|
||||
packaging==21.3
|
||||
# via pytest
|
||||
pluggy==1.0.0
|
||||
# via pytest
|
||||
py==1.11.0
|
||||
# via pytest
|
||||
pyparsing==3.0.7
|
||||
# via packaging
|
||||
pytest==7.0.1
|
||||
# via -r requirements/tests.in
|
||||
tomli==2.0.1
|
||||
# via pytest
|
||||
@ -1 +0,0 @@
|
||||
mypy
|
||||
@ -1,15 +0,0 @@
|
||||
# SHA1:7983aaa01d64547827c20395d77e248c41b2572f
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile-multi
|
||||
#
|
||||
mypy==0.931
|
||||
# via -r requirements/typing.in
|
||||
mypy-extensions==0.4.3
|
||||
# via mypy
|
||||
tomli==2.0.1
|
||||
# via mypy
|
||||
typing-extensions==4.1.1
|
||||
# via mypy
|
||||
@ -29,9 +29,9 @@ def collapse_ranges(data):
|
||||
|
||||
Source: https://stackoverflow.com/a/4629241/400617
|
||||
"""
|
||||
for _, b in itertools.groupby(enumerate(data), lambda x: ord(x[1]) - x[0]):
|
||||
b = list(b)
|
||||
yield b[0][1], b[-1][1]
|
||||
for _, g in itertools.groupby(enumerate(data), lambda x: ord(x[1]) - x[0]):
|
||||
lb = list(g)
|
||||
yield lb[0][1], lb[-1][1]
|
||||
|
||||
|
||||
def build_pattern(ranges):
|
||||
@ -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")
|
||||
|
||||
109
setup.cfg
109
setup.cfg
@ -1,109 +0,0 @@
|
||||
[metadata]
|
||||
name = Jinja2
|
||||
version = attr: jinja2.__version__
|
||||
url = https://palletsprojects.com/p/jinja/
|
||||
project_urls =
|
||||
Donate = https://palletsprojects.com/donate
|
||||
Documentation = https://jinja.palletsprojects.com/
|
||||
Changes = https://jinja.palletsprojects.com/changes/
|
||||
Source Code = https://github.com/pallets/jinja/
|
||||
Issue Tracker = https://github.com/pallets/jinja/issues/
|
||||
Twitter = https://twitter.com/PalletsTeam
|
||||
Chat = https://discord.gg/pallets
|
||||
license = BSD-3-Clause
|
||||
license_files = LICENSE.rst
|
||||
author = Armin Ronacher
|
||||
author_email = armin.ronacher@active-4.com
|
||||
maintainer = Pallets
|
||||
maintainer_email = contact@palletsprojects.com
|
||||
description = A very fast and expressive template engine.
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
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
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
package_dir = = src
|
||||
include_package_data = True
|
||||
python_requires = >= 3.7
|
||||
# Dependencies are in setup.py for GitHub's dependency graph.
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
||||
|
||||
[options.entry_points]
|
||||
babel.extractors =
|
||||
jinja2 = jinja2.ext:babel_extract[i18n]
|
||||
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
filterwarnings =
|
||||
error
|
||||
# Python 3.9 raises a deprecation from internal asyncio code.
|
||||
ignore:The loop argument:DeprecationWarning:asyncio[.]base_events:542
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
source =
|
||||
jinja2
|
||||
tests
|
||||
|
||||
[coverage:paths]
|
||||
source =
|
||||
src
|
||||
*/site-packages
|
||||
|
||||
[flake8]
|
||||
# B = bugbear
|
||||
# E = pycodestyle errors
|
||||
# F = flake8 pyflakes
|
||||
# W = pycodestyle warnings
|
||||
# B9 = bugbear opinions
|
||||
# ISC = implicit str concat
|
||||
select = B, E, F, W, B9, ISC
|
||||
ignore =
|
||||
# slice notation whitespace, invalid
|
||||
E203
|
||||
# line length, handled by bugbear B950
|
||||
E501
|
||||
# bare except, handled by bugbear B001
|
||||
E722
|
||||
# bin op line break, invalid
|
||||
W503
|
||||
# up to 88 allowed by bugbear B950
|
||||
max-line-length = 80
|
||||
per-file-ignores =
|
||||
# __init__ exports names
|
||||
src/jinja2/__init__.py: F401
|
||||
|
||||
[mypy]
|
||||
files = src/jinja2
|
||||
python_version = 3.7
|
||||
show_error_codes = True
|
||||
disallow_subclassing_any = True
|
||||
disallow_untyped_calls = True
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
no_implicit_optional = True
|
||||
local_partial_types = True
|
||||
no_implicit_reexport = True
|
||||
strict_equality = True
|
||||
warn_redundant_casts = True
|
||||
warn_unused_configs = True
|
||||
warn_unused_ignores = True
|
||||
warn_return_any = True
|
||||
warn_unreachable = True
|
||||
|
||||
[mypy-jinja2.defaults]
|
||||
no_implicit_reexport = False
|
||||
|
||||
[mypy-markupsafe]
|
||||
no_implicit_reexport = False
|
||||
8
setup.py
8
setup.py
@ -1,8 +0,0 @@
|
||||
from setuptools import setup
|
||||
|
||||
# Metadata goes in setup.cfg. These are here for GitHub's dependency graph.
|
||||
setup(
|
||||
name="Jinja2",
|
||||
install_requires=["MarkupSafe>=2.0"],
|
||||
extras_require={"i18n": ["Babel>=2.7"]},
|
||||
)
|
||||
@ -2,6 +2,11 @@
|
||||
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
|
||||
@ -34,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.1.1"
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@ -6,6 +6,9 @@ from functools import wraps
|
||||
from .utils import _PassArg
|
||||
from .utils import pass_eval_context
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
V = t.TypeVar("V")
|
||||
|
||||
|
||||
@ -47,7 +50,7 @@ def async_variant(normal_func): # type: ignore
|
||||
if need_eval_context:
|
||||
wrapper = pass_eval_context(wrapper)
|
||||
|
||||
wrapper.jinja_async_variant = True
|
||||
wrapper.jinja_async_variant = True # type: ignore[attr-defined]
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
@ -64,21 +67,33 @@ 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
|
||||
|
||||
|
||||
async def auto_aiter(
|
||||
iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||
class _IteratorToAsyncIterator(t.Generic[V]):
|
||||
def __init__(self, iterator: "t.Iterator[V]"):
|
||||
self._iterator = iterator
|
||||
|
||||
def __aiter__(self) -> "te.Self":
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> V:
|
||||
try:
|
||||
return next(self._iterator)
|
||||
except StopIteration as e:
|
||||
raise StopAsyncIteration(e.value) from e
|
||||
|
||||
|
||||
def auto_aiter(
|
||||
iterable: "t.AsyncIterable[V] | t.Iterable[V]",
|
||||
) -> "t.AsyncIterator[V]":
|
||||
if hasattr(iterable, "__aiter__"):
|
||||
async for item in t.cast("t.AsyncIterable[V]", iterable):
|
||||
yield item
|
||||
return iterable.__aiter__()
|
||||
else:
|
||||
for item in t.cast("t.Iterable[V]", iterable):
|
||||
yield item
|
||||
return _IteratorToAsyncIterator(iter(iterable))
|
||||
|
||||
|
||||
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)]
|
||||
|
||||
@ -5,6 +5,7 @@ slows down your application too much.
|
||||
Situations where this is useful are often forking web applications that
|
||||
are initialized on the first request.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import fnmatch
|
||||
import marshal
|
||||
@ -20,14 +21,13 @@ from types import CodeType
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
from .environment import Environment
|
||||
|
||||
class _MemcachedClient(te.Protocol):
|
||||
def get(self, key: str) -> bytes:
|
||||
...
|
||||
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
|
||||
@ -58,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."""
|
||||
@ -79,7 +79,7 @@ class Bucket:
|
||||
self.reset()
|
||||
return
|
||||
|
||||
def write_bytecode(self, f: t.BinaryIO) -> None:
|
||||
def write_bytecode(self, f: t.IO[bytes]) -> None:
|
||||
"""Dump the bytecode into the file or file like object passed."""
|
||||
if self.code is None:
|
||||
raise TypeError("can't write empty bucket")
|
||||
@ -147,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"))
|
||||
|
||||
@ -166,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
|
||||
@ -202,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()
|
||||
@ -262,13 +260,55 @@ class FileSystemBytecodeCache(BytecodeCache):
|
||||
def load_bytecode(self, bucket: Bucket) -> None:
|
||||
filename = self._get_cache_filename(bucket)
|
||||
|
||||
if os.path.exists(filename):
|
||||
with open(filename, "rb") as f:
|
||||
bucket.load_bytecode(f)
|
||||
# Don't test for existence before opening the file, since the
|
||||
# file could disappear after the test before the open.
|
||||
try:
|
||||
f = open(filename, "rb")
|
||||
except (FileNotFoundError, IsADirectoryError, PermissionError):
|
||||
# PermissionError can occur on Windows when an operation is
|
||||
# in progress, such as calling clear().
|
||||
return
|
||||
|
||||
with f:
|
||||
bucket.load_bytecode(f)
|
||||
|
||||
def dump_bytecode(self, bucket: Bucket) -> None:
|
||||
with open(self._get_cache_filename(bucket), "wb") as f:
|
||||
bucket.write_bytecode(f)
|
||||
# Write to a temporary file, then rename to the real name after
|
||||
# writing. This avoids another process reading the file before
|
||||
# it is fully written.
|
||||
name = self._get_cache_filename(bucket)
|
||||
f = tempfile.NamedTemporaryFile(
|
||||
mode="wb",
|
||||
dir=os.path.dirname(name),
|
||||
prefix=os.path.basename(name),
|
||||
suffix=".tmp",
|
||||
delete=False,
|
||||
)
|
||||
|
||||
def remove_silent() -> None:
|
||||
try:
|
||||
os.remove(f.name)
|
||||
except OSError:
|
||||
# Another process may have called clear(). On Windows,
|
||||
# another program may be holding the file open.
|
||||
pass
|
||||
|
||||
try:
|
||||
with f:
|
||||
bucket.write_bytecode(f)
|
||||
except BaseException:
|
||||
remove_silent()
|
||||
raise
|
||||
|
||||
try:
|
||||
os.replace(f.name, name)
|
||||
except OSError:
|
||||
# Another process may have called clear(). On Windows,
|
||||
# another program may be holding the file open.
|
||||
remove_silent()
|
||||
except BaseException:
|
||||
remove_silent()
|
||||
raise
|
||||
|
||||
def clear(self) -> None:
|
||||
# imported lazily here because google app-engine doesn't support
|
||||
@ -333,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
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
"""Compiles nodes from the parser into Python code."""
|
||||
|
||||
import typing as t
|
||||
from contextlib import contextmanager
|
||||
from functools import update_wrapper
|
||||
@ -24,6 +25,7 @@ from .visitor import NodeVisitor
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
from .environment import Environment
|
||||
|
||||
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||
@ -53,15 +55,14 @@ def optimizeconst(f: F) -> F:
|
||||
|
||||
return f(self, node, frame, **kwargs)
|
||||
|
||||
return update_wrapper(t.cast(F, new_func), f)
|
||||
return update_wrapper(new_func, f) # type: ignore[return-value]
|
||||
|
||||
|
||||
def _make_binop(op: str) -> t.Callable[["CodeGenerator", nodes.BinExpr, "Frame"], None]:
|
||||
@optimizeconst
|
||||
def visitor(self: "CodeGenerator", node: nodes.BinExpr, frame: Frame) -> None:
|
||||
if (
|
||||
self.environment.sandboxed
|
||||
and op in self.environment.intercepted_binops # type: ignore
|
||||
self.environment.sandboxed and op in self.environment.intercepted_binops # type: ignore
|
||||
):
|
||||
self.write(f"environment.call_binop(context, {op!r}, ")
|
||||
self.visit(node.left, frame)
|
||||
@ -84,8 +85,7 @@ def _make_unop(
|
||||
@optimizeconst
|
||||
def visitor(self: "CodeGenerator", node: nodes.UnaryExpr, frame: Frame) -> None:
|
||||
if (
|
||||
self.environment.sandboxed
|
||||
and op in self.environment.intercepted_unops # type: ignore
|
||||
self.environment.sandboxed and op in self.environment.intercepted_unops # type: ignore
|
||||
):
|
||||
self.write(f"environment.call_unop(context, {op!r}, ")
|
||||
self.visit(node.node, frame)
|
||||
@ -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")
|
||||
@ -133,15 +133,13 @@ def has_safe_repr(value: t.Any) -> bool:
|
||||
if type(value) in {tuple, list, set, frozenset}:
|
||||
return all(has_safe_repr(v) for v in value)
|
||||
|
||||
if type(value) is dict:
|
||||
if type(value) is dict: # noqa E721
|
||||
return all(has_safe_repr(k) and has_safe_repr(v) for k, v in value.items())
|
||||
|
||||
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
|
||||
@ -551,10 +547,13 @@ class CodeGenerator(NodeVisitor):
|
||||
for node in nodes:
|
||||
visitor.visit(node)
|
||||
|
||||
for id_map, names, dependency in (self.filters, visitor.filters, "filters"), (
|
||||
self.tests,
|
||||
visitor.tests,
|
||||
"tests",
|
||||
for id_map, names, dependency in (
|
||||
(self.filters, visitor.filters, "filters"),
|
||||
(
|
||||
self.tests,
|
||||
visitor.tests,
|
||||
"tests",
|
||||
),
|
||||
):
|
||||
for name in sorted(names):
|
||||
if name not in id_map:
|
||||
@ -609,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)
|
||||
@ -808,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)
|
||||
@ -818,18 +817,17 @@ 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)
|
||||
|
||||
from .runtime import exported, async_exported
|
||||
from .runtime import async_exported
|
||||
from .runtime import exported
|
||||
|
||||
if self.environment.is_async:
|
||||
exported_names = sorted(exported + async_exported)
|
||||
@ -898,12 +896,15 @@ class CodeGenerator(NodeVisitor):
|
||||
if not self.environment.is_async:
|
||||
self.writeline("yield from parent_template.root_render_func(context)")
|
||||
else:
|
||||
self.writeline(
|
||||
"async for event in parent_template.root_render_func(context):"
|
||||
)
|
||||
self.writeline("agen = parent_template.root_render_func(context)")
|
||||
self.writeline("try:")
|
||||
self.indent()
|
||||
self.writeline("async for event in agen:")
|
||||
self.indent()
|
||||
self.writeline("yield event")
|
||||
self.outdent()
|
||||
self.outdent()
|
||||
self.writeline("finally: await agen.aclose()")
|
||||
self.outdent(1 + (not self.has_known_extends))
|
||||
|
||||
# at this point we now have the blocks collected and can visit them too.
|
||||
@ -973,14 +974,20 @@ class CodeGenerator(NodeVisitor):
|
||||
f"yield from context.blocks[{node.name!r}][0]({context})", node
|
||||
)
|
||||
else:
|
||||
self.writeline(f"gen = context.blocks[{node.name!r}][0]({context})")
|
||||
self.writeline("try:")
|
||||
self.indent()
|
||||
self.writeline(
|
||||
f"{self.choose_async()}for event in"
|
||||
f" context.blocks[{node.name!r}][0]({context}):",
|
||||
f"{self.choose_async()}for event in gen:",
|
||||
node,
|
||||
)
|
||||
self.indent()
|
||||
self.simple_write("event", frame)
|
||||
self.outdent()
|
||||
self.outdent()
|
||||
self.writeline(
|
||||
f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}"
|
||||
)
|
||||
|
||||
self.outdent(level)
|
||||
|
||||
@ -993,7 +1000,6 @@ class CodeGenerator(NodeVisitor):
|
||||
# far, we don't have to add a check if something extended
|
||||
# the template before this one.
|
||||
if self.extends_so_far > 0:
|
||||
|
||||
# if we have a known extends we just add a template runtime
|
||||
# error into the generated code. We could catch that at compile
|
||||
# time too, but i welcome it not to confuse users by throwing the
|
||||
@ -1054,32 +1060,39 @@ class CodeGenerator(NodeVisitor):
|
||||
self.writeline("else:")
|
||||
self.indent()
|
||||
|
||||
skip_event_yield = False
|
||||
def loop_body() -> None:
|
||||
self.indent()
|
||||
self.simple_write("event", frame)
|
||||
self.outdent()
|
||||
|
||||
if node.with_context:
|
||||
self.writeline(
|
||||
f"{self.choose_async()}for event in template.root_render_func("
|
||||
f"gen = template.root_render_func("
|
||||
"template.new_context(context.get_all(), True,"
|
||||
f" {self.dump_local_context(frame)})):"
|
||||
f" {self.dump_local_context(frame)}))"
|
||||
)
|
||||
self.writeline("try:")
|
||||
self.indent()
|
||||
self.writeline(f"{self.choose_async()}for event in gen:")
|
||||
loop_body()
|
||||
self.outdent()
|
||||
self.writeline(
|
||||
f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}"
|
||||
)
|
||||
elif self.environment.is_async:
|
||||
self.writeline(
|
||||
"for event in (await template._get_default_module_async())"
|
||||
"._body_stream:"
|
||||
)
|
||||
loop_body()
|
||||
else:
|
||||
self.writeline("yield from template._get_default_module()._body_stream")
|
||||
skip_event_yield = True
|
||||
|
||||
if not skip_event_yield:
|
||||
self.indent()
|
||||
self.simple_write("event", frame)
|
||||
self.outdent()
|
||||
|
||||
if node.ignore_missing:
|
||||
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)
|
||||
@ -1122,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(
|
||||
@ -1347,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(" = ")
|
||||
@ -1360,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:
|
||||
@ -1371,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
|
||||
@ -1389,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
|
||||
|
||||
@ -1407,7 +1425,7 @@ class CodeGenerator(NodeVisitor):
|
||||
|
||||
if pass_arg is None:
|
||||
|
||||
def finalize(value: t.Any) -> t.Any:
|
||||
def finalize(value: t.Any) -> t.Any: # noqa: F811
|
||||
return default(env_finalize(value))
|
||||
|
||||
else:
|
||||
@ -1415,7 +1433,7 @@ class CodeGenerator(NodeVisitor):
|
||||
|
||||
if pass_arg == "environment":
|
||||
|
||||
def finalize(value: t.Any) -> t.Any:
|
||||
def finalize(value: t.Any) -> t.Any: # noqa: F811
|
||||
return default(env_finalize(self.environment, value))
|
||||
|
||||
self._finalize = self._FinalizeInfo(finalize, src)
|
||||
@ -1487,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
|
||||
@ -1557,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(" = ")
|
||||
@ -1613,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:
|
||||
@ -1752,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
|
||||
@ -118,26 +118,7 @@ def fake_traceback( # type: ignore
|
||||
elif function.startswith("block_"):
|
||||
location = f"block {function[6:]!r}"
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
code = code.replace(co_name=location)
|
||||
else:
|
||||
code = CodeType(
|
||||
code.co_argcount,
|
||||
code.co_kwonlyargcount,
|
||||
code.co_nlocals,
|
||||
code.co_stacksize,
|
||||
code.co_flags,
|
||||
code.co_code,
|
||||
code.co_consts,
|
||||
code.co_names,
|
||||
code.co_varnames,
|
||||
code.co_filename,
|
||||
location,
|
||||
code.co_firstlineno,
|
||||
code.co_lnotab,
|
||||
code.co_freevars,
|
||||
code.co_cellvars,
|
||||
)
|
||||
code = code.replace(co_name=location)
|
||||
|
||||
# Execute the new code, which is guaranteed to raise, and return
|
||||
# the new traceback without this frame.
|
||||
@ -147,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 = {}
|
||||
|
||||
@ -163,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,
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
"""Classes for managing templates and their runtime and compile time
|
||||
options.
|
||||
"""
|
||||
|
||||
import os
|
||||
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
|
||||
@ -20,10 +22,10 @@ from .defaults import BLOCK_END_STRING
|
||||
from .defaults import BLOCK_START_STRING
|
||||
from .defaults import COMMENT_END_STRING
|
||||
from .defaults import COMMENT_START_STRING
|
||||
from .defaults import DEFAULT_FILTERS
|
||||
from .defaults import DEFAULT_FILTERS # type: ignore[attr-defined]
|
||||
from .defaults import DEFAULT_NAMESPACE
|
||||
from .defaults import DEFAULT_POLICIES
|
||||
from .defaults import DEFAULT_TESTS
|
||||
from .defaults import DEFAULT_TESTS # type: ignore[attr-defined]
|
||||
from .defaults import KEEP_TRAILING_NEWLINE
|
||||
from .defaults import LINE_COMMENT_PREFIX
|
||||
from .defaults import LINE_STATEMENT_PREFIX
|
||||
@ -55,6 +57,7 @@ from .utils import missing
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
from .bccache import BytecodeCache
|
||||
from .ext import Extension
|
||||
from .loaders import BaseLoader
|
||||
@ -64,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.
|
||||
@ -79,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, str], "Template"]]:
|
||||
) -> t.MutableMapping[tuple["weakref.ref[BaseLoader]", str], "Template"] | None:
|
||||
"""Return the cache class for the given size."""
|
||||
if size == 0:
|
||||
return None
|
||||
@ -91,13 +94,13 @@ def create_cache(
|
||||
|
||||
|
||||
def copy_cache(
|
||||
cache: t.Optional[t.MutableMapping],
|
||||
) -> t.Optional[t.MutableMapping[t.Tuple[weakref.ref, 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
|
||||
|
||||
if type(cache) is dict:
|
||||
if type(cache) is dict: # noqa E721
|
||||
return {}
|
||||
|
||||
return LRUCache(cache.capacity) # type: ignore
|
||||
@ -105,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.
|
||||
"""
|
||||
@ -114,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
|
||||
@ -279,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,
|
||||
@ -297,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,
|
||||
@ -340,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
|
||||
@ -365,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
|
||||
@ -389,20 +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,
|
||||
extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = missing,
|
||||
newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = missing,
|
||||
keep_trailing_newline: bool = 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,
|
||||
) -> "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
|
||||
@ -413,9 +419,16 @@ class Environment:
|
||||
up completely. Not all attributes are truly linked, some are just
|
||||
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``,
|
||||
and ``enable_async`` parameters to match ``__init__``.
|
||||
"""
|
||||
args = dict(locals())
|
||||
del args["self"], args["cache_size"], args["extensions"]
|
||||
del args["self"], args["cache_size"], args["extensions"], args["enable_async"]
|
||||
|
||||
rv = object.__new__(self.__class__)
|
||||
rv.__dict__.update(self.__dict__)
|
||||
@ -437,6 +450,9 @@ class Environment:
|
||||
if extensions is not missing:
|
||||
rv.extensions.update(load_extensions(rv, extensions))
|
||||
|
||||
if enable_async is not missing:
|
||||
rv.is_async = enable_async
|
||||
|
||||
return _environment_config_check(rv)
|
||||
|
||||
@property
|
||||
@ -448,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]
|
||||
@ -482,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:
|
||||
@ -538,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.
|
||||
|
||||
@ -559,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.
|
||||
|
||||
@ -584,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
|
||||
@ -601,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()
|
||||
@ -609,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>`
|
||||
@ -630,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`
|
||||
@ -646,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`.
|
||||
@ -660,15 +674,15 @@ class Environment:
|
||||
stream = ext.filter_stream(stream) # type: ignore
|
||||
|
||||
if not isinstance(stream, TokenStream):
|
||||
stream = TokenStream(stream, name, filename) # type: ignore
|
||||
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
|
||||
@ -691,39 +705,37 @@ class Environment:
|
||||
|
||||
.. versionadded:: 2.5
|
||||
"""
|
||||
return compile(source, filename, "exec") # type: ignore
|
||||
|
||||
@typing.overload
|
||||
def compile( # type: ignore
|
||||
self,
|
||||
source: t.Union[str, nodes.Template],
|
||||
name: t.Optional[str] = None,
|
||||
filename: t.Optional[str] = None,
|
||||
raw: "te.Literal[False]" = False,
|
||||
defer_init: bool = False,
|
||||
) -> CodeType:
|
||||
...
|
||||
return compile(source, filename, "exec")
|
||||
|
||||
@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[False]" = False,
|
||||
defer_init: bool = False,
|
||||
) -> CodeType: ...
|
||||
|
||||
@typing.overload
|
||||
def compile(
|
||||
self,
|
||||
source: str | nodes.Template,
|
||||
name: str | None = None,
|
||||
filename: str | None = None,
|
||||
raw: "te.Literal[True]" = ...,
|
||||
defer_init: bool = False,
|
||||
) -> str:
|
||||
...
|
||||
) -> str: ...
|
||||
|
||||
@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.
|
||||
@ -804,11 +816,11 @@ class Environment:
|
||||
|
||||
def compile_templates(
|
||||
self,
|
||||
target: t.Union[str, os.PathLike],
|
||||
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,
|
||||
target: t.Union[str, "os.PathLike[str]"],
|
||||
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
|
||||
@ -848,7 +860,10 @@ class Environment:
|
||||
f.write(data.encode("utf8"))
|
||||
|
||||
if zip is not None:
|
||||
from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED, ZIP_STORED
|
||||
from zipfile import ZIP_DEFLATED
|
||||
from zipfile import ZIP_STORED
|
||||
from zipfile import ZipFile
|
||||
from zipfile import ZipInfo
|
||||
|
||||
zip_file = ZipFile(
|
||||
target, "w", dict(deflated=ZIP_DEFLATED, stored=ZIP_STORED)[zip]
|
||||
@ -882,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.
|
||||
@ -910,14 +925,14 @@ class Environment:
|
||||
)
|
||||
|
||||
def filter_func(x: str) -> bool:
|
||||
return "." in x and x.rsplit(".", 1)[1] in extensions # type: ignore
|
||||
return "." in x and x.rsplit(".", 1)[1] in extensions
|
||||
|
||||
if filter_func is not None:
|
||||
names = [name for name in names if filter_func(name)]
|
||||
|
||||
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.
|
||||
"""
|
||||
@ -939,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")
|
||||
@ -966,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
|
||||
@ -1003,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`
|
||||
@ -1056,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.
|
||||
@ -1075,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`.
|
||||
@ -1095,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`.
|
||||
@ -1136,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(
|
||||
@ -1203,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.
|
||||
@ -1235,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"]
|
||||
@ -1243,7 +1256,7 @@ class Template:
|
||||
t.blocks = namespace["blocks"]
|
||||
|
||||
# render function and module
|
||||
t.root_render_func = namespace["root"] # type: ignore
|
||||
t.root_render_func = namespace["root"]
|
||||
t._module = None
|
||||
|
||||
# debug and loader helpers
|
||||
@ -1269,19 +1282,7 @@ class Template:
|
||||
if self.environment.is_async:
|
||||
import asyncio
|
||||
|
||||
close = False
|
||||
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
close = True
|
||||
|
||||
try:
|
||||
return loop.run_until_complete(self.render_async(*args, **kwargs))
|
||||
finally:
|
||||
if close:
|
||||
loop.close()
|
||||
return asyncio.run(self.render_async(*args, **kwargs))
|
||||
|
||||
ctx = self.new_context(dict(*args, **kwargs))
|
||||
|
||||
@ -1330,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())
|
||||
@ -1339,13 +1340,13 @@ class Template:
|
||||
ctx = self.new_context(dict(*args, **kwargs))
|
||||
|
||||
try:
|
||||
yield from self.root_render_func(ctx) # type: ignore
|
||||
yield from self.root_render_func(ctx)
|
||||
except Exception:
|
||||
yield self.environment.handle_exception()
|
||||
|
||||
async def generate_async(
|
||||
self, *args: t.Any, **kwargs: t.Any
|
||||
) -> t.AsyncIterator[str]:
|
||||
) -> t.AsyncGenerator[str, object]:
|
||||
"""An async version of :meth:`generate`. Works very similarly but
|
||||
returns an async iterator instead.
|
||||
"""
|
||||
@ -1357,16 +1358,19 @@ class Template:
|
||||
ctx = self.new_context(dict(*args, **kwargs))
|
||||
|
||||
try:
|
||||
async for event in self.root_render_func(ctx): # type: ignore
|
||||
yield event
|
||||
agen: t.AsyncGenerator[str, None] = self.root_render_func(ctx) # type: ignore[assignment]
|
||||
|
||||
async with aclosing(agen):
|
||||
async for event in agen:
|
||||
yield event
|
||||
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
|
||||
@ -1381,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
|
||||
@ -1396,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
|
||||
@ -1407,11 +1411,13 @@ class Template:
|
||||
"""
|
||||
ctx = self.new_context(vars, shared, locals)
|
||||
return TemplateModule(
|
||||
self, ctx, [x async for x in self.root_render_func(ctx)] # type: ignore
|
||||
self,
|
||||
ctx,
|
||||
[x async for x in self.root_render_func(ctx)], # type: ignore
|
||||
)
|
||||
|
||||
@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
|
||||
@ -1438,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()
|
||||
@ -1484,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 [
|
||||
@ -1512,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:
|
||||
@ -1522,7 +1528,7 @@ class TemplateModule:
|
||||
" API you are using."
|
||||
)
|
||||
|
||||
body_stream = list(template.root_render_func(context)) # type: ignore
|
||||
body_stream = list(template.root_render_func(context))
|
||||
|
||||
self._body_stream = body_stream
|
||||
self.__dict__.update(context.get_exported())
|
||||
@ -1552,9 +1558,9 @@ 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)) # type: ignore
|
||||
consume(self._template.root_render_func(context))
|
||||
rv = context.vars["result"]
|
||||
if self._undefined_to_none and isinstance(rv, Undefined):
|
||||
rv = None
|
||||
@ -1578,9 +1584,9 @@ class TemplateStream:
|
||||
|
||||
def dump(
|
||||
self,
|
||||
fp: t.Union[str, t.IO],
|
||||
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
|
||||
@ -1596,22 +1602,25 @@ class TemplateStream:
|
||||
if encoding is None:
|
||||
encoding = "utf-8"
|
||||
|
||||
fp = open(fp, "wb")
|
||||
real_fp: t.IO[bytes] = open(fp, "wb")
|
||||
close = True
|
||||
else:
|
||||
real_fp = fp
|
||||
|
||||
try:
|
||||
if encoding is not None:
|
||||
iterable = (x.encode(encoding, errors) for x in self) # type: ignore
|
||||
else:
|
||||
iterable = self # type: ignore
|
||||
|
||||
if hasattr(fp, "writelines"):
|
||||
fp.writelines(iterable)
|
||||
if hasattr(real_fp, "writelines"):
|
||||
real_fp.writelines(iterable)
|
||||
else:
|
||||
for item in iterable:
|
||||
fp.write(item)
|
||||
real_fp.write(item)
|
||||
finally:
|
||||
if close:
|
||||
fp.close()
|
||||
real_fp.close()
|
||||
|
||||
def disable_buffering(self) -> None:
|
||||
"""Disable the output buffering."""
|
||||
@ -1619,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
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
"""Extension API for adding custom tags and behavior."""
|
||||
|
||||
import pprint
|
||||
import re
|
||||
import typing as t
|
||||
@ -18,30 +19,30 @@ from .utils import pass_context
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
from .lexer import Token
|
||||
from .lexer import TokenStream
|
||||
from .parser import Parser
|
||||
|
||||
class _TranslationsBasic(te.Protocol):
|
||||
def gettext(self, message: str) -> str:
|
||||
...
|
||||
def gettext(self, message: str) -> str: ...
|
||||
|
||||
def ngettext(self, singular: str, plural: str, n: int) -> str:
|
||||
pass
|
||||
|
||||
class _TranslationsContext(_TranslationsBasic):
|
||||
def pgettext(self, context: str, message: str) -> str:
|
||||
...
|
||||
def pgettext(self, context: str, message: str) -> str: ...
|
||||
|
||||
def npgettext(self, context: str, singular: str, plural: str, n: int) -> str:
|
||||
...
|
||||
def npgettext(
|
||||
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",
|
||||
@ -76,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
|
||||
@ -88,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__)
|
||||
@ -96,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
|
||||
@ -114,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
|
||||
@ -122,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.
|
||||
|
||||
@ -137,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`.
|
||||
@ -163,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)
|
||||
|
||||
|
||||
@ -218,7 +217,7 @@ def _make_new_pgettext(func: t.Callable[[str, str], str]) -> t.Callable[..., str
|
||||
|
||||
|
||||
def _make_new_npgettext(
|
||||
func: t.Callable[[str, str, str, int], str]
|
||||
func: t.Callable[[str, str, str, int], str],
|
||||
) -> t.Callable[..., str]:
|
||||
@pass_context
|
||||
def npgettext(
|
||||
@ -267,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.
|
||||
@ -284,41 +283,25 @@ 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()
|
||||
|
||||
if hasattr(translations, "pgettext"):
|
||||
# Python < 3.8
|
||||
pgettext = translations.pgettext # type: ignore
|
||||
else:
|
||||
|
||||
def pgettext(c: str, s: str) -> str:
|
||||
return s
|
||||
|
||||
if hasattr(translations, "npgettext"):
|
||||
npgettext = translations.npgettext # type: ignore
|
||||
else:
|
||||
|
||||
def npgettext(c: str, s: str, p: str, n: int) -> str:
|
||||
return s if n == 1 else p
|
||||
|
||||
self._install_callables(
|
||||
gettext=translations.gettext,
|
||||
ngettext=translations.ngettext,
|
||||
newstyle=newstyle,
|
||||
pgettext=pgettext,
|
||||
npgettext=npgettext,
|
||||
pgettext=translations.pgettext,
|
||||
npgettext=translations.npgettext,
|
||||
)
|
||||
|
||||
def _install_callables(
|
||||
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
|
||||
@ -342,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
|
||||
|
||||
@ -364,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:
|
||||
@ -478,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 = []
|
||||
@ -495,16 +476,26 @@ class InternationalizationExtension(Extension):
|
||||
parser.stream.expect("variable_end")
|
||||
elif parser.stream.current.type == "block_begin":
|
||||
next(parser.stream)
|
||||
if parser.stream.current.test("name:endtrans"):
|
||||
block_name = (
|
||||
parser.stream.current.value
|
||||
if parser.stream.current.type == "name"
|
||||
else None
|
||||
)
|
||||
if block_name == "endtrans":
|
||||
break
|
||||
elif parser.stream.current.test("name:pluralize"):
|
||||
elif block_name == "pluralize":
|
||||
if allow_pluralize:
|
||||
break
|
||||
parser.fail(
|
||||
"a translatable section can have only one pluralize section"
|
||||
)
|
||||
elif block_name == "trans":
|
||||
parser.fail(
|
||||
"trans blocks can't be nested; did you mean `endtrans`?"
|
||||
)
|
||||
parser.fail(
|
||||
"control structures in translatable sections are not allowed"
|
||||
f"control structures in translatable sections are not allowed; "
|
||||
f"saw `{block_name}`"
|
||||
)
|
||||
elif parser.stream.eos:
|
||||
parser.fail("unclosed translation block")
|
||||
@ -516,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:
|
||||
@ -535,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))
|
||||
@ -594,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)
|
||||
@ -645,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
|
||||
@ -682,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 (
|
||||
@ -691,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):
|
||||
@ -728,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]
|
||||
@ -751,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 :]):
|
||||
@ -764,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
|
||||
@ -797,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()
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
"""Built-in template filters used with the ``|`` operator."""
|
||||
|
||||
import math
|
||||
import random
|
||||
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
|
||||
|
||||
@ -28,6 +30,7 @@ from .utils import urlize
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
from .environment import Environment
|
||||
from .nodes import EvalContext
|
||||
from .runtime import Context
|
||||
@ -54,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
|
||||
@ -82,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
|
||||
@ -96,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):
|
||||
@ -122,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 []
|
||||
|
||||
@ -133,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__()
|
||||
@ -142,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.
|
||||
|
||||
@ -163,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
|
||||
|
||||
@ -174,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
|
||||
@ -218,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
|
||||
@ -248,13 +251,25 @@ def do_items(value: t.Union[t.Mapping[K, V], Undefined]) -> t.Iterator[t.Tuple[K
|
||||
yield from value.items()
|
||||
|
||||
|
||||
# Check for characters that would move the parser state from key to value.
|
||||
# https://html.spec.whatwg.org/#attribute-name-state
|
||||
_attr_key_re = re.compile(r"[\s/>=]", flags=re.ASCII)
|
||||
|
||||
|
||||
@pass_eval_context
|
||||
def do_xmlattr(
|
||||
eval_ctx: "EvalContext", d: t.Mapping[str, t.Any], autospace: bool = True
|
||||
) -> str:
|
||||
"""Create an SGML/XML attribute string based on the items in a dict.
|
||||
All values that are neither `none` nor `undefined` are automatically
|
||||
escaped:
|
||||
|
||||
**Values** that are neither ``none`` nor ``undefined`` are automatically
|
||||
escaped, safely allowing untrusted user input.
|
||||
|
||||
User input should not be used as **keys** to this filter. If any key
|
||||
contains a space, ``/`` solidus, ``>`` greater-than sign, or ``=`` equals
|
||||
sign, this fails with a ``ValueError``. Regardless of this, user input
|
||||
should never be used as keys to this filter, or must be separately validated
|
||||
first.
|
||||
|
||||
.. sourcecode:: html+jinja
|
||||
|
||||
@ -273,12 +288,26 @@ def do_xmlattr(
|
||||
|
||||
As you can see it automatically prepends a space in front of the item
|
||||
if the filter returned something unless the second parameter is false.
|
||||
|
||||
.. versionchanged:: 3.1.4
|
||||
Keys with ``/`` solidus, ``>`` greater-than sign, or ``=`` equals sign
|
||||
are not allowed.
|
||||
|
||||
.. versionchanged:: 3.1.3
|
||||
Keys with spaces are not allowed.
|
||||
"""
|
||||
rv = " ".join(
|
||||
f'{escape(key)}="{escape(value)}"'
|
||||
for key, value in d.items()
|
||||
if value is not None and not isinstance(value, Undefined)
|
||||
)
|
||||
items = []
|
||||
|
||||
for key, value in d.items():
|
||||
if value is None or isinstance(value, Undefined):
|
||||
continue
|
||||
|
||||
if _attr_key_re.search(key) is not None:
|
||||
raise ValueError(f"Invalid character in attribute name: {key!r}")
|
||||
|
||||
items.append(f'{escape(key)}="{escape(value)}"')
|
||||
|
||||
rv = " ".join(items)
|
||||
|
||||
if autospace and rv:
|
||||
rv = " " + rv
|
||||
@ -317,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.
|
||||
|
||||
@ -342,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:
|
||||
@ -359,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
|
||||
@ -410,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.
|
||||
|
||||
@ -442,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:
|
||||
@ -467,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
|
||||
@ -487,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
|
||||
@ -538,9 +579,9 @@ def do_default(
|
||||
@pass_eval_context
|
||||
def sync_do_join(
|
||||
eval_ctx: "EvalContext",
|
||||
value: t.Iterable,
|
||||
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
|
||||
@ -596,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.Iterable],
|
||||
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)
|
||||
|
||||
@ -609,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))
|
||||
@ -621,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:
|
||||
@ -630,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
|
||||
@ -652,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)
|
||||
@ -660,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
|
||||
@ -705,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.
|
||||
|
||||
@ -782,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.
|
||||
@ -836,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
|
||||
@ -883,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
|
||||
@ -959,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
|
||||
|
||||
|
||||
@ -1002,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__()
|
||||
@ -1016,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:
|
||||
@ -1060,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
|
||||
@ -1088,7 +1125,7 @@ def do_batch(
|
||||
{%- endfor %}
|
||||
</table>
|
||||
"""
|
||||
tmp: "t.List[V]" = []
|
||||
tmp: list[V] = []
|
||||
|
||||
for item in value:
|
||||
if len(tmp) == linecount:
|
||||
@ -1146,7 +1183,7 @@ def do_round(
|
||||
|
||||
class _GroupTuple(t.NamedTuple):
|
||||
grouper: t.Any
|
||||
list: t.List
|
||||
list: list[t.Any]
|
||||
|
||||
# Use the regular tuple repr to hide this subclass if users print
|
||||
# out the value during debugging.
|
||||
@ -1161,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``,
|
||||
@ -1244,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,
|
||||
@ -1272,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
|
||||
@ -1286,20 +1323,20 @@ def sync_do_sum(
|
||||
Total: {{ items|sum(attribute='price') }}
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
The `attribute` parameter was added to allow suming up over
|
||||
attributes. Also the `start` parameter was moved on to the right.
|
||||
The ``attribute`` parameter was added to allow summing up over
|
||||
attributes. Also the ``start`` parameter was moved on to the right.
|
||||
"""
|
||||
if attribute is not None:
|
||||
iterable = map(make_attrgetter(environment, attribute), iterable)
|
||||
|
||||
return sum(iterable, start)
|
||||
return sum(iterable, start) # type: ignore[no-any-return, call-overload]
|
||||
|
||||
|
||||
@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
|
||||
@ -1317,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.
|
||||
"""
|
||||
@ -1325,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)
|
||||
|
||||
|
||||
@ -1342,16 +1379,14 @@ def do_mark_unsafe(value: str) -> str:
|
||||
|
||||
|
||||
@typing.overload
|
||||
def do_reverse(value: str) -> str:
|
||||
...
|
||||
def do_reverse(value: str) -> str: ...
|
||||
|
||||
|
||||
@typing.overload
|
||||
def do_reverse(value: "t.Iterable[V]") -> "t.Iterable[V]":
|
||||
...
|
||||
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.
|
||||
"""
|
||||
@ -1370,58 +1405,52 @@ 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)
|
||||
|
||||
|
||||
@typing.overload
|
||||
def sync_do_map(
|
||||
context: "Context", value: t.Iterable, name: str, *args: t.Any, **kwargs: t.Any
|
||||
) -> t.Iterable:
|
||||
...
|
||||
return environment.getattr(obj, name)
|
||||
|
||||
|
||||
@typing.overload
|
||||
def sync_do_map(
|
||||
context: "Context",
|
||||
value: t.Iterable,
|
||||
value: t.Iterable[t.Any],
|
||||
name: str,
|
||||
*args: t.Any,
|
||||
**kwargs: t.Any,
|
||||
) -> t.Iterable[t.Any]: ...
|
||||
|
||||
|
||||
@typing.overload
|
||||
def sync_do_map(
|
||||
context: "Context",
|
||||
value: t.Iterable[t.Any],
|
||||
*,
|
||||
attribute: str = ...,
|
||||
default: t.Optional[t.Any] = None,
|
||||
) -> t.Iterable:
|
||||
...
|
||||
default: t.Any | None = None,
|
||||
) -> t.Iterable[t.Any]: ...
|
||||
|
||||
|
||||
@pass_context
|
||||
def sync_do_map(
|
||||
context: "Context", value: t.Iterable, *args: t.Any, **kwargs: t.Any
|
||||
) -> t.Iterable:
|
||||
context: "Context", value: t.Iterable[t.Any], *args: t.Any, **kwargs: t.Any
|
||||
) -> t.Iterable[t.Any]:
|
||||
"""Applies a filter on a sequence of objects or looks up an attribute.
|
||||
This is useful when dealing with lists of objects but you are really
|
||||
only interested in a certain value of it.
|
||||
@ -1471,32 +1500,30 @@ def sync_do_map(
|
||||
@typing.overload
|
||||
def do_map(
|
||||
context: "Context",
|
||||
value: t.Union[t.AsyncIterable, t.Iterable],
|
||||
value: t.AsyncIterable[t.Any] | t.Iterable[t.Any],
|
||||
name: str,
|
||||
*args: t.Any,
|
||||
**kwargs: t.Any,
|
||||
) -> t.Iterable:
|
||||
...
|
||||
) -> t.Iterable[t.Any]: ...
|
||||
|
||||
|
||||
@typing.overload
|
||||
def do_map(
|
||||
context: "Context",
|
||||
value: t.Union[t.AsyncIterable, t.Iterable],
|
||||
value: t.AsyncIterable[t.Any] | t.Iterable[t.Any],
|
||||
*,
|
||||
attribute: str = ...,
|
||||
default: t.Optional[t.Any] = None,
|
||||
) -> t.Iterable:
|
||||
...
|
||||
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.Iterable],
|
||||
value: t.AsyncIterable[t.Any] | t.Iterable[t.Any],
|
||||
*args: t.Any,
|
||||
**kwargs: t.Any,
|
||||
) -> t.AsyncIterable:
|
||||
) -> t.AsyncIterable[t.Any]:
|
||||
if value:
|
||||
func = prepare_map(context, args, kwargs)
|
||||
|
||||
@ -1538,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]":
|
||||
@ -1574,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]":
|
||||
@ -1603,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
|
||||
"""
|
||||
@ -1614,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]":
|
||||
@ -1641,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
|
||||
"""
|
||||
@ -1652,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]":
|
||||
@ -1661,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.
|
||||
@ -1689,7 +1716,7 @@ def do_tojson(
|
||||
|
||||
|
||||
def prepare_map(
|
||||
context: "Context", args: t.Tuple, 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")
|
||||
@ -1718,8 +1745,8 @@ def prepare_map(
|
||||
|
||||
def prepare_select_or_reject(
|
||||
context: "Context",
|
||||
args: t.Tuple,
|
||||
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]:
|
||||
@ -1742,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
|
||||
@ -1753,8 +1780,8 @@ def prepare_select_or_reject(
|
||||
def select_or_reject(
|
||||
context: "Context",
|
||||
value: "t.Iterable[V]",
|
||||
args: t.Tuple,
|
||||
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]":
|
||||
@ -1768,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,
|
||||
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():
|
||||
|
||||
@ -3,6 +3,7 @@ is used to do some preprocessing. It filters out invalid operators like
|
||||
the bitshift operators we don't allow in templates. It separates
|
||||
template code and python code in expressions.
|
||||
"""
|
||||
|
||||
import re
|
||||
import typing as t
|
||||
from ast import literal_eval
|
||||
@ -15,11 +16,12 @@ from .utils import LRUCache
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
from .environment import Environment
|
||||
|
||||
# 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+")
|
||||
@ -208,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 = [
|
||||
@ -255,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)
|
||||
|
||||
|
||||
@ -323,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
|
||||
@ -362,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`.
|
||||
"""
|
||||
@ -447,7 +449,7 @@ def get_lexer(environment: "Environment") -> "Lexer":
|
||||
return lexer
|
||||
|
||||
|
||||
class OptionalLStrip(tuple):
|
||||
class OptionalLStrip(tuple): # type: ignore[type-arg]
|
||||
"""A special tuple for marking a point in the state that can have
|
||||
lstrip applied.
|
||||
"""
|
||||
@ -462,8 +464,8 @@ class OptionalLStrip(tuple):
|
||||
|
||||
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:
|
||||
@ -482,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),
|
||||
@ -521,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(
|
||||
@ -602,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)
|
||||
@ -612,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.
|
||||
@ -667,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.
|
||||
|
||||
@ -694,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
|
||||
|
||||
@ -755,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
|
||||
@ -776,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
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""API and implementations for loading templates from different data
|
||||
sources.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import posixpath
|
||||
@ -15,21 +16,20 @@ from types import ModuleType
|
||||
|
||||
from .exceptions import TemplateNotFound
|
||||
from .utils import internalcode
|
||||
from .utils import open_if_exists
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from .environment import Environment
|
||||
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.
|
||||
"""
|
||||
pieces = []
|
||||
for piece in template.split("/"):
|
||||
if (
|
||||
os.path.sep in piece
|
||||
os.sep in piece
|
||||
or (os.path.altsep and os.path.altsep in piece)
|
||||
or piece == os.path.pardir
|
||||
):
|
||||
@ -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
|
||||
@ -178,7 +178,9 @@ class FileSystemLoader(BaseLoader):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
searchpath: t.Union[str, os.PathLike, t.Sequence[t.Union[str, os.PathLike]]],
|
||||
searchpath: t.Union[
|
||||
str, "os.PathLike[str]", t.Sequence[t.Union[str, "os.PathLike[str]"]]
|
||||
],
|
||||
encoding: str = "utf-8",
|
||||
followlinks: bool = False,
|
||||
) -> None:
|
||||
@ -191,33 +193,39 @@ 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:
|
||||
# Use posixpath even on Windows to avoid "drive:" or UNC
|
||||
# segments breaking out of the search directory.
|
||||
filename = posixpath.join(searchpath, *pieces)
|
||||
f = open_if_exists(filename)
|
||||
if f is None:
|
||||
continue
|
||||
|
||||
if os.path.isfile(filename):
|
||||
break
|
||||
else:
|
||||
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()
|
||||
|
||||
mtime = os.path.getmtime(filename)
|
||||
|
||||
def uptodate() -> bool:
|
||||
try:
|
||||
contents = f.read().decode(self.encoding)
|
||||
finally:
|
||||
f.close()
|
||||
return os.path.getmtime(filename) == mtime
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
mtime = os.path.getmtime(filename)
|
||||
# Use normpath to convert Windows altsep to sep.
|
||||
return contents, os.path.normpath(filename), uptodate
|
||||
|
||||
def uptodate() -> bool:
|
||||
try:
|
||||
return os.path.getmtime(filename) == mtime
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
# Use normpath to convert Windows altsep to sep.
|
||||
return contents, os.path.normpath(filename), uptodate
|
||||
raise TemplateNotFound(template)
|
||||
|
||||
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)
|
||||
@ -225,8 +233,8 @@ class FileSystemLoader(BaseLoader):
|
||||
for filename in filenames:
|
||||
template = (
|
||||
os.path.join(dirpath, filename)[len(searchpath) :]
|
||||
.strip(os.path.sep)
|
||||
.replace(os.path.sep, "/")
|
||||
.strip(os.sep)
|
||||
.replace(os.sep, "/")
|
||||
)
|
||||
if template[:2] == "./":
|
||||
template = template[2:]
|
||||
@ -235,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.
|
||||
|
||||
@ -274,12 +305,12 @@ class PackageLoader(BaseLoader):
|
||||
package_path: "str" = "templates",
|
||||
encoding: str = "utf-8",
|
||||
) -> None:
|
||||
package_path = os.path.normpath(package_path).rstrip(os.path.sep)
|
||||
package_path = os.path.normpath(package_path).rstrip(os.sep)
|
||||
|
||||
# normpath preserves ".", which isn't valid in zip paths.
|
||||
if package_path == os.path.curdir:
|
||||
package_path = ""
|
||||
elif package_path[:2] == os.path.curdir + os.path.sep:
|
||||
elif package_path[:2] == os.path.curdir + os.sep:
|
||||
package_path = package_path[2:]
|
||||
|
||||
self.package_path = package_path
|
||||
@ -295,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.path.sep)
|
||||
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.
|
||||
@ -312,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.
|
||||
@ -365,37 +400,30 @@ 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.
|
||||
offset = len(self._template_root)
|
||||
|
||||
for dirpath, _, filenames in os.walk(self._template_root):
|
||||
dirpath = dirpath[offset:].lstrip(os.path.sep)
|
||||
dirpath = dirpath[offset:].lstrip(os.sep)
|
||||
results.extend(
|
||||
os.path.join(dirpath, name).replace(os.path.sep, "/")
|
||||
os.path.join(dirpath, name).replace(os.sep, "/")
|
||||
for name in filenames
|
||||
)
|
||||
else:
|
||||
if not hasattr(self._loader, "_files"):
|
||||
raise TypeError(
|
||||
"This zip import does not have the required"
|
||||
" metadata to list templates."
|
||||
)
|
||||
files = _get_zipimporter_files(self._loader)
|
||||
|
||||
# Package is a zip file.
|
||||
prefix = (
|
||||
self._template_root[len(self._archive) :].lstrip(os.path.sep)
|
||||
+ os.path.sep
|
||||
)
|
||||
prefix = self._template_root[len(self._archive) :].lstrip(os.sep) + os.sep
|
||||
offset = len(prefix)
|
||||
|
||||
for name in self._loader._files.keys(): # type: ignore
|
||||
for name in files:
|
||||
# Find names under the templates directory that aren't directories.
|
||||
if name.startswith(prefix) and name[-1] != os.path.sep:
|
||||
results.append(name[offset:].replace(os.path.sep, "/"))
|
||||
if name.startswith(prefix) and name[-1] != os.sep:
|
||||
results.append(name[offset:].replace(os.sep, "/"))
|
||||
|
||||
results.sort()
|
||||
return results
|
||||
@ -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`.
|
||||
"""
|
||||
@ -601,7 +622,10 @@ class ModuleLoader(BaseLoader):
|
||||
has_source_access = False
|
||||
|
||||
def __init__(
|
||||
self, path: t.Union[str, os.PathLike, t.Sequence[t.Union[str, os.PathLike]]]
|
||||
self,
|
||||
path: t.Union[
|
||||
str, "os.PathLike[str]", t.Sequence[t.Union[str, "os.PathLike[str]"]]
|
||||
],
|
||||
) -> None:
|
||||
package_name = f"_jinja2_module_templates_{id(self):x}"
|
||||
|
||||
@ -637,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}"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""Functions that expose information about templates that might be
|
||||
interesting for introspection.
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
|
||||
from . import nodes
|
||||
@ -16,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."""
|
||||
@ -30,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
|
||||
@ -55,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
|
||||
@ -106,7 +106,7 @@ class NativeTemplate(Template):
|
||||
|
||||
try:
|
||||
return self.environment_class.concat( # type: ignore
|
||||
self.root_render_func(ctx) # type: ignore
|
||||
self.root_render_func(ctx)
|
||||
)
|
||||
except Exception:
|
||||
return self.environment.handle_exception()
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
some node tree helper functions used by the parser and compiler in order
|
||||
to normalize nodes.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import operator
|
||||
import typing as t
|
||||
@ -13,11 +14,12 @@ from .utils import _PassArg
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
from .environment import Environment
|
||||
|
||||
_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,
|
||||
@ -27,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,
|
||||
@ -56,7 +58,7 @@ class NodeType(type):
|
||||
|
||||
def __new__(mcs, name, bases, d): # type: ignore
|
||||
for attr in "fields", "attributes":
|
||||
storage = []
|
||||
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"
|
||||
@ -72,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):
|
||||
@ -89,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(
|
||||
@ -117,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
|
||||
@ -135,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))
|
||||
@ -144,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`
|
||||
@ -166,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
|
||||
@ -181,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`.
|
||||
"""
|
||||
@ -191,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.
|
||||
@ -248,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
|
||||
@ -272,7 +274,7 @@ class Node(metaclass=NodeType):
|
||||
_dump(value)
|
||||
buf.append(")")
|
||||
|
||||
buf: t.List[str] = []
|
||||
buf: list[str] = []
|
||||
_dump(self)
|
||||
return "".join(buf)
|
||||
|
||||
@ -295,7 +297,7 @@ class Template(Node):
|
||||
"""
|
||||
|
||||
fields = ("body",)
|
||||
body: t.List[Node]
|
||||
body: list[Node]
|
||||
|
||||
|
||||
class Output(Stmt):
|
||||
@ -304,7 +306,7 @@ class Output(Stmt):
|
||||
"""
|
||||
|
||||
fields = ("nodes",)
|
||||
nodes: t.List["Expr"]
|
||||
nodes: list["Expr"]
|
||||
|
||||
|
||||
class Extends(Stmt):
|
||||
@ -326,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
|
||||
|
||||
|
||||
@ -337,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):
|
||||
@ -350,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):
|
||||
@ -362,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"
|
||||
|
||||
|
||||
@ -383,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):
|
||||
@ -397,7 +399,7 @@ class Block(Stmt):
|
||||
|
||||
fields = ("name", "body", "scoped", "required")
|
||||
name: str
|
||||
body: t.List[Node]
|
||||
body: list[Node]
|
||||
scoped: bool
|
||||
required: bool
|
||||
|
||||
@ -434,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
|
||||
|
||||
|
||||
@ -459,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):
|
||||
@ -467,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.
|
||||
|
||||
@ -494,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
|
||||
@ -518,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
|
||||
@ -582,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
|
||||
@ -609,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()
|
||||
@ -625,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)
|
||||
|
||||
@ -643,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]
|
||||
|
||||
@ -656,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)
|
||||
|
||||
@ -672,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)
|
||||
|
||||
@ -686,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)
|
||||
|
||||
@ -699,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)
|
||||
@ -714,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)
|
||||
|
||||
@ -738,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:
|
||||
@ -790,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()
|
||||
|
||||
@ -822,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):
|
||||
@ -836,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()
|
||||
|
||||
@ -860,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()
|
||||
|
||||
@ -878,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)
|
||||
@ -899,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)
|
||||
|
||||
@ -913,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)
|
||||
|
||||
@ -991,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)
|
||||
|
||||
@ -1001,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)
|
||||
|
||||
@ -1084,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))
|
||||
|
||||
@ -1099,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()
|
||||
@ -1150,7 +1146,7 @@ class Scope(Stmt):
|
||||
"""An artificial scope."""
|
||||
|
||||
fields = ("body",)
|
||||
body: t.List[Node]
|
||||
body: list[Node]
|
||||
|
||||
|
||||
class OverlayScope(Stmt):
|
||||
@ -1169,7 +1165,7 @@ class OverlayScope(Stmt):
|
||||
|
||||
fields = ("context", "body")
|
||||
context: Expr
|
||||
body: t.List[Node]
|
||||
body: list[Node]
|
||||
|
||||
|
||||
class EvalContextModifier(Stmt):
|
||||
@ -1182,7 +1178,7 @@ class EvalContextModifier(Stmt):
|
||||
"""
|
||||
|
||||
fields = ("options",)
|
||||
options: t.List[Keyword]
|
||||
options: list[Keyword]
|
||||
|
||||
|
||||
class ScopedEvalContextModifier(EvalContextModifier):
|
||||
@ -1192,7 +1188,7 @@ class ScopedEvalContextModifier(EvalContextModifier):
|
||||
"""
|
||||
|
||||
fields = ("body",)
|
||||
body: t.List[Node]
|
||||
body: list[Node]
|
||||
|
||||
|
||||
# make sure nobody creates custom nodes
|
||||
|
||||
@ -7,6 +7,7 @@ want. For example, loop unrolling doesn't work because unrolled loops
|
||||
would have a different scope. The solution would be a second syntax tree
|
||||
that stored the scoping rules.
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
|
||||
from . import nodes
|
||||
@ -24,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(
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
"""Parse tokens from the lexer into nodes for the compiler."""
|
||||
|
||||
import typing
|
||||
import typing as t
|
||||
|
||||
@ -10,6 +11,7 @@ from .lexer import describe_token_expr
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
from .environment import Environment
|
||||
|
||||
_ImportInclude = t.TypeVar("_ImportInclude", nodes.Import, nodes.Include)
|
||||
@ -33,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,
|
||||
@ -52,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
|
||||
@ -87,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:
|
||||
@ -125,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.
|
||||
@ -136,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)
|
||||
@ -145,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
|
||||
@ -155,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":
|
||||
@ -192,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
|
||||
@ -220,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)
|
||||
@ -270,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")
|
||||
@ -311,12 +309,14 @@ class Parser:
|
||||
# enforce that required blocks only contain whitespace or comments
|
||||
# by asserting that the body, if not empty, is just TemplateData nodes
|
||||
# with whitespace data
|
||||
if node.required and not all(
|
||||
isinstance(child, nodes.TemplateData) and child.data.isspace()
|
||||
for body in node.body
|
||||
for child in body.nodes # type: ignore
|
||||
):
|
||||
self.fail("Required blocks can only contain comments or whitespace")
|
||||
if node.required:
|
||||
for body_node in node.body:
|
||||
if not isinstance(body_node, nodes.Output) or any(
|
||||
not isinstance(output_node, nodes.TemplateData)
|
||||
or not output_node.data.isspace()
|
||||
for output_node in body_node.nodes
|
||||
):
|
||||
self.fail("Required blocks can only contain comments or whitespace")
|
||||
|
||||
self.stream.skip_if("name:" + node.name)
|
||||
return node
|
||||
@ -455,26 +455,24 @@ class Parser:
|
||||
@typing.overload
|
||||
def parse_assign_target(
|
||||
self, with_tuple: bool = ..., name_only: "te.Literal[True]" = ...
|
||||
) -> nodes.Name:
|
||||
...
|
||||
) -> nodes.Name: ...
|
||||
|
||||
@typing.overload
|
||||
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: ...
|
||||
|
||||
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
|
||||
@ -485,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")
|
||||
|
||||
@ -522,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()
|
||||
@ -641,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]
|
||||
@ -679,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
|
||||
@ -702,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:
|
||||
@ -743,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")
|
||||
@ -755,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")
|
||||
@ -796,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
|
||||
|
||||
@ -814,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")
|
||||
@ -829,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)
|
||||
@ -857,9 +861,16 @@ class Parser:
|
||||
else:
|
||||
args.append(None)
|
||||
|
||||
return nodes.Slice(lineno=lineno, *args)
|
||||
return nodes.Slice(lineno=lineno, *args) # noqa: B026
|
||||
|
||||
def parse_call_args(self) -> t.Tuple:
|
||||
def parse_call_args(
|
||||
self,
|
||||
) -> tuple[
|
||||
list[nodes.Expr],
|
||||
list[nodes.Keyword],
|
||||
nodes.Expr | None,
|
||||
nodes.Expr | None,
|
||||
]:
|
||||
token = self.stream.expect("lparen")
|
||||
args = []
|
||||
kwargs = []
|
||||
@ -916,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)
|
||||
@ -950,7 +961,7 @@ class Parser:
|
||||
next(self.stream)
|
||||
name += "." + self.stream.expect("name").value
|
||||
dyn_args = dyn_kwargs = None
|
||||
kwargs = []
|
||||
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 {
|
||||
@ -976,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:
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
"""The runtime functions and state used by compiled templates."""
|
||||
|
||||
import functools
|
||||
import sys
|
||||
import typing as t
|
||||
@ -28,7 +29,9 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import logging
|
||||
|
||||
import typing_extensions as te
|
||||
|
||||
from .environment import Environment
|
||||
|
||||
class LoopRenderFunc(te.Protocol):
|
||||
@ -37,8 +40,7 @@ if t.TYPE_CHECKING:
|
||||
reciter: t.Iterable[V],
|
||||
loop_render_func: "LoopRenderFunc",
|
||||
depth: int = 0,
|
||||
) -> str:
|
||||
...
|
||||
) -> str: ...
|
||||
|
||||
|
||||
# these variables are exported to the template runtime
|
||||
@ -90,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:
|
||||
@ -163,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)
|
||||
|
||||
@ -242,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.
|
||||
@ -259,7 +261,10 @@ class Context:
|
||||
|
||||
@internalcode
|
||||
def call(
|
||||
__self, __obj: t.Callable, *args: t.Any, **kwargs: t.Any # noqa: B902
|
||||
__self, # noqa: B902
|
||||
__obj: t.Callable[..., t.Any],
|
||||
*args: t.Any,
|
||||
**kwargs: t.Any,
|
||||
) -> t.Union[t.Any, "Undefined"]:
|
||||
"""Call the callable with the arguments and keyword arguments
|
||||
provided but inject the active context or environment as first
|
||||
@ -272,9 +277,9 @@ class Context:
|
||||
# Allow callable classes to take a context
|
||||
if (
|
||||
hasattr(__obj, "__call__") # noqa: B004
|
||||
and _PassArg.from_obj(__obj.__call__) is not None # type: ignore
|
||||
and _PassArg.from_obj(__obj.__call__) is not None
|
||||
):
|
||||
__obj = __obj.__call__ # type: ignore
|
||||
__obj = __obj.__call__
|
||||
|
||||
pass_arg = _PassArg.from_obj(__obj)
|
||||
|
||||
@ -302,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.
|
||||
@ -343,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
|
||||
@ -362,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
|
||||
)
|
||||
|
||||
@ -376,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)
|
||||
@ -392,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
|
||||
@ -401,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:
|
||||
@ -551,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
|
||||
@ -586,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)
|
||||
|
||||
@ -639,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
|
||||
@ -660,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
|
||||
@ -762,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:
|
||||
@ -770,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
|
||||
|
||||
@ -787,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)
|
||||
@ -810,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
|
||||
@ -853,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()
|
||||
@ -899,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.
|
||||
@ -927,9 +938,7 @@ def make_logging_undefined(
|
||||
logger.addHandler(logging.StreamHandler(sys.stderr))
|
||||
|
||||
def _log_message(undef: Undefined) -> None:
|
||||
logger.warning( # type: ignore
|
||||
"Template variable warning: %s", undef._undefined_message
|
||||
)
|
||||
logger.warning("Template variable warning: %s", undef._undefined_message)
|
||||
|
||||
class LoggingUndefined(base): # type: ignore
|
||||
__slots__ = ()
|
||||
@ -979,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):
|
||||
@ -1041,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__,
|
||||
)
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
"""A sandbox layer that ensures unsafe operations cannot be performed.
|
||||
Useful when the template itself comes from an untrusted source.
|
||||
"""
|
||||
|
||||
import operator
|
||||
import types
|
||||
import 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 markupsafe import EscapeFormatter
|
||||
@ -23,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"}
|
||||
@ -37,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.FrozenSet[str]], ...] = (
|
||||
_mutable_spec: tuple[tuple[type[t.Any], frozenset[str]], ...] = (
|
||||
(
|
||||
abc.MutableSet,
|
||||
frozenset(
|
||||
@ -59,7 +61,9 @@ _mutable_spec: t.Tuple[t.Tuple[t.Type, t.FrozenSet[str]], ...] = (
|
||||
),
|
||||
(
|
||||
abc.MutableSequence,
|
||||
frozenset(["append", "reverse", "insert", "sort", "extend", "remove"]),
|
||||
frozenset(
|
||||
["append", "clear", "pop", "reverse", "insert", "sort", "extend", "remove"]
|
||||
),
|
||||
),
|
||||
(
|
||||
deque,
|
||||
@ -80,20 +84,6 @@ _mutable_spec: t.Tuple[t.Tuple[t.Type, t.FrozenSet[str]], ...] = (
|
||||
)
|
||||
|
||||
|
||||
def inspect_format_method(callable: t.Callable) -> 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.
|
||||
@ -200,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,
|
||||
@ -213,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,
|
||||
}
|
||||
@ -232,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
|
||||
@ -247,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)
|
||||
@ -295,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]
|
||||
@ -313,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.
|
||||
"""
|
||||
@ -330,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)
|
||||
@ -345,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] = 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
|
||||
@ -382,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.
|
||||
@ -413,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:
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
"""Built-in template tests used with the ``is`` operator."""
|
||||
|
||||
import operator
|
||||
import typing as t
|
||||
from collections import abc
|
||||
@ -169,7 +170,7 @@ def test_sequence(value: t.Any) -> bool:
|
||||
"""
|
||||
try:
|
||||
len(value)
|
||||
value.__getitem__
|
||||
value.__getitem__ # noqa B018
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@ -204,7 +205,7 @@ def test_escaped(value: t.Any) -> bool:
|
||||
return hasattr(value, "__html__")
|
||||
|
||||
|
||||
def test_in(value: t.Any, seq: t.Container) -> bool:
|
||||
def test_in(value: t.Any, seq: t.Container[t.Any]) -> bool:
|
||||
"""Check if value is in seq.
|
||||
|
||||
.. versionadded:: 2.10
|
||||
|
||||
@ -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]:
|
||||
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``.
|
||||
"""
|
||||
@ -182,7 +191,7 @@ def object_type_repr(obj: t.Any) -> str:
|
||||
|
||||
def pformat(obj: t.Any) -> str:
|
||||
"""Format an object using :func:`pprint.pformat`."""
|
||||
from pprint import pformat # type: ignore
|
||||
from pprint import pformat
|
||||
|
||||
return pformat(obj)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -259,7 +268,7 @@ def urlize(
|
||||
if trim_url_limit is not None:
|
||||
|
||||
def trim_url(x: str) -> str:
|
||||
if len(x) > trim_url_limit: # type: ignore
|
||||
if len(x) > trim_url_limit:
|
||||
return f"{x[:trim_url_limit]}..."
|
||||
|
||||
return x
|
||||
@ -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:
|
||||
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
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""API for traversing the AST nodes. Implemented by the compiler and
|
||||
meta introspection.
|
||||
"""
|
||||
|
||||
import typing as t
|
||||
|
||||
from .nodes import Node
|
||||
@ -9,8 +10,7 @@ if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
class VisitCallable(te.Protocol):
|
||||
def __call__(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||
...
|
||||
def __call__(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.Any: ...
|
||||
|
||||
|
||||
class NodeVisitor:
|
||||
@ -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.
|
||||
@ -43,8 +43,8 @@ class NodeVisitor:
|
||||
|
||||
def generic_visit(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||
"""Called if no explicit visitor function exists for a node."""
|
||||
for node in node.iter_child_nodes():
|
||||
self.visit(node, *args, **kwargs)
|
||||
for child_node in node.iter_child_nodes():
|
||||
self.visit(child_node, *args, **kwargs)
|
||||
|
||||
|
||||
class NodeTransformer(NodeVisitor):
|
||||
@ -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."""
|
||||
|
||||
@ -150,7 +150,8 @@ class TestExtendedAPI:
|
||||
assert t.render(foo="<foo>") == "<foo>"
|
||||
|
||||
def test_sandbox_max_range(self, env):
|
||||
from jinja2.sandbox import SandboxedEnvironment, MAX_RANGE
|
||||
from jinja2.sandbox import MAX_RANGE
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
env = SandboxedEnvironment()
|
||||
t = env.from_string("{% for item in range(total) %}{{ item }}{% endfor %}")
|
||||
@ -264,7 +265,7 @@ class TestUndefined:
|
||||
|
||||
def test_undefined_and_special_attributes(self):
|
||||
with pytest.raises(AttributeError):
|
||||
Undefined("Foo").__dict__
|
||||
Undefined("Foo").__dict__ # noqa B018
|
||||
|
||||
def test_undefined_attribute_error(self):
|
||||
# Django's LazyObject turns the __class__ attribute into a
|
||||
@ -322,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)
|
||||
@ -334,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() == ""
|
||||
@ -367,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)
|
||||
@ -385,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):
|
||||
@ -432,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,5 +1,3 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from jinja2 import ChainableUndefined
|
||||
@ -13,7 +11,7 @@ from jinja2.exceptions import UndefinedError
|
||||
from jinja2.nativetypes import NativeEnvironment
|
||||
|
||||
|
||||
def test_basic_async():
|
||||
def test_basic_async(run_async_fn):
|
||||
t = Template(
|
||||
"{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True
|
||||
)
|
||||
@ -21,11 +19,11 @@ def test_basic_async():
|
||||
async def func():
|
||||
return await t.render_async()
|
||||
|
||||
rv = asyncio.run(func())
|
||||
rv = run_async_fn(func)
|
||||
assert rv == "[1][2][3]"
|
||||
|
||||
|
||||
def test_await_on_calls():
|
||||
def test_await_on_calls(run_async_fn):
|
||||
t = Template("{{ async_func() + normal_func() }}", enable_async=True)
|
||||
|
||||
async def async_func():
|
||||
@ -37,7 +35,7 @@ def test_await_on_calls():
|
||||
async def func():
|
||||
return await t.render_async(async_func=async_func, normal_func=normal_func)
|
||||
|
||||
rv = asyncio.run(func())
|
||||
rv = run_async_fn(func)
|
||||
assert rv == "65"
|
||||
|
||||
|
||||
@ -54,7 +52,7 @@ def test_await_on_calls_normal_render():
|
||||
assert rv == "65"
|
||||
|
||||
|
||||
def test_await_and_macros():
|
||||
def test_await_and_macros(run_async_fn):
|
||||
t = Template(
|
||||
"{% macro foo(x) %}[{{ x }}][{{ async_func() }}]{% endmacro %}{{ foo(42) }}",
|
||||
enable_async=True,
|
||||
@ -66,11 +64,11 @@ def test_await_and_macros():
|
||||
async def func():
|
||||
return await t.render_async(async_func=async_func)
|
||||
|
||||
rv = asyncio.run(func())
|
||||
rv = run_async_fn(func)
|
||||
assert rv == "[42][42]"
|
||||
|
||||
|
||||
def test_async_blocks():
|
||||
def test_async_blocks(run_async_fn):
|
||||
t = Template(
|
||||
"{% block foo %}<Test>{% endblock %}{{ self.foo() }}",
|
||||
enable_async=True,
|
||||
@ -80,7 +78,7 @@ def test_async_blocks():
|
||||
async def func():
|
||||
return await t.render_async()
|
||||
|
||||
rv = asyncio.run(func())
|
||||
rv = run_async_fn(func)
|
||||
assert rv == "<Test><Test>"
|
||||
|
||||
|
||||
@ -156,8 +154,8 @@ class TestAsyncImports:
|
||||
test_env_async.from_string('{% from "foo" import bar, with, context %}')
|
||||
test_env_async.from_string('{% from "foo" import bar, with with context %}')
|
||||
|
||||
def test_exports(self, test_env_async):
|
||||
coro = test_env_async.from_string(
|
||||
def test_exports(self, test_env_async, run_async_fn):
|
||||
coro_fn = test_env_async.from_string(
|
||||
"""
|
||||
{% macro toplevel() %}...{% endmacro %}
|
||||
{% macro __private() %}...{% endmacro %}
|
||||
@ -166,9 +164,9 @@ class TestAsyncImports:
|
||||
{% macro notthere() %}{% endmacro %}
|
||||
{% endfor %}
|
||||
"""
|
||||
)._get_default_module_async()
|
||||
m = asyncio.run(coro)
|
||||
assert asyncio.run(m.toplevel()) == "..."
|
||||
)._get_default_module_async
|
||||
m = run_async_fn(coro_fn)
|
||||
assert run_async_fn(m.toplevel) == "..."
|
||||
assert not hasattr(m, "__missing")
|
||||
assert m.variable == 42
|
||||
assert not hasattr(m, "notthere")
|
||||
@ -451,23 +449,23 @@ 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"
|
||||
|
||||
def test_loop_errors(self, test_env_async):
|
||||
def test_loop_errors(self, test_env_async, run_async_fn):
|
||||
tmpl = test_env_async.from_string(
|
||||
"""{% for item in [1] if loop.index
|
||||
== 0 %}...{% endfor %}"""
|
||||
)
|
||||
pytest.raises(UndefinedError, tmpl.render)
|
||||
with pytest.raises(UndefinedError):
|
||||
run_async_fn(tmpl.render_async)
|
||||
|
||||
tmpl = test_env_async.from_string(
|
||||
"""{% for item in [] %}...{% else
|
||||
%}{{ loop }}{% endfor %}"""
|
||||
)
|
||||
assert tmpl.render() == ""
|
||||
assert run_async_fn(tmpl.render_async) == ""
|
||||
|
||||
def test_loop_filter(self, test_env_async):
|
||||
tmpl = test_env_async.from_string(
|
||||
@ -597,7 +595,7 @@ class TestAsyncForLoop:
|
||||
assert t.render(a=dict(b=[1, 2, 3])) == "1"
|
||||
|
||||
|
||||
def test_namespace_awaitable(test_env_async):
|
||||
def test_namespace_awaitable(test_env_async, run_async_fn):
|
||||
async def _test():
|
||||
t = test_env_async.from_string(
|
||||
'{% set ns = namespace(foo="Bar") %}{{ ns.foo }}'
|
||||
@ -605,10 +603,10 @@ def test_namespace_awaitable(test_env_async):
|
||||
actual = await t.render_async()
|
||||
assert actual == "Bar"
|
||||
|
||||
asyncio.run(_test())
|
||||
run_async_fn(_test)
|
||||
|
||||
|
||||
def test_chainable_undefined_aiter():
|
||||
def test_chainable_undefined_aiter(run_async_fn):
|
||||
async def _test():
|
||||
t = Template(
|
||||
"{% for x in a['b']['c'] %}{{ x }}{% endfor %}",
|
||||
@ -618,7 +616,7 @@ def test_chainable_undefined_aiter():
|
||||
rv = await t.render_async(a={})
|
||||
assert rv == ""
|
||||
|
||||
asyncio.run(_test())
|
||||
run_async_fn(_test)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -626,22 +624,22 @@ def async_native_env():
|
||||
return NativeEnvironment(enable_async=True)
|
||||
|
||||
|
||||
def test_native_async(async_native_env):
|
||||
def test_native_async(async_native_env, run_async_fn):
|
||||
async def _test():
|
||||
t = async_native_env.from_string("{{ x }}")
|
||||
rv = await t.render_async(x=23)
|
||||
assert rv == 23
|
||||
|
||||
asyncio.run(_test())
|
||||
run_async_fn(_test)
|
||||
|
||||
|
||||
def test_native_list_async(async_native_env):
|
||||
def test_native_list_async(async_native_env, run_async_fn):
|
||||
async def _test():
|
||||
t = async_native_env.from_string("{{ x }}")
|
||||
rv = await t.render_async(x=list(range(3)))
|
||||
assert rv == [0, 1, 2]
|
||||
|
||||
asyncio.run(_test())
|
||||
run_async_fn(_test)
|
||||
|
||||
|
||||
def test_getitem_after_filter():
|
||||
@ -658,3 +656,65 @@ def test_getitem_after_call():
|
||||
t = env.from_string("{{ add_each(a, 2)[1:] }}")
|
||||
out = t.render(a=range(3))
|
||||
assert out == "[3, 4]"
|
||||
|
||||
|
||||
def test_basic_generate_async(run_async_fn):
|
||||
t = Template(
|
||||
"{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True
|
||||
)
|
||||
|
||||
async def func():
|
||||
agen = t.generate_async()
|
||||
try:
|
||||
return await agen.__anext__()
|
||||
finally:
|
||||
await agen.aclose()
|
||||
|
||||
rv = run_async_fn(func)
|
||||
assert rv == "["
|
||||
|
||||
|
||||
def test_include_generate_async(run_async_fn, test_env_async):
|
||||
t = test_env_async.from_string('{% include "header" %}')
|
||||
|
||||
async def func():
|
||||
agen = t.generate_async()
|
||||
try:
|
||||
return await agen.__anext__()
|
||||
finally:
|
||||
await agen.aclose()
|
||||
|
||||
rv = run_async_fn(func)
|
||||
assert rv == "["
|
||||
|
||||
|
||||
def test_blocks_generate_async(run_async_fn):
|
||||
t = Template(
|
||||
"{% block foo %}<Test>{% endblock %}{{ self.foo() }}",
|
||||
enable_async=True,
|
||||
autoescape=True,
|
||||
)
|
||||
|
||||
async def func():
|
||||
agen = t.generate_async()
|
||||
try:
|
||||
return await agen.__anext__()
|
||||
finally:
|
||||
await agen.aclose()
|
||||
|
||||
rv = run_async_fn(func)
|
||||
assert rv == "<Test>"
|
||||
|
||||
|
||||
def test_async_extend(run_async_fn, test_env_async):
|
||||
t = test_env_async.from_string('{% extends "header" %}')
|
||||
|
||||
async def func():
|
||||
agen = t.generate_async()
|
||||
try:
|
||||
return await agen.__anext__()
|
||||
finally:
|
||||
await agen.aclose()
|
||||
|
||||
rv = run_async_fn(func)
|
||||
assert rv == "["
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import contextlib
|
||||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
@ -26,10 +27,30 @@ def env_async():
|
||||
return Environment(enable_async=True)
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def closing_factory():
|
||||
async with contextlib.AsyncExitStack() as stack:
|
||||
|
||||
def closing(maybe_agen):
|
||||
try:
|
||||
aclose = maybe_agen.aclose
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
stack.push_async_callback(aclose)
|
||||
return maybe_agen
|
||||
|
||||
yield closing
|
||||
|
||||
|
||||
@mark_dualiter("foo", lambda: range(10))
|
||||
def test_first(env_async, foo):
|
||||
tmpl = env_async.from_string("{{ foo()|first }}")
|
||||
out = tmpl.render(foo=foo)
|
||||
def test_first(env_async, foo, run_async_fn):
|
||||
async def test():
|
||||
async with closing_factory() as closing:
|
||||
tmpl = env_async.from_string("{{ closing(foo())|first }}")
|
||||
return await tmpl.render_async(foo=foo, closing=closing)
|
||||
|
||||
out = run_async_fn(test)
|
||||
assert out == "0"
|
||||
|
||||
|
||||
@ -245,18 +266,30 @@ def test_slice(env_async, items):
|
||||
)
|
||||
|
||||
|
||||
def test_custom_async_filter(env_async):
|
||||
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)
|
||||
|
||||
env_async.filters["customfilter"] = customfilter
|
||||
tmpl = env_async.from_string("{{ 'static'|customfilter }} {{ arg|customfilter }}")
|
||||
out = tmpl.render(arg="dynamic")
|
||||
async def test():
|
||||
env_async.filters["customfilter"] = customfilter
|
||||
tmpl = env_async.from_string(
|
||||
"{{ 'static'|customfilter }} {{ arg|customfilter }}"
|
||||
)
|
||||
return await tmpl.render_async(arg="dynamic")
|
||||
|
||||
out = run_async_fn(test)
|
||||
assert out == "static dynamic"
|
||||
|
||||
|
||||
@mark_dualiter("items", lambda: range(10))
|
||||
def test_custom_async_iteratable_filter(env_async, items):
|
||||
def test_custom_async_iteratable_filter(env_async, items, run_async_fn):
|
||||
async def customfilter(iterable):
|
||||
items = []
|
||||
async for item in auto_aiter(iterable):
|
||||
@ -265,9 +298,13 @@ def test_custom_async_iteratable_filter(env_async, items):
|
||||
break
|
||||
return ",".join(items)
|
||||
|
||||
env_async.filters["customfilter"] = customfilter
|
||||
tmpl = env_async.from_string(
|
||||
"{{ items()|customfilter }} .. {{ [3, 4, 5, 6]|customfilter }}"
|
||||
)
|
||||
out = tmpl.render(items=items)
|
||||
async def test():
|
||||
async with closing_factory() as closing:
|
||||
env_async.filters["customfilter"] = customfilter
|
||||
tmpl = env_async.from_string(
|
||||
"{{ closing(items())|customfilter }} .. {{ [3, 4, 5, 6]|customfilter }}"
|
||||
)
|
||||
return await tmpl.render_async(items=items, closing=closing)
|
||||
|
||||
out = run_async_fn(test)
|
||||
assert out == "0,1,2 .. 3,4,5"
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -7,6 +7,7 @@ from jinja2 import DictLoader
|
||||
from jinja2 import Environment
|
||||
from jinja2 import nodes
|
||||
from jinja2 import pass_context
|
||||
from jinja2 import TemplateSyntaxError
|
||||
from jinja2.exceptions import TemplateAssertionError
|
||||
from jinja2.ext import Extension
|
||||
from jinja2.lexer import count_newlines
|
||||
@ -468,6 +469,18 @@ class TestInternationalization:
|
||||
(3, "npgettext", ("babel", "%(users)s user", "%(users)s users", None), []),
|
||||
]
|
||||
|
||||
def test_nested_trans_error(self):
|
||||
s = "{% trans %}foo{% trans %}{% endtrans %}"
|
||||
with pytest.raises(TemplateSyntaxError) as excinfo:
|
||||
i18n_env.from_string(s)
|
||||
assert "trans blocks can't be nested" in str(excinfo.value)
|
||||
|
||||
def test_trans_block_error(self):
|
||||
s = "{% trans %}foo{% wibble bar %}{% endwibble %}{% endtrans %}"
|
||||
with pytest.raises(TemplateSyntaxError) as excinfo:
|
||||
i18n_env.from_string(s)
|
||||
assert "saw `wibble`" in str(excinfo.value)
|
||||
|
||||
|
||||
class TestScope:
|
||||
def test_basic_scope_behavior(self):
|
||||
@ -541,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() == (
|
||||
@ -474,6 +475,13 @@ class TestFilter:
|
||||
assert 'bar="23"' in out
|
||||
assert 'blub:blub="<?>"' in out
|
||||
|
||||
@pytest.mark.parametrize("sep", ("\t", "\n", "\f", " ", "/", ">", "="))
|
||||
def test_xmlattr_key_invalid(self, env: Environment, sep: str) -> None:
|
||||
with pytest.raises(ValueError, match="Invalid character"):
|
||||
env.from_string("{{ {key: 'my_class'}|xmlattr }}").render(
|
||||
key=f"class{sep}onclick=alert(1)"
|
||||
)
|
||||
|
||||
def test_sort1(self, env):
|
||||
tmpl = env.from_string("{{ [2, 3, 1]|sort }}|{{ [2, 3, 1]|sort(true) }}")
|
||||
assert tmpl.render() == "[1, 2, 3]|[3, 2, 1]"
|
||||
@ -870,4 +878,6 @@ class TestFilter:
|
||||
|
||||
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
|
||||
t1.render(x=42)
|
||||
|
||||
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
|
||||
t2.render(x=42)
|
||||
|
||||
@ -287,26 +287,34 @@ class TestInheritance:
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"default": "{% block x required %}data {# #}{% endblock %}",
|
||||
"default1": "{% block x required %}{% block y %}"
|
||||
"{% endblock %} {% endblock %}",
|
||||
"default2": "{% block x required %}{% if true %}"
|
||||
"{% endif %} {% endblock %}",
|
||||
"level1": "{% if default %}{% extends default %}"
|
||||
"{% else %}{% extends 'default' %}{% endif %}"
|
||||
"{%- block x %}CHILD{% endblock %}",
|
||||
"empty": "{% block x required %}{% endblock %}",
|
||||
"blank": "{% block x required %} {# c #}{% endblock %}",
|
||||
"text": "{% block x required %}data {# c #}{% endblock %}",
|
||||
"block": "{% block x required %}{% block y %}"
|
||||
"{% endblock %}{% endblock %}",
|
||||
"if": "{% block x required %}{% if true %}"
|
||||
"{% endif %}{% endblock %}",
|
||||
"top": "{% extends t %}{% block x %}CHILD{% endblock %}",
|
||||
}
|
||||
)
|
||||
)
|
||||
t = env.get_template("level1")
|
||||
t = env.get_template("top")
|
||||
assert t.render(t="empty") == "CHILD"
|
||||
assert t.render(t="blank") == "CHILD"
|
||||
|
||||
with pytest.raises(
|
||||
required_block_check = pytest.raises(
|
||||
TemplateSyntaxError,
|
||||
match="Required blocks can only contain comments or whitespace",
|
||||
):
|
||||
assert t.render(default="default")
|
||||
assert t.render(default="default2")
|
||||
assert t.render(default="default3")
|
||||
)
|
||||
|
||||
with required_block_check:
|
||||
t.render(t="text")
|
||||
|
||||
with required_block_check:
|
||||
t.render(t="block")
|
||||
|
||||
with required_block_check:
|
||||
t.render(t="if")
|
||||
|
||||
def test_required_with_scope(self, env):
|
||||
env = Environment(
|
||||
@ -347,18 +355,20 @@ class TestInheritance:
|
||||
)
|
||||
)
|
||||
tmpl = env.get_template("child")
|
||||
|
||||
with pytest.raises(TemplateSyntaxError):
|
||||
tmpl.render(default="default1", seq=list(range(3)))
|
||||
|
||||
with pytest.raises(TemplateSyntaxError):
|
||||
tmpl.render(default="default2", seq=list(range(3)))
|
||||
|
||||
|
||||
class TestBugFix:
|
||||
def test_fixed_macro_scoping_bug(self, env):
|
||||
assert (
|
||||
Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"test.html": """\
|
||||
assert Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"test.html": """\
|
||||
{% extends 'details.html' %}
|
||||
|
||||
{% macro my_macro() %}
|
||||
@ -369,7 +379,7 @@ class TestBugFix:
|
||||
{{ my_macro() }}
|
||||
{% endblock %}
|
||||
""",
|
||||
"details.html": """\
|
||||
"details.html": """\
|
||||
{% extends 'standard.html' %}
|
||||
|
||||
{% macro my_macro() %}
|
||||
@ -385,17 +395,12 @@ class TestBugFix:
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
""",
|
||||
"standard.html": """
|
||||
"standard.html": """
|
||||
{% block content %} {% endblock %}
|
||||
""",
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
.get_template("test.html")
|
||||
.render()
|
||||
.split()
|
||||
== ["outer_box", "my_macro"]
|
||||
)
|
||||
).get_template("test.html").render().split() == ["outer_box", "my_macro"]
|
||||
|
||||
def test_double_extends(self, env):
|
||||
"""Ensures that a template with more than 1 {% extends ... %} usage
|
||||
|
||||
@ -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 %}"
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ import importlib.abc
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
@ -180,9 +179,28 @@ 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
|
||||
mod_env = None
|
||||
|
||||
def compile_down(self, prefix_loader, zip="deflated"):
|
||||
log = []
|
||||
@ -196,13 +214,14 @@ class TestModuleLoader:
|
||||
self.mod_env = Environment(loader=loaders.ModuleLoader(self.archive))
|
||||
return "".join(log)
|
||||
|
||||
def teardown(self):
|
||||
if hasattr(self, "mod_env"):
|
||||
def teardown_method(self):
|
||||
if self.archive is not None:
|
||||
if os.path.isfile(self.archive):
|
||||
os.remove(self.archive)
|
||||
else:
|
||||
shutil.rmtree(self.archive)
|
||||
self.archive = None
|
||||
self.mod_env = None
|
||||
|
||||
def test_log(self, prefix_loader):
|
||||
log = self.compile_down(prefix_loader)
|
||||
@ -362,8 +381,8 @@ def test_package_zip_source(package_zip_loader, template, expect):
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
platform.python_implementation() == "PyPy",
|
||||
reason="PyPy's zipimporter doesn't have a '_files' attribute.",
|
||||
sys.implementation.name == "pypy",
|
||||
reason="zipimporter doesn't have a '_files' attribute",
|
||||
raises=TypeError,
|
||||
)
|
||||
def test_package_zip_list(package_zip_loader):
|
||||
@ -410,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)
|
||||
|
||||
@ -599,6 +599,7 @@ class TestBug:
|
||||
|
||||
def test_markup_and_chainable_undefined(self):
|
||||
from markupsafe import Markup
|
||||
|
||||
from jinja2.runtime import ChainableUndefined
|
||||
|
||||
assert str(Markup(ChainableUndefined())) == ""
|
||||
@ -736,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
|
||||
|
||||
24
tox.ini
24
tox.ini
@ -1,24 +0,0 @@
|
||||
[tox]
|
||||
envlist =
|
||||
py3{11,10,9,8,7},pypy3{8,7}
|
||||
style
|
||||
typing
|
||||
docs
|
||||
skip_missing_interpreters = true
|
||||
|
||||
[testenv]
|
||||
deps = -r requirements/tests.txt
|
||||
commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs}
|
||||
|
||||
[testenv:style]
|
||||
deps = pre-commit
|
||||
skip_install = true
|
||||
commands = pre-commit run --all-files --show-diff-on-failure
|
||||
|
||||
[testenv:typing]
|
||||
deps = -r requirements/typing.txt
|
||||
commands = mypy
|
||||
|
||||
[testenv:docs]
|
||||
deps = -r requirements/docs.txt
|
||||
commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html
|
||||
Loading…
Reference in New Issue
Block a user