Compare commits

...

352 Commits
3.1.0 ... main

Author SHA1 Message Date
David Lord
5ef70112a1
Merge branch 'stable' 2025-06-14 13:34:58 -07:00
David Lord
284501eb0f
remove slsa provenance (#2105) 2025-06-14 13:10:44 -07:00
David Lord
7eb5758063
remove slsa provenance
PyPI and trusted publishing has built-in attestation support now.
2025-06-14 13:08:46 -07:00
David Lord
0514dce509
Merge branch 'stable' 2025-06-12 13:59:39 -07:00
David Lord
ede7905d30
svg logo 2025-06-12 13:58:36 -07:00
David Lord
77092a882a
Merge branch 'stable' 2025-06-11 10:38:29 -07:00
David Lord
0b08e13cac
svg logo (#2102) 2025-06-11 10:37:58 -07:00
David Lord
8ee8f90d67
svg logo 2025-06-11 10:32:48 -07:00
David Lord
10304827e4
update identifier pattern for Python 3.10 (#2099) 2025-05-28 12:14:31 -07:00
David Lord
574565b1ef
update identifier pattern for Python 3.10 2025-05-28 12:10:09 -07:00
David Lord
0547cd6c58
add Python version comment 2025-05-28 12:09:42 -07:00
David Lord
9c11dd65ff
fix typo 2025-05-28 11:44:50 -07:00
David Lord
9e49736ae0
deprecate __version__ (#2098) 2025-05-28 11:43:08 -07:00
David Lord
b7ce542db1
deprecate __version__ 2025-05-28 11:39:42 -07:00
David Lord
dfe82ade3d
bump minimum versions of dependencies (#2097) 2025-05-28 11:20:08 -07:00
David Lord
9508c57faa
bump minimum versions of dependencies 2025-05-28 11:17:40 -07:00
David Lord
49c48a0b31
drop end of life python versions (#2096) 2025-05-28 11:15:29 -07:00
David Lord
ece7c271f3
fix ruff pyupgrade findings 2025-05-28 11:13:57 -07:00
David Lord
0cc6ff9051
drop end of life python versions 2025-05-28 11:13:57 -07:00
David Lord
05f5d74849
Updated dependency management to uv (#2093) 2025-05-28 10:10:27 -07:00
Adam Englander
a3dce7bb64
fix type/lint/format findings 2025-05-28 10:08:39 -07:00
Adam Englander
51dbd8977e
use uv for dependency management
drop python 3.7 and 3.8
move tox config into pyproject
use new license metadata
2025-05-28 10:08:29 -07:00
David Lord
220e67ae99
Merge branch 'stable' 2025-03-05 12:15:40 -08:00
David Lord
01d0bab939
release version 3.1.6 (#2077) 2025-03-05 12:05:21 -08:00
David Lord
15206881c0
release version 3.1.6 2025-03-05 11:51:17 -08:00
David Lord
90457bbf33
Merge commit from fork
attr filter uses env.getattr
2025-03-05 11:49:35 -08:00
David Lord
065334d1ee
attr filter uses env.getattr 2025-03-05 10:08:48 -08:00
David Lord
033c20015c
start version 3.1.6 2025-03-05 09:50:59 -08:00
David Lord
bc68d4efa9
use global contributing guide (#2070) 2025-01-14 13:45:32 -08:00
David Lord
247de5e0c5
use global contributing guide
Remove the per-project files so we don't have to keep them in sync.
GitHub's UI links to everything except the contributing guide, so add a
section about that to the readme.
2025-01-14 13:43:41 -08:00
David Lord
6aeab5d1da
Merge branch 'stable' 2024-12-21 10:47:46 -08:00
David Lord
ab8218c7a1
use project advisory link instead of global 2024-12-21 10:47:08 -08:00
David Lord
b4ffc8ff29
release version 3.1.5 (#2066) 2024-12-21 10:30:50 -08:00
David Lord
877f6e51be
release version 3.1.5 2024-12-21 10:16:13 -08:00
David Lord
8d58859265
remove test pypi 2024-12-21 10:14:49 -08:00
David Lord
eda8fe86fd
update dev dependencies 2024-12-21 10:14:25 -08:00
David Lord
c8fdce1e03
Fix bug involving calling set on a template parameter within all branches of an if block (#1665) 2024-12-21 09:46:55 -08:00
Kevin Brown-Silva
66587ce989
Fix bug where set would sometimes fail within if
There was a bug that came as the result of an early optimization done
within ID tracking that caused loading parameters to fail in a very
specific and rare edge case. That edge case only occurred when the
parameter was being set within all 3 standard branches of an if block,
since the optimization would assume that the parameter was never being
referenced and was only ever being set. This would cause the variable to
be set to undefined.

The fix for this was to remove the optimization and still continue to
load in the parameter even if it is set in all 3 branches.
2024-12-21 09:40:06 -08:00
David Lord
fbc3a696c7
Add support for namespaces in tuple parsing (#1664) 2024-12-20 14:52:51 -08:00
David Lord
b8f4831d41
more comments about nsref assignment
only emit nsref instance check once per ref name
refactor primary name parsing a bit
2024-12-20 14:49:58 -08:00
Kevin Brown-Silva
ee832194cd
Add support for namespaces in tuple assignment
This fixes a bug that existed because namespaces within `{% set %}`
were treated as a special case. This special case had the side-effect
of bypassing the code which allows for tuples to be assigned to.

The solution was to make tuple handling (and by extension, primary token
handling) aware of namespaces so that namespace tokens can be handled
appropriately. This is handled in a backwards-compatible way which
ensures that we do not try to parse namespace tokens when we otherwise
would be expecting to parse out name tokens with attributes.

Namespace instance checks are moved earlier, and deduplicated, so that
all checks are done before the assignment. Otherwise, the check could be
emitted in the middle of the tuple.
2024-12-20 14:09:40 -08:00
David Lord
1d55cddbb2
Triple quotes in docs (#2064) 2024-12-20 08:31:34 -08:00
David Lord
8a8eafc6b9
edit block assignment section 2024-12-20 08:29:04 -08:00
ratchek
d6998ab74e
Make ease of use update to template documentation
Add the phrases 'multiline comment' and 'triple quotes' to docs
in the templates/#block-assignments section. This allows for new
users to find this alternative easily.
2024-12-20 08:19:26 -08:00
David Lord
e7cb37de59
document SandboxedNativeEnvironment pattern (#2063) 2024-12-20 07:58:33 -08:00
David Lord
ae68c961dc
document SandboxedNativeEnvironment pattern 2024-12-20 07:57:11 -08:00
David Lord
028f61da7b
Pass context to test when using select (#1762) 2024-12-19 20:49:09 -08:00
Rens Groothuijsen
d05bd3858c
Pass context when using select 2024-12-19 20:47:24 -08:00
David Lord
7a41ddb915
don't apply urlize to @a@b (#2062) 2024-12-19 20:41:36 -08:00
наб
0cd6948192
don't apply urlize to @a@b 2024-12-19 20:37:58 -08:00
David Lord
106d61cba5
improve annotations for methods returning copies (#1880) 2024-12-19 20:28:13 -08:00
Victor Westerhuis
ded9915fc5
improve annotations for methods returning copies 2024-12-19 20:26:44 -08:00
David Lord
53c75915c9
Improve the PackageLoader error message (#1706) 2024-12-19 20:17:25 -08:00
David Lord
aaa083d265
separate messages, add test 2024-12-19 20:15:10 -08:00
Lily Foote
f54fa113d3
Improve the PackageLoader error message
This exception is raised when the `package_path` directory (default "templates") is not
found, so explain this.
2024-12-19 20:02:32 -08:00
David Lord
58a358f092
FileSystemLoader include paths in error (#1663) 2024-12-19 19:38:22 -08:00
David Lord
227edfd372
clean up message, add test 2024-12-19 19:34:34 -08:00
Yourun-Proger
ed5f76206a
FileSystemLoader includes search paths in error 2024-12-19 19:33:29 -08:00
David Lord
b4b28ec01c
fix default for Environment.overlay(enable_async) (#2061) 2024-12-19 18:23:52 -08:00
SamyCookie
e45bc745a7
Bugfix: wrong default argument for Environment.overlay(enable_async) parameter 2024-12-19 18:22:32 -08:00
David Lord
767b236176
fix f-string syntax error in code generation (#1852) 2024-12-19 18:10:28 -08:00
Sigurd Spieckermann
56a724644b
fix f-string syntax error in code generation 2024-12-19 18:08:42 -08:00
David Lord
48b0687e05
Merge commit from fork
fix format string vulnerability
2024-12-19 14:33:08 -08:00
Lydxn
91a972f580
sandbox indirect calls to str.format 2024-12-19 12:34:27 -08:00
David Lord
0871c71d01
rearrange change entry 2024-12-19 12:07:14 -08:00
David Lord
91e3521173
sandbox disallows clear and pop on mutable sequence (#2033) 2024-12-19 08:36:07 -08:00
Dylan Scott
b512058270
sandbox disallows clear and pop on mutable sequence 2024-12-19 08:33:38 -08:00
David Lord
1dc04bccf9
Fix pickle/copy support for the missing singleton (#2029) 2024-12-19 08:20:38 -08:00
Matt Clay
7232b82462
Fix pickle/copy support for the missing singleton 2024-12-19 08:19:07 -08:00
David Lord
ba8847a466
Preserve __slots__ metadata on Undefined types (#2026) 2024-12-19 08:14:53 -08:00
Matt Davis
d4fb0e8c40
preserve __slots__ on Undefined classes 2024-12-19 08:11:49 -08:00
David Lord
39d9ffff1f
Make compiled output deterministic for tuple unpacking in set tag (#2022) 2024-12-19 08:05:48 -08:00
Anentropic
4936e4d482
make tuple unpacking deterministic in compiler 2024-12-19 08:02:33 -08:00
David Lord
3ef3ba885b
fix how int filter handles scientific notation (#1984) 2024-12-19 07:59:45 -08:00
Felipe Moreno
2eb4542cba
int filter handles OverflowError to handle scientific notation 2024-12-19 07:58:25 -08:00
David Lord
20be10e566
make unique filter async aware (#1782) 2024-12-19 07:26:24 -08:00
Mehdi ABAAKOUK
76af7110ea
make unique filter async-aware 2024-12-19 07:19:13 -08:00
David Lord
a4abbfd753
Use correct concat function for blocks evaluation (#1702) 2024-12-19 07:15:37 -08:00
Martin Krizek
d3a0b1a4ab
use env.concat when calling block reference 2024-12-19 07:12:43 -08:00
David Lord
791dd3b041
Simplify example for ModuleLoader (#1695) 2024-12-18 16:08:33 -08:00
Charles-Axel Dein
955d7daf3d
Simplify example for ModuleLoader
The `ModuleLoader` example seems copy pasted from `ChoiceLoader`. As a result it's not immediately clear how their API differ.
2024-12-18 10:07:18 -08:00
David Lord
13ce60bad8
fix Jinja syntax in example (#2056) 2024-12-18 09:47:24 -08:00
JamesParrott
0c0a3d02d1
fix Jinja syntax in example 2024-12-18 09:44:30 -08:00
David Lord
3d0a7d7b0f
clarify blocks docs (#2060) 2024-12-18 09:39:32 -08:00
David Lord
786d12b529
clarify block outer scope docs 2024-12-18 09:36:11 -08:00
David Lord
c667d56de3
change "per default" to "by default" 2024-12-18 09:33:31 -08:00
David Lord
a12789e7f9
fix list comprehension example (#2017) 2024-12-18 09:21:09 -08:00
Andreas Lindhé
75f0fbf6cb
fix list comprehension example 2024-12-18 09:17:18 -08:00
David Lord
13c42b3aab
Slightly improve clarity of logical bool ops (#1938) 2024-12-18 09:13:55 -08:00
Stephen Rosen
64a6bd1b66
improve clarity of logical bool ops
co-authored-by: David Lord <davidism@gmail.com>
2024-12-18 09:11:57 -08:00
David Lord
619d8eef41
Fix typo on filter name (#1911) 2024-12-18 08:54:02 -08:00
Vitor Buxbaum
7d023e5a86
Fix typo on filter name 2024-12-18 08:52:06 -08:00
David Lord
420082efa5
fix a typo in docs/templates.rst (#1881) 2024-12-18 08:50:01 -08:00
Meng Xiangzhuo
8a90b760a8
fix a typo in docs/templates.rst 2024-12-18 08:48:25 -08:00
David Lord
058e059662
fix boolean error about whitespace control (#1819) 2024-12-18 08:46:48 -08:00
Hugo Vassard
9c3622c1af
fix boolean error about whitespace control 2024-12-18 08:44:58 -08:00
David Lord
da6729990f
Clarify what operations the default "Undefined" supports (#1818) 2024-12-18 08:36:06 -08:00
Clay Sweetser
4e7850ce1b
Clarify what operations the default Undefined supports 2024-12-18 08:34:47 -08:00
David Lord
9849db5215
Add link to MarkupSafe project mentioned in FAQ (#1767) 2024-12-18 08:29:29 -08:00
Matheus Felipe
f502aac8dc
Add link to MarkupSafe in FAQ 2024-12-18 08:27:51 -08:00
Ronan Amicel
d680a95932
Fix nl2br example in documentation (#2054) 2024-12-07 11:04:28 -05:00
David Lord
ada0a9a6fc
update test workflow trigger 2024-10-24 14:16:05 -07:00
David Lord
ee6c734e9b
Merge branch 'stable' 2024-10-24 14:14:07 -07:00
David Lord
d3d0910d8a
update test workflow trigger 2024-10-24 14:13:40 -07:00
David Lord
1e383959f7
update dev dependencies 2024-10-24 14:13:23 -07:00
David Lord
af054f3e48
Improve documentation for initializing the i18n extension (#2023) 2024-09-10 08:43:21 -07:00
Aarni Koskela
a9a0197e3c Improve documentation for initializing the i18n extension
Refs discussion at https://stackoverflow.com/a/78970088
2024-09-10 18:42:07 +03:00
David Lord
eb0df049de
set up pre-commit lite workflow
Committed via https://github.com/asottile/all-repos
2024-09-01 09:04:14 -07:00
David Lord
180816e571
set up pre-commit lite workflow
Committed via https://github.com/asottile/all-repos
2024-09-01 08:48:02 -07:00
David Lord
4c49d2322c
set up pre-commit lite workflow
Committed via https://github.com/asottile/all-repos
2024-09-01 08:32:27 -07:00
David Lord
cd74006a9b
remove pre-commit.ci update 2024-08-23 18:06:04 -07:00
David Lord
9949b49808
Merge branch '3.1.x' 2024-08-23 17:17:11 -07:00
David Lord
3e5b5b2794
refactor 3.7 test pins 2024-08-23 17:15:36 -07:00
David Lord
7f0fc0ad2c
Merge branch '3.1.x' 2024-08-23 16:52:48 -07:00
David Lord
896a1d59b7
remove dependabot 2024-08-23 16:50:05 -07:00
David Lord
3adf44dde2
apply ruff fixes 2024-08-23 16:49:44 -07:00
David Lord
65b27afb61
update dev dependencies 2024-08-23 16:49:33 -07:00
David Lord
a59744f50e
add gha-update 2024-08-23 16:43:52 -07:00
David Lord
b490da6b23
[pre-commit.ci] pre-commit autoupdate (#2012) 2024-08-06 08:30:44 -07:00
pre-commit-ci[bot]
295b284b7c
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.5.6)
2024-08-05 23:09:20 +00:00
David Lord
68d75132c4
Bump the python-requirements group in /requirements with 3 updates (#2007) 2024-08-03 07:29:33 -07:00
David Lord
0464cf88a0
Bump the github-actions group with 3 updates (#2008) 2024-08-03 07:28:10 -07:00
dependabot[bot]
293c8abe93
Bump the github-actions group with 3 updates
Bumps the github-actions group with 3 updates: [actions/setup-python](https://github.com/actions/setup-python), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact).


Updates `actions/setup-python` from 5.1.0 to 5.1.1
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](82c7e631bb...39cd14951b)

Updates `actions/upload-artifact` from 4.3.3 to 4.3.4
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](65462800fd...0b2256b8c0)

Updates `actions/download-artifact` from 4.1.7 to 4.1.8
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](65a9edc588...fa0a91b85d)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 07:36:57 +00:00
dependabot[bot]
ae8b5354e0
Bump the python-requirements group in /requirements with 3 updates
Bumps the python-requirements group in /requirements with 3 updates: [sphinx](https://github.com/sphinx-doc/sphinx), [trio](https://github.com/python-trio/trio) and [pre-commit](https://github.com/pre-commit/pre-commit).


Updates `sphinx` from 7.3.7 to 8.0.2
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/v8.0.2/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.3.7...v8.0.2)

Updates `trio` from 0.25.1 to 0.26.0
- [Release notes](https://github.com/python-trio/trio/releases)
- [Commits](https://github.com/python-trio/trio/compare/v0.25.1...v0.26.0)

Updates `pre-commit` from 3.7.1 to 3.8.0
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.7.1...v3.8.0)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: python-requirements
- dependency-name: trio
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-requirements
- dependency-name: pre-commit
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: python-requirements
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 07:18:20 +00:00
James Addison
f8323cf404
Additional test coverage for async rendering of native type templates (#1807) 2024-07-10 09:14:52 -07:00
David Lord
d7225e65f3
[pre-commit.ci] pre-commit autoupdate (#2001) 2024-07-02 04:17:37 -07:00
pre-commit-ci[bot]
4e04e110e7
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.7 → v0.5.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.7...v0.5.0)
2024-07-02 00:15:40 +00:00
David Lord
0087c5fe00
Bump the python-requirements group in /requirements with 2 updates (#1998) 2024-07-01 16:22:58 -07:00
David Lord
e72c1825d4
Bump the github-actions group with 2 updates (#1999) 2024-07-01 12:55:36 -07:00
dependabot[bot]
64b54f2189
Bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish).


Updates `actions/checkout` from 4.1.6 to 4.1.7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](a5ac7e51b4...692973e3d9)

Updates `pypa/gh-action-pypi-publish` from 1.8.14 to 1.9.0
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](81e9d935c8...ec4db0b4dd)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 07:37:23 +00:00
dependabot[bot]
64e6151474
Bump the python-requirements group in /requirements with 2 updates
Bumps the python-requirements group in /requirements with 2 updates: [trio](https://github.com/python-trio/trio) and [tox](https://github.com/tox-dev/tox).


Updates `trio` from 0.22.2 to 0.25.1
- [Release notes](https://github.com/python-trio/trio/releases)
- [Commits](https://github.com/python-trio/trio/compare/v0.22.2...v0.25.1)

Updates `tox` from 4.15.0 to 4.15.1
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.15.0...4.15.1)

---
updated-dependencies:
- dependency-name: trio
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-requirements
- dependency-name: tox
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: python-requirements
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 07:16:24 +00:00
David Lord
9c6c319899
[pre-commit.ci] pre-commit autoupdate (#1993) 2024-06-03 15:25:15 -07:00
pre-commit-ci[bot]
4b6dac1b6b [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-06-03 22:10:44 +00:00
pre-commit-ci[bot]
951868f355
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.4 → v0.4.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.4...v0.4.7)
2024-06-03 22:10:36 +00:00
David Lord
afb577b313
Bump actions/checkout from 4.1.4 to 4.1.6 in the github-actions group (#1990) 2024-06-01 06:02:39 -07:00
dependabot[bot]
71e374d895
Bump actions/checkout from 4.1.4 to 4.1.6 in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4.1.4 to 4.1.6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](0ad4b8fada...a5ac7e51b4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-01 07:12:42 +00:00
Timothée Mazzucotelli
1470c17f9f
Convert rST code block to Markdown in README 2024-05-19 09:51:33 -04:00
David Lord
8710cabd4f
Convert rST code block to Markdown in README (#1981) 2024-05-19 09:48:28 -04:00
Timothée Mazzucotelli
90750800d4
Convert rST code block to Markdown in README 2024-05-19 15:32:56 +02:00
David Lord
8a8e2bc4d7
fix test_package_zip_list on 3.13 (#1979) 2024-05-13 12:34:50 -07:00
Thomas Grainger
679af7f816
fix test_package_zip_list on 3.13 2024-05-13 20:04:38 +01:00
David Lord
b002d9c6c3
Merge branch '3.1.x' 2024-05-13 08:47:27 -07:00
David Lord
e82013c399
test with python 3.13 (#1977) 2024-05-13 08:45:44 -07:00
David Lord
004476c22b
test on python 3.13
update dev dependencies
refactor update tox envs
3.7 requires an old version of trio

xfail zip loader template test
2024-05-13 08:40:44 -07:00
Thomas Grainger
1655128cfc
test on trio, fix all missing aclose related warnings (#1960) 2024-05-11 15:01:12 -07:00
David Lord
079e8312c3
use asyncio.run in Template.render (#1952) 2024-05-11 13:45:07 -07:00
Thomas Grainger
5bc613ec45
use asyncio.run 2024-05-11 13:41:46 -07:00
David Lord
2fcabb529f
start version 3.1.5 2024-05-11 13:41:03 -07:00
David Lord
a516a99bab
[pre-commit.ci] pre-commit autoupdate (#1975) 2024-05-06 15:29:13 -07:00
pre-commit-ci[bot]
a89ed5fe0f
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.1 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.1...v0.4.3)
2024-05-06 22:05:37 +00:00
David Lord
11550f9df9
Merge branch '3.1.x' 2024-05-05 16:45:28 -07:00
David Lord
6e7b0face6
release version 3.1.4 (#1974) 2024-05-05 16:42:40 -07:00
David Lord
dd4a8b5466
release version 3.1.4 2024-05-05 16:37:30 -07:00
David Lord
0668239dc6
Merge pull request from GHSA-h75v-3vvj-5mfj
disallow invalid characters in keys to xmlattr filter
2024-05-05 16:35:24 -07:00
David Lord
bbd5bcee7b
Merge branch '3.1.x' 2024-05-02 09:18:48 -07:00
David Lord
d655030770
disallow invalid characters in keys to xmlattr filter 2024-05-02 09:14:00 -07:00
David Lord
a7863ba9d3
add ghsa links 2024-05-02 08:42:59 -07:00
David Lord
b5c98e78c2
start version 3.1.4 2024-05-02 08:41:50 -07:00
David Lord
c6dd4bac24
Bump the python-requirements group in /requirements with 5 updates (#1973) 2024-05-01 06:43:12 -07:00
David Lord
6fcf463011
fix mypy findings 2024-05-01 06:41:20 -07:00
David Lord
27ea85b001
Bump the github-actions group with 2 updates (#1972) 2024-05-01 05:54:56 -07:00
dependabot[bot]
2e8bbca767
Bump the python-requirements group in /requirements with 5 updates
Bumps the python-requirements group in /requirements with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [pytest](https://github.com/pytest-dev/pytest) | `8.1.1` | `8.2.0` |
| [pallets-sphinx-themes](https://github.com/pallets/pallets-sphinx-themes) | `2.1.2` | `2.1.3` |
| [mypy](https://github.com/python/mypy) | `1.9.0` | `1.10.0` |
| [pyright](https://github.com/RobertCraigie/pyright-python) | `1.1.359` | `1.1.360` |
| [tox](https://github.com/tox-dev/tox) | `4.14.2` | `4.15.0` |


Updates `pytest` from 8.1.1 to 8.2.0
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.1.1...8.2.0)

Updates `pallets-sphinx-themes` from 2.1.2 to 2.1.3
- [Release notes](https://github.com/pallets/pallets-sphinx-themes/releases)
- [Changelog](https://github.com/pallets/pallets-sphinx-themes/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/pallets-sphinx-themes/compare/2.1.2...2.1.3)

Updates `mypy` from 1.9.0 to 1.10.0
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/1.9.0...v1.10.0)

Updates `pyright` from 1.1.359 to 1.1.360
- [Release notes](https://github.com/RobertCraigie/pyright-python/releases)
- [Commits](https://github.com/RobertCraigie/pyright-python/compare/v1.1.359...v1.1.360)

Updates `tox` from 4.14.2 to 4.15.0
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.14.2...4.15.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-requirements
- dependency-name: pallets-sphinx-themes
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-requirements
- dependency-name: pyright
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
- dependency-name: tox
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: python-requirements
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-01 07:30:56 +00:00
dependabot[bot]
4a7a153a48
Bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/download-artifact](https://github.com/actions/download-artifact).


Updates `actions/checkout` from 4.1.3 to 4.1.4
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](1d96c772d1...0ad4b8fada)

Updates `actions/download-artifact` from 4.1.6 to 4.1.7
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](9c19ed7fe5...65a9edc588)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-01 07:10:08 +00:00
David Lord
2a17038fca
Revert "upload/download-artifact v4"
This reverts commit c8aca74587.
2024-04-23 16:22:41 -07:00
David Lord
c8aca74587
upload/download-artifact v4 2024-04-23 15:48:42 -07:00
David Lord
9b33637538
Bump the github-actions group across 1 directory with 4 updates (#1970) 2024-04-23 12:35:10 -07:00
dependabot[bot]
2e3e3774a9
Bump the github-actions group across 1 directory with 4 updates
Bumps the github-actions group with 4 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [actions/upload-artifact](https://github.com/actions/upload-artifact), [slsa-framework/slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator) and [actions/download-artifact](https://github.com/actions/download-artifact).


Updates `actions/checkout` from 4.1.2 to 4.1.3
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](9bb56186c3...1d96c772d1)

Updates `actions/upload-artifact` from 3.1.3 to 4.3.3
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](a8a3f3ad30...65462800fd)

Updates `slsa-framework/slsa-github-generator` from 1.10.0 to 2.0.0
- [Release notes](https://github.com/slsa-framework/slsa-github-generator/releases)
- [Changelog](https://github.com/slsa-framework/slsa-github-generator/blob/main/CHANGELOG.md)
- [Commits](https://github.com/slsa-framework/slsa-github-generator/compare/v1.10.0...v2.0.0)

Updates `actions/download-artifact` from 3.0.2 to 4.1.6
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](9bc31d5ccc...9c19ed7fe5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: slsa-framework/slsa-github-generator
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-23 19:33:07 +00:00
David Lord
6d6a6c2546
unignore upload/download-artifact 2024-04-23 12:32:15 -07:00
David Lord
a2438d20b0
update dev dependencies 2024-04-23 12:23:07 -07:00
David Lord
fcd3d3bbf3
drop support for Python 3.7 2024-04-23 12:22:59 -07:00
David Lord
de6131232a
Merge branch '3.1.x' 2024-04-23 12:07:24 -07:00
David Lord
da3a9f0b80
update project files (#1968) 2024-04-23 10:20:59 -07:00
David Lord
0ee5eb41d1
satisfy formatter, linter, and strict mypy 2024-04-23 09:29:26 -07:00
David Lord
20477c6357
update project files (#5457)
* update pre-commit hooks
* add devcontainer
* show url in publish environment
* update actions versions
* separate typing job
* use dependabot grouped updates
  ignore upload/download-artifact until slsa updates
* use sphinx.ext.extlinks instead of sphinx-issues
* update editorconfig
* update gitignore
* update .readthedocs.yaml
* license is txt, readme is md
* use pyproject.toml and flit_core instead of setuptools
 add typed classifier
 add pyright config
 simplify urls
* tox builds docs in place
* add tox env to update all dev dependencies
* update issue and pr templates
* simplify matrix
2024-04-23 09:28:57 -07:00
David Lord
e491223739
update pyyaml dev dependency 2024-04-22 11:08:14 -07:00
David Lord
3fd91e4d11
Merge branch '3.1.x' 2024-01-10 15:22:02 -08:00
David Lord
36f98854c7
fix pr link 2024-01-10 15:17:32 -08:00
David Lord
a0e864ec0f
release version 3.1.3 (#1926) 2024-01-10 15:12:50 -08:00
David Lord
d9de4bb215
release version 3.1.3 2024-01-10 15:08:43 -08:00
David Lord
50124e1656
skip test pypi 2024-01-10 15:08:33 -08:00
David Lord
9ea7222ef3
use trusted publishing 2024-01-10 15:01:45 -08:00
David Lord
da703f7aae
use trusted publishing 2024-01-10 14:53:37 -08:00
David Lord
bce1746925
use trusted publishing 2024-01-10 14:43:52 -08:00
David Lord
7f8fb54782
use trusted publishing 2024-01-10 14:37:08 -08:00
David Lord
7277d8068b
update pre-commit hooks 2024-01-10 14:33:07 -08:00
David Lord
5c8a105224
Make nested-trans-block exceptions nicer (#1918) 2024-01-10 14:28:47 -08:00
Aarni Koskela
19a55db3b4
Make nested-trans-block exceptions nicer 2024-01-10 14:27:09 -08:00
David Lord
716795349a
Merge pull request from GHSA-h5c8-rqwp-cp95
Raise an exception when spaces are used in HTML attribute keys generated by xmlattr
2024-01-10 14:07:26 -08:00
Calum Hutton
7dd3680e6e
xmlattr filter disallows keys with spaces 2024-01-10 14:01:13 -08:00
David Lord
d594969d72
Bump slsa-framework/slsa-github-generator from 1.7.0 to 1.9.0 (#1885) 2023-09-06 08:59:20 -07:00
David Lord
ec22f25312
Bump pypa/gh-action-pypi-publish from 1.8.8 to 1.8.10 (#1884) 2023-09-06 08:59:11 -07:00
David Lord
21fa43ca01
Bump actions/checkout from 3.5.3 to 3.6.0 (#1883) 2023-09-06 08:59:00 -07:00
dependabot[bot]
938e7ca5bb
Bump slsa-framework/slsa-github-generator from 1.7.0 to 1.9.0
Bumps [slsa-framework/slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator) from 1.7.0 to 1.9.0.
- [Release notes](https://github.com/slsa-framework/slsa-github-generator/releases)
- [Changelog](https://github.com/slsa-framework/slsa-github-generator/blob/main/CHANGELOG.md)
- [Commits](https://github.com/slsa-framework/slsa-github-generator/compare/v1.7.0...v1.9.0)

---
updated-dependencies:
- dependency-name: slsa-framework/slsa-github-generator
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-01 16:03:13 +00:00
dependabot[bot]
f0685845e1
Bump pypa/gh-action-pypi-publish from 1.8.8 to 1.8.10
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.8 to 1.8.10.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](f8c70e705f...b7f401de30)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-01 16:03:08 +00:00
dependabot[bot]
fcafd5087b
Bump actions/checkout from 3.5.3 to 3.6.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.3 to 3.6.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](c85c95e3d7...f43a0e5ff2)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-01 16:03:03 +00:00
David Lord
86f28a9df0
Bump pypa/gh-action-pypi-publish from 1.8.7 to 1.8.8 (#1877) 2023-08-01 09:50:36 -07:00
David Lord
f272b6d8b6
Bump actions/setup-python from 4.6.1 to 4.7.0 (#1876) 2023-08-01 09:50:28 -07:00
dependabot[bot]
9db787b566
Bump pypa/gh-action-pypi-publish from 1.8.7 to 1.8.8
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.7 to 1.8.8.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](f5622bde02...f8c70e705f)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 16:43:19 +00:00
dependabot[bot]
f575dc7385
Bump actions/setup-python from 4.6.1 to 4.7.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.6.1 to 4.7.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](bd6b4b6205...61a6322f88)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 16:43:15 +00:00
David Lord
d84a1743e4
[pre-commit.ci] pre-commit autoupdate (#1875) 2023-08-01 09:17:50 -07:00
pre-commit-ci[bot]
4504beba06
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.8.0 → v3.10.1](https://github.com/asottile/pyupgrade/compare/v3.8.0...v3.10.1)
- [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0)
- [github.com/PyCQA/flake8: 6.0.0 → 6.1.0](https://github.com/PyCQA/flake8/compare/6.0.0...6.1.0)
2023-08-01 08:46:03 +00:00
David Lord
31b764ea83
[pre-commit.ci] pre-commit autoupdate (#1868) 2023-07-04 06:18:32 -07:00
pre-commit-ci[bot]
ac57ea048d
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.7.0 → v3.8.0](https://github.com/asottile/pyupgrade/compare/v3.7.0...v3.8.0)
2023-07-04 06:36:57 +00:00
David Lord
2a2bfb7f95
Bump actions/checkout from 3.5.2 to 3.5.3 (#1867) 2023-07-01 09:21:29 -07:00
David Lord
ec2649688d
Bump slsa-framework/slsa-github-generator from 1.6.0 to 1.7.0 (#1866) 2023-07-01 09:21:14 -07:00
David Lord
859039244a
Bump dessant/lock-threads from 4.0.0 to 4.0.1 (#1865) 2023-07-01 09:21:06 -07:00
David Lord
890c8a9519
Bump pypa/gh-action-pypi-publish from 1.8.6 to 1.8.7 (#1864) 2023-07-01 09:20:56 -07:00
dependabot[bot]
505effc995
Bump actions/checkout from 3.5.2 to 3.5.3
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.2 to 3.5.3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](8e5e7e5ab8...c85c95e3d7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-01 16:13:15 +00:00
dependabot[bot]
956b1f1ce9
Bump slsa-framework/slsa-github-generator from 1.6.0 to 1.7.0
Bumps [slsa-framework/slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/slsa-framework/slsa-github-generator/releases)
- [Changelog](https://github.com/slsa-framework/slsa-github-generator/blob/main/CHANGELOG.md)
- [Commits](https://github.com/slsa-framework/slsa-github-generator/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: slsa-framework/slsa-github-generator
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-01 16:13:11 +00:00
dependabot[bot]
c4b8d066d6
Bump dessant/lock-threads from 4.0.0 to 4.0.1
Bumps [dessant/lock-threads](https://github.com/dessant/lock-threads) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/dessant/lock-threads/releases)
- [Changelog](https://github.com/dessant/lock-threads/blob/main/CHANGELOG.md)
- [Commits](c1b35aecc5...be8aa5be94)

---
updated-dependencies:
- dependency-name: dessant/lock-threads
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-01 16:13:08 +00:00
dependabot[bot]
5258c9d27d
Bump pypa/gh-action-pypi-publish from 1.8.6 to 1.8.7
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.6 to 1.8.7.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](a56da0b891...f5622bde02)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-01 16:13:06 +00:00
David Lord
534c9e13ed
switch to flit build backend (#1863) 2023-06-28 07:44:58 -07:00
David Lord
8310b2bbef
switch to flit build backend 2023-06-28 07:31:12 -07:00
David Lord
fed2d0808f
Merge branch '3.1.x' 2023-06-27 08:01:35 -07:00
David Lord
d80f186832
simplify tox config
envs inherit base testenv
2023-06-27 08:01:19 -07:00
David Lord
c0e4f69ff3
Merge branch '3.1.x' 2023-06-27 07:37:41 -07:00
David Lord
8d0ea74289
update dependencies 2023-06-27 07:36:34 -07:00
David Lord
fd5128f864
[pre-commit.ci] pre-commit autoupdate (#1859) 2023-06-06 09:31:03 -07:00
pre-commit-ci[bot]
02df0dad29
[pre-commit.ci] pre-commit autoupdate
updates:
- https://github.com/asottile/reorder_python_importshttps://github.com/asottile/reorder-python-imports
2023-06-06 04:21:37 +00:00
David Lord
7b48764688
Bump actions/setup-python from 4.6.0 to 4.6.1 (#1856) 2023-06-01 11:49:14 -07:00
David Lord
4b18fd4f1f
Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6 (#1855) 2023-06-01 11:47:50 -07:00
David Lord
3914664578
Bump slsa-framework/slsa-github-generator from 1.5.0 to 1.6.0 (#1857) 2023-06-01 11:46:45 -07:00
David Lord
81a23847cd
Merge branch '3.1.x' 2023-06-01 10:50:38 -07:00
David Lord
ae312b3065
update dependencies 2023-06-01 10:47:07 -07:00
David Lord
051df10c7b
fix check for empty required block (#1858) 2023-06-01 10:42:27 -07:00
David Lord
37f5b058ee
start version 3.1.3 2023-06-01 10:31:10 -07:00
dependabot[bot]
23aab8330c
Bump slsa-framework/slsa-github-generator from 1.5.0 to 1.6.0
Bumps [slsa-framework/slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/slsa-framework/slsa-github-generator/releases)
- [Changelog](https://github.com/slsa-framework/slsa-github-generator/blob/main/CHANGELOG.md)
- [Commits](https://github.com/slsa-framework/slsa-github-generator/compare/v1.5.0...v1.6.0)

---
updated-dependencies:
- dependency-name: slsa-framework/slsa-github-generator
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-01 16:57:05 +00:00
dependabot[bot]
7e03bef475
Bump actions/setup-python from 4.6.0 to 4.6.1
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](57ded4d7d5...bd6b4b6205)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-01 16:57:00 +00:00
dependabot[bot]
b364f26a11
Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.5 to 1.8.6.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](0bf742be3e...a56da0b891)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-01 16:56:55 +00:00
David Lord
bfceede8ad
remove unused links 2023-06-01 08:09:59 -07:00
David Lord
7e691a0862
update metadata 2023-06-01 08:06:33 -07:00
David Lord
235ecaf576
[pre-commit.ci] pre-commit autoupdate (#1847) 2023-05-02 05:55:07 -07:00
pre-commit-ci[bot]
a24a4b1574
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.3.1 → v3.3.2](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2)
2023-05-02 04:33:39 +00:00
David Lord
953acd65b2
Bump pypa/gh-action-pypi-publish from 1.8.4 to 1.8.5 (#1843) 2023-05-01 10:21:49 -07:00
David Lord
c5685b6dc4
Bump actions/checkout from 3.5.0 to 3.5.2 (#1844) 2023-05-01 10:19:49 -07:00
dependabot[bot]
7018e3fc76
Bump actions/checkout from 3.5.0 to 3.5.2
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.0 to 3.5.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](8f4b7f8486...8e5e7e5ab8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 17:17:29 +00:00
David Lord
2cd9ed2ad9
Bump actions/setup-python from 4.5.0 to 4.6.0 (#1845) 2023-05-01 10:16:55 -07:00
dependabot[bot]
1e357f34ff
Bump actions/setup-python from 4.5.0 to 4.6.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](d27e3f3d7c...57ded4d7d5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 16:57:11 +00:00
dependabot[bot]
055bbfd1fe
Bump pypa/gh-action-pypi-publish from 1.8.4 to 1.8.5
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.4 to 1.8.5.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](29930c9cf5...0bf742be3e)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 16:57:04 +00:00
David Lord
090a5a9e22
fix flake8 bugbear error 2023-04-07 12:19:07 -07:00
David Lord
6fef24ce45
[pre-commit.ci] pre-commit autoupdate (#1831) 2023-04-04 05:14:59 -07:00
pre-commit-ci[bot]
c8c3c846d5
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0)
2023-04-04 06:32:14 +00:00
David Lord
85e5ad85d1
Bump pypa/gh-action-pypi-publish from 1.6.4 to 1.8.4 (#1830) 2023-04-03 06:13:54 -07:00
David Lord
3067df7261
Bump actions/cache from 3.2.6 to 3.3.1 (#1829) 2023-04-03 06:13:47 -07:00
David Lord
23e5cec554
Bump actions/checkout from 3.3.0 to 3.5.0 (#1828) 2023-04-03 06:13:34 -07:00
dependabot[bot]
c48f131143
Bump pypa/gh-action-pypi-publish from 1.6.4 to 1.8.4
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.6.4 to 1.8.4.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](c7f29f7ade...29930c9cf5)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 16:57:53 +00:00
dependabot[bot]
4863d6534f
Bump actions/cache from 3.2.6 to 3.3.1
Bumps [actions/cache](https://github.com/actions/cache) from 3.2.6 to 3.3.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](69d9d449ac...88522ab9f3)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 16:57:47 +00:00
dependabot[bot]
80f4e5586a
Bump actions/checkout from 3.3.0 to 3.5.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.3.0 to 3.5.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](ac59398561...8f4b7f8486)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 16:57:43 +00:00
David Lord
be0bcb61f9
[pre-commit.ci] pre-commit autoupdate (#1822) 2023-03-07 06:59:06 -08:00
David Lord
832bdaadfc
address flake8 findings 2023-03-07 06:55:44 -08:00
pre-commit-ci[bot]
f39ffa0d8a
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/peterdemin/pip-compile-multi: v2.6.1 → v2.6.2](https://github.com/peterdemin/pip-compile-multi/compare/v2.6.1...v2.6.2)
2023-03-07 03:52:21 +00:00
David Lord
9dad679695
Bump slsa-framework/slsa-github-generator from 1.4.0 to 1.5.0 (#1816) 2023-03-01 09:04:52 -08:00
David Lord
47957d571c
Bump actions/cache from 3.2.4 to 3.2.6 (#1815) 2023-03-01 09:02:41 -08:00
dependabot[bot]
7b3cb76e71
Bump slsa-framework/slsa-github-generator from 1.4.0 to 1.5.0
Bumps [slsa-framework/slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/slsa-framework/slsa-github-generator/releases)
- [Changelog](https://github.com/slsa-framework/slsa-github-generator/blob/main/CHANGELOG.md)
- [Commits](https://github.com/slsa-framework/slsa-github-generator/compare/v1.4.0...v1.5.0)

---
updated-dependencies:
- dependency-name: slsa-framework/slsa-github-generator
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-01 16:57:30 +00:00
dependabot[bot]
02e058df6c
Bump actions/cache from 3.2.4 to 3.2.6
Bumps [actions/cache](https://github.com/actions/cache) from 3.2.4 to 3.2.6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](627f0f41f6...69d9d449ac)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-01 16:57:27 +00:00
David Lord
b7cb6ee667
Merge branch '3.1.x' 2023-02-07 07:20:32 -08:00
David Lord
3e07d14a0f
[pre-commit.ci] pre-commit autoupdate (#1800) 2023-02-07 07:12:28 -08:00
pre-commit-ci[bot]
291dfe27d5 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2023-02-07 04:43:17 +00:00
pre-commit-ci[bot]
42b3a35410
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0)
2023-02-07 04:43:06 +00:00
David Lord
37561cead6
Bump actions/cache from 3.2.3 to 3.2.4 (#1799) 2023-02-01 08:59:04 -08:00
David Lord
56f7c4e083
Bump actions/setup-python from 4.4.0 to 4.5.0 (#1798) 2023-02-01 08:58:53 -08:00
dependabot[bot]
c01f51b345
Bump actions/cache from 3.2.3 to 3.2.4
Bumps [actions/cache](https://github.com/actions/cache) from 3.2.3 to 3.2.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](58c146cc91...627f0f41f6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-01 16:01:29 +00:00
dependabot[bot]
1cc0b63e2f
Bump actions/setup-python from 4.4.0 to 4.5.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](5ccb29d877...d27e3f3d7c)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-01 16:01:23 +00:00
David Lord
96a83e9014
Merge branch '3.1.x' 2023-01-20 13:35:30 -08:00
David Lord
795ab3db02
build, provenance, publish workflow (#1794) 2023-01-20 13:34:34 -08:00
David Lord
50a5fd4fb2
move and update flake8 config 2023-01-20 11:07:11 -08:00
David Lord
102ba5d688
build, provenance, publish workflow 2023-01-20 09:02:05 -08:00
David Lord
05a5f8120a
switch to pyproject.toml (#1793) 2023-01-19 18:22:50 -08:00
David Lord
8ed8e1d0ed
fix mypy strict findings 2023-01-19 18:17:03 -08:00
David Lord
614b045fab
ignore bugbear opinion 2023-01-19 18:17:03 -08:00
David Lord
a9c8111d24
switch to pyproject.toml 2023-01-19 18:17:03 -08:00
David Lord
048a068697
Merge remote-tracking branch 'origin/3.1.x' 2023-01-19 17:18:57 -08:00
David Lord
89eec1c5ee
set workflow permissions 2023-01-09 14:51:55 -08:00
David Lord
495b889b7c
Merge branch '3.1.x' 2023-01-09 14:51:38 -08:00
David Lord
623df9b3f2
update tested python versions (#1790) 2023-01-09 14:50:52 -08:00
David Lord
782151081d
update tested python versions
test 3.11 final
test 3.12 dev
update for tox 4
2023-01-09 14:47:15 -08:00
David Lord
80e7a83235
Bump actions/cache from 3.0.11 to 3.2.2 (#1787) 2023-01-03 08:23:02 -07:00
David Lord
e46be11e3e
Bump dessant/lock-threads from 3 to 4 (#1786) 2023-01-03 08:22:53 -07:00
dependabot[bot]
45c23ea56d
Bump actions/cache from 3.0.11 to 3.2.2
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.11 to 3.2.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.11...v3.2.2)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-01 16:01:00 +00:00
dependabot[bot]
e026c72c19
Bump dessant/lock-threads from 3 to 4
Bumps [dessant/lock-threads](https://github.com/dessant/lock-threads) from 3 to 4.
- [Release notes](https://github.com/dessant/lock-threads/releases)
- [Changelog](https://github.com/dessant/lock-threads/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dessant/lock-threads/compare/v3...v4)

---
updated-dependencies:
- dependency-name: dessant/lock-threads
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-01 16:00:56 +00:00
David Lord
36b601f24b
Merge branch '3.1.x' 2022-12-29 10:39:27 -08:00
David Lord
3fadee01b7
update dependencies 2022-12-29 10:39:00 -08:00
David Lord
5b13cea00a
ignore flake8-bugbear B905 (#1784) 2022-12-29 10:56:15 -07:00
Mehdi ABAAKOUK
522391c5bd
ignore flake8-bugbear B905
requires python>=3.10
2022-12-29 09:54:02 -08:00
Kevin Brown-Silva
e0486050d3
[pre-commit.ci] pre-commit autoupdate (#1772) 2022-12-08 09:25:47 -08:00
pre-commit-ci[bot]
7e691ed15b
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.2.0 → v3.3.0](https://github.com/asottile/pyupgrade/compare/v3.2.0...v3.3.0)
- [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0)
- [github.com/peterdemin/pip-compile-multi: v2.5.0 → v2.6.1](https://github.com/peterdemin/pip-compile-multi/compare/v2.5.0...v2.6.1)
- [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0)
2022-12-06 00:35:35 +00:00
pre-commit-ci[bot]
6a8246be1e
[pre-commit.ci] pre-commit autoupdate (#1756)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-07 20:38:40 -08:00
dependabot[bot]
ae53ea5350
Bump actions/cache from 3.0.9 to 3.0.11 (#1750) 2022-11-01 10:23:17 -07:00
pre-commit-ci[bot]
46298e0c6b
[pre-commit.ci] pre-commit autoupdate (#1748) 2022-11-01 06:53:00 -07:00
David Lord
1746bcdfd5
Update templates.rst (#1742) 2022-10-25 07:09:36 -07:00
neilsquinn
3ccc61b0bd
Update templates.rst
Fix typo
2022-10-25 09:33:24 -04:00
David Lord
5d9ece6d65
Merge branch '3.1.x' 2022-10-13 09:12:06 -07:00
David Lord
bf251517c1
Show how {% filter %} can take filter arguments (#1733) 2022-10-13 09:11:38 -07:00
Simon Willison
b1bb29d292
Show how {% filter %} can take filter arguments
Closes #1732
2022-10-13 09:09:49 -07:00
David Lord
0a10079f33
update dev dependencies 2022-10-13 09:08:56 -07:00
dependabot[bot]
e740cc65d5
Bump actions/cache from 3.0.8 to 3.0.9 (#1729) 2022-10-02 07:03:32 -07:00
pre-commit-ci[bot]
c436c9f18f
[pre-commit.ci] pre-commit autoupdate (#1716) 2022-10-02 07:03:21 -07:00
Anton Topchii
9fde7eb820
Update pylons url in documentation (#1715)
Update pylons homepage url in docs
2022-09-02 06:11:15 -07:00
dependabot[bot]
15e4959a2e
Bump actions/cache from 3.0.5 to 3.0.8 (#1713)
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.5 to 3.0.8.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.5...v3.0.8)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-01 09:09:06 -07:00
Tilman Klaeger
997f7f5243
Replacing os.path.sep with os.sep in loader (#1698) 2022-08-01 17:44:14 -07:00
David Lord
c3fdbac68c
Merge pull request #1700 from pallets/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-08-01 16:23:00 -07:00
pre-commit-ci[bot]
9e1895f0e2
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.34.0 → v2.37.3](https://github.com/asottile/pyupgrade/compare/v2.34.0...v2.37.3)
- [github.com/asottile/reorder_python_imports: v3.3.0 → v3.8.2](https://github.com/asottile/reorder_python_imports/compare/v3.3.0...v3.8.2)
- [github.com/PyCQA/flake8: 4.0.1 → 5.0.2](https://github.com/PyCQA/flake8/compare/4.0.1...5.0.2)
- [github.com/peterdemin/pip-compile-multi: v2.4.5 → v2.4.6](https://github.com/peterdemin/pip-compile-multi/compare/v2.4.5...v2.4.6)
2022-08-01 23:18:35 +00:00
David Lord
7f936deac0
Merge pull request #1699 from pallets/dependabot/github_actions/actions/cache-3.0.5
Bump actions/cache from 3.0.4 to 3.0.5
2022-08-01 09:08:36 -07:00
dependabot[bot]
769921b12b
Bump actions/cache from 3.0.4 to 3.0.5
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.4 to 3.0.5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.4...v3.0.5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 16:05:50 +00:00
David Lord
f07a7b5229
Merge pull request #1694 from sdidier-dev/docs-patch
Add in docs/switching/#loops little improvement for consistency
2022-07-22 10:12:03 -07:00
Sébastien DIDIER
c4bff4efee Add in docs/switching/#loops little improvement for consistency 2022-07-22 17:47:56 +02:00
David Lord
4bfc33a276
Merge pull request #1685 from sdidier-dev/docs-patch
Fix typo in docs/api/#custom-tests
2022-07-18 07:01:50 -07:00
Sébastien DIDIER
5e636989f0 Fix typo in docs/api/#custom-tests 2022-07-18 15:35:31 +02:00
David Lord
7fb13bf944
Merge pull request #1679 from pallets/dependabot/github_actions/actions/cache-3.0.4
Bump actions/cache from 3.0.3 to 3.0.4
2022-07-04 15:56:42 -07:00
David Lord
32708a5ce8
Merge pull request #1678 from pallets/dependabot/github_actions/actions/setup-python-4
Bump actions/setup-python from 3 to 4
2022-07-04 15:56:36 -07:00
David Lord
94fd2d9241
Merge branch '3.1.x' 2022-07-04 07:40:11 -07:00
David Lord
e9cb0a5fb5
Merge pull request #1682 from pallets/docs-get_template-example
fix get_template example
2022-07-04 07:39:45 -07:00
David Lord
8fec9b4220
fix get_template example 2022-07-04 07:36:59 -07:00
David Lord
fcefbe7472
Merge pull request #1681 from pallets/update-requirements
Update requirements
2022-07-04 07:35:16 -07:00
David Lord
dd22b7a10e
update requirements 2022-07-04 07:26:57 -07:00
David Lord
6089d142c1
move closure out of loop for bugbear B023 2022-07-04 07:26:15 -07:00
David Lord
033bfd12d7
update pre-commit hooks 2022-07-04 07:25:04 -07:00
dependabot[bot]
bd07dfbf18
Bump actions/cache from 3.0.3 to 3.0.4
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.3 to 3.0.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.3...v3.0.4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-01 16:06:00 +00:00
dependabot[bot]
b8b2c6b445
Bump actions/setup-python from 3 to 4
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-01 16:05:58 +00:00
David Lord
0d177809e2
Merge pull request #1670 from pallets/dependabot/github_actions/actions/cache-3.0.3
Bump actions/cache from 3.0.2 to 3.0.3
2022-06-01 09:17:20 -07:00
dependabot[bot]
5cede152e8
Bump actions/cache from 3.0.2 to 3.0.3
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.2 to 3.0.3.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.2...v3.0.3)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-01 16:06:19 +00:00
David Lord
c9593aa388
Merge pull request #1662 from pallets/dependabot/github_actions/actions/cache-3.0.2
Bump actions/cache from 3.0.1 to 3.0.2
2022-05-01 10:16:36 -06:00
dependabot[bot]
47493d082c
Bump actions/cache from 3.0.1 to 3.0.2
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.1 to 3.0.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.1...v3.0.2)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-01 16:02:59 +00:00
David Lord
1b714c7e82
Merge branch '3.1.x' 2022-04-28 10:22:02 -07:00
David Lord
b08cd4bc64
Merge pull request #1660 from pallets/release-3.1.2
release version 3.1.2
2022-04-28 10:20:01 -07:00
David Lord
1e68ba8617
release version 3.1.2 2022-04-28 10:14:05 -07:00
David Lord
52843b5cbf
Merge branch '3.1.x' 2022-04-28 07:08:26 -07:00
David Lord
8efee35092
pre-commit updates latest release branch 2022-04-28 07:08:16 -07:00
David Lord
a24df26d54
ignore new mypy finding 2022-04-28 07:07:32 -07:00
David Lord
9faee281ea
update requirements 2022-04-28 07:04:19 -07:00
David Lord
b802b5a6ad
Merge pull request #1655 from dvitek/dvitek/issue1654
Fix Race conditions in FileSystemBytecodeCache
2022-04-25 14:14:10 -07:00
David Vitek
746bb95780
Fix race conditions in FileSystemBytecodeCache 2022-04-25 14:09:33 -07:00
David Lord
a0dd7753d0
Merge branch '3.1.x' 2022-04-25 12:41:39 -07:00
David Lord
466a200ea4
update requirements 2022-04-25 12:40:57 -07:00
David Lord
990602f719
Merge pull request #1647 from Tom-Brouwer/202204/add-missing-overlay-options
Add missing environment options to the Environment.overlay method
2022-04-04 06:59:47 -07:00
David Lord
5d3d241471
fix flake8-bugbear finding 2022-04-04 06:56:11 -07:00
Tom Brouwer
21da8f5298
add missing options to overlay from __init__ 2022-04-04 06:40:57 -07:00
David Lord
ea69e41db3
start version 3.1.2 2022-04-04 06:18:39 -07:00
David Lord
c3a61d6ef6
Merge branch '3.1.x' 2022-04-01 12:33:53 -07:00
David Lord
9b521347aa
Merge pull request #1646 from pallets/update-requirements
update requirements
2022-04-01 12:33:42 -07:00
David Lord
8fd4b28a22
update requirements 2022-04-01 12:31:39 -07:00
David Lord
2c8e84db29
Merge pull request #1643 from pallets/dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 2 to 3
2022-04-01 09:08:09 -07:00
David Lord
4f5630f43b
Merge pull request #1644 from pallets/dependabot/github_actions/actions/cache-3.0.1
Bump actions/cache from 2 to 3.0.1
2022-04-01 09:07:10 -07:00
dependabot[bot]
d6b4900742
Bump actions/cache from 2 to 3.0.1
Bumps [actions/cache](https://github.com/actions/cache) from 2 to 3.0.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v2...v3.0.1)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-01 16:04:12 +00:00
dependabot[bot]
a7eedafa2a
Bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-01 16:04:09 +00:00
David Lord
6f79daafe8
Merge branch '3.1.x' 2022-03-25 15:34:33 -07:00
David Lord
7f66a58a96
Merge pull request #1639 from pallets/release-3.1.1
release version 3.1.1
2022-03-25 15:31:46 -07:00
David Lord
d80ffb47e6
release version 3.1.1 2022-03-25 15:27:45 -07:00
David Lord
f3db5f4b40
Merge pull request #1638 from pallets/windows-altsep
normpath on final template filename
2022-03-25 15:26:46 -07:00
David Lord
1b02fccaf4
normpath on final template filename 2022-03-25 15:23:00 -07:00
David Lord
155e51d90b
start version 3.1.1 2022-03-25 15:05:58 -07:00
David Lord
e542e10002
start version 3.2.0 2022-03-24 07:43:09 -07:00
96 changed files with 3625 additions and 2117 deletions

View 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"
}

View 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

View File

@ -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

View File

@ -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.
-->

View File

@ -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.

View File

@ -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?
-->

View File

@ -1,9 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
day: "monday"
time: "16:00"
timezone: "UTC"

View File

@ -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.

View File

@ -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
View 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
View 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/

View File

@ -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
View File

@ -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/

View File

@ -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

View File

@ -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

View File

@ -1,5 +1,122 @@
.. 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
-------------
Released 2022-03-25
- The template filename on Windows uses the primary path separator.
:issue:`1637`
Version 3.1.0
-------------
@ -89,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
@ -943,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

View File

@ -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

View File

@ -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/>`__.

View File

@ -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

View File

@ -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
View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

11
docs/_static/jinja-logo.svg vendored Normal file
View 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
View 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

View File

@ -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`.

View File

@ -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")]

View File

@ -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"\\?[()]")

View File

@ -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
~~~~~~~~~~~~~~~~~~~

View File

@ -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/

View File

@ -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

View File

@ -91,4 +91,4 @@ this add this to ``config/environment.py``:
config['pylons.strict_c'] = True
.. _Pylons: https://pylonshq.com/
.. _Pylons: https://pylonsproject.org/

View File

@ -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

View File

@ -1,4 +1,5 @@
BSD-3-Clause License
====================
.. include:: ../LICENSE.rst
.. literalinclude:: ../LICENSE.txt
:language: text

View File

@ -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
---

View File

@ -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 %}

View File

@ -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.

View File

@ -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::

View File

@ -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
View 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}]]

View File

@ -1,6 +0,0 @@
-r docs.in
-r tests.in
-r typing.in
pip-compile-multi
pre-commit
tox

View File

@ -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

View File

@ -1,4 +0,0 @@
Pallets-Sphinx-Themes
Sphinx
sphinx-issues
sphinxcontrib-log-cabinet

View File

@ -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

View File

@ -1 +0,0 @@
pytest

View File

@ -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

View File

@ -1 +0,0 @@
mypy

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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"]},
)

View File

@ -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.0"
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)

View File

@ -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
)

View File

@ -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)]

View File

@ -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

View File

@ -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(")

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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]":

View File

@ -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():

View File

@ -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

View File

@ -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,32 +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
return contents, 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)
@ -224,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:]
@ -234,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.
@ -273,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
@ -294,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.
@ -311,28 +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.
p = posixpath.join(self._template_root, *split_template_path(template))
up_to_date: t.Optional[t.Callable[[], bool]]
# 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.Callable[[], bool] | None
if self._archive is None:
# Package is a directory.
@ -361,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
@ -403,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:
@ -411,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)
@ -443,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:
@ -487,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]
@ -497,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)
@ -511,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:
@ -521,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():
@ -548,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)
@ -561,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:
@ -570,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())
@ -586,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`.
"""
@ -597,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}"
@ -633,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}"

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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(

View File

@ -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:

View File

@ -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__,
)

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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.
"""

View File

@ -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."""

View File

@ -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

View File

@ -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 == "["

View File

@ -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"

View File

@ -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()

View File

@ -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(

View File

@ -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():

View File

@ -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: &lt;test&gt;</strong>"
assert t.render(ae=False) == "<strong>Wert: <test></strong>"

View File

@ -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="&lt;?&gt;"' 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)

View File

@ -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 %}&nbsp;{% 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

View File

@ -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 %}"

View File

@ -2,8 +2,6 @@ import importlib.abc
import importlib.machinery
import importlib.util
import os
import platform
import posixpath
import shutil
import sys
import tempfile
@ -172,9 +170,37 @@ class TestFileSystemLoader:
t = e.get_template("mojibake.txt")
assert t.render() == expect
def test_filename_normpath(self):
"""Nested template names should only contain ``os.sep`` in the
loaded filename.
"""
loader = loaders.FileSystemLoader(self.searchpath)
e = Environment(loader=loader)
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 = []
@ -188,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)
@ -304,7 +331,7 @@ def package_dir_loader(monkeypatch):
def test_package_dir_source(package_dir_loader, template, expect):
source, name, up_to_date = package_dir_loader.get_source(None, template)
assert source.rstrip() == expect
assert name.endswith(posixpath.join(*split_template_path(template)))
assert name.endswith(os.path.join(*split_template_path(template)))
assert up_to_date()
@ -326,7 +353,7 @@ def package_file_loader(monkeypatch):
def test_package_file_source(package_file_loader, template, expect):
source, name, up_to_date = package_file_loader.get_source(None, template)
assert source.rstrip() == expect
assert name.endswith(posixpath.join(*split_template_path(template)))
assert name.endswith(os.path.join(*split_template_path(template)))
assert up_to_date()
@ -349,13 +376,13 @@ def package_zip_loader(monkeypatch):
def test_package_zip_source(package_zip_loader, template, expect):
source, name, up_to_date = package_zip_loader.get_source(None, template)
assert source.rstrip() == expect
assert name.endswith(posixpath.join(*split_template_path(template)))
assert name.endswith(os.path.join(*split_template_path(template)))
assert up_to_date is None
@pytest.mark.xfail(
platform.python_implementation() == "PyPy",
reason="PyPy's zipimporter doesn't have a '_files' attribute.",
sys.implementation.name == "pypy",
reason="zipimporter doesn't have a '_files' attribute",
raises=TypeError,
)
def test_package_zip_list(package_zip_loader):
@ -402,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")

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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&lt;foo&gt;"
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()

View File

@ -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
View File

@ -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

1244
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff