Skip to content

Commit 01b7c8c

Browse files
Add support for Python 3.14 (#312)
1 parent df62359 commit 01b7c8c

File tree

6 files changed

+140
-34
lines changed

6 files changed

+140
-34
lines changed

.readthedocs.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ version: 2
66
build:
77
os: ubuntu-22.04
88
tools:
9-
python: "3.13"
10-
rust: "latest"
9+
python: latest
10+
rust: latest
1111
commands:
1212
- asdf plugin add just && asdf install just latest && asdf global just latest
1313
- asdf plugin add uv && asdf install uv latest && asdf global uv latest

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
### Added
22+
23+
- Added support for Python 3.14
24+
2125
## [5.2.2]
2226

2327
### Added

CONTRIBUTING.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,81 @@ Not all contributions need to start with an issue, such as typo fixes in documen
88

99
We adhere to Django's Code of Conduct in all interactions and expect all contributors to do the same. Please read the [Code of Conduct](https://www.djangoproject.com/conduct/) before contributing.
1010

11+
## Development
12+
13+
### Version Updates
14+
15+
#### Python
16+
17+
The project uses [`noxfile.py`](noxfile.py) as the single source of truth for supported Python versions. The `PY_VERSIONS` list in this file controls:
18+
19+
- **Auto-generated documentation**: [cogapp](https://nedbatchelder.com/code/cog/) reads `PY_VERSIONS` to generate Python version classifiers in [`pyproject.toml`](pyproject.toml) and the supported versions list in [`README.md`](README.md)
20+
- **CI/CD test matrix**: GitHub Actions workflows call the `gha_matrix` nox session to dynamically generate the test matrix from `PY_VERSIONS`, ensuring all supported Python versions are tested automatically
21+
- **Local testing**: The `tests` nox session uses `PY_VERSIONS` to parametrize test runs across all supported Python versions
22+
23+
> [!NOTE]
24+
> When possible, prefer submitting additions and removals in separate pull requests. This makes it easier to review changes and track the impact of each version update independently.
25+
26+
**To update the list of supported Python versions:**
27+
28+
1. Update [`noxfile.py`](noxfile.py), adding or removing version constants as needed and updating the `PY_VERSIONS` list accordingly.
29+
30+
For example, given the following versions:
31+
32+
```python
33+
PY39 = "3.9"
34+
PY310 = "3.10"
35+
PY311 = "3.11"
36+
PY312 = "3.12"
37+
PY313 = "3.13"
38+
PY_VERSIONS = [PY39, PY310, PY311, PY312, PY313]
39+
```
40+
41+
To add Python 3.14 and remove Python 3.9, the final list will be:
42+
43+
```python
44+
PY310 = "3.10"
45+
PY311 = "3.11"
46+
PY312 = "3.12"
47+
PY313 = "3.13"
48+
PY314 = "3.14"
49+
PY_VERSIONS = [PY310, PY311, PY312, PY313, PY314]
50+
```
51+
52+
2. Regenerate auto-generated content:
53+
54+
```bash
55+
just cog
56+
```
57+
58+
This updates:
59+
60+
- The `requires-python` field in [`pyproject.toml`](pyproject.toml)
61+
- Python version trove classifiers in [`pyproject.toml`](pyproject.toml)
62+
- Supported versions list in [`README.md`](README.md)
63+
64+
3. Update the lock file:
65+
66+
```bash
67+
uv lock
68+
```
69+
70+
4. Test the changes:
71+
72+
```bash
73+
just testall
74+
```
75+
76+
Use `just testall` rather than `just test` to ensure all Python versions are tested. The `just test` command only runs against the default versions (the oldest supported Python and Django LTS) and won't catch issues with newly added versions.
77+
78+
If you want, you can also test only a specific Python version across all Django versions by `nox` directly:
79+
80+
```bash
81+
nox --python 3.14 --session tests
82+
```
83+
84+
5. Update [`CHANGELOG.md`](CHANGELOG.md), adding entries for any versions added or removed.
85+
1186
### `Justfile`
1287

1388
The repository includes a [`Justfile`](./Justfile) that provides all common development tasks with a consistent interface. Running `just` without arguments shows all available commands and their descriptions.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ django_versions = [
6363
cog.outl(f"- Python {', '.join(PY_VERSIONS)}")
6464
cog.outl(f"- Django {', '.join(django_versions)}")
6565
]]] -->
66-
- Python 3.9, 3.10, 3.11, 3.12, 3.13
66+
- Python 3.9, 3.10, 3.11, 3.12, 3.13, 3.14
6767
- Django 4.2, 5.1, 5.2, 6.0
6868
<!-- [[[end]]] -->
6969

noxfile.py

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import os
55
import re
66
import shutil
7+
import tempfile
78
from pathlib import Path
89

910
import nox
11+
from nox.command import CommandFailed
1012

1113
nox.options.default_venv_backend = "uv|virtualenv"
1214
nox.options.reuse_existing_virtualenvs = True
@@ -16,7 +18,8 @@
1618
PY311 = "3.11"
1719
PY312 = "3.12"
1820
PY313 = "3.13"
19-
PY_VERSIONS = [PY39, PY310, PY311, PY312, PY313]
21+
PY314 = "3.14"
22+
PY_VERSIONS = [PY39, PY310, PY311, PY312, PY313, PY314]
2023
PY_DEFAULT = PY_VERSIONS[0]
2124
PY_LATEST = PY_VERSIONS[-1]
2225

@@ -114,21 +117,51 @@ def tests(session, django):
114117

115118
@nox.session
116119
def lint(session):
117-
session.run(
118-
"uv",
119-
"run",
120-
"--no-project",
121-
"--with",
122-
"pre-commit-uv",
123-
"--python",
124-
PY_LATEST,
125-
"pre-commit",
126-
"run",
127-
"--all-files",
128-
"--show-diff-on-failure",
129-
"--color",
130-
"always",
131-
)
120+
for python_version in reversed(PY_VERSIONS):
121+
with tempfile.TemporaryFile(mode="w+") as output_file:
122+
try:
123+
session.run(
124+
"uv",
125+
"run",
126+
"--no-project",
127+
"--with",
128+
"pre-commit-uv",
129+
"--python",
130+
python_version,
131+
"pre-commit",
132+
"run",
133+
"--all-files",
134+
"--show-diff-on-failure",
135+
"--color",
136+
"always",
137+
stdout=output_file,
138+
stderr=output_file,
139+
)
140+
output_file.seek(0)
141+
output = output_file.read().rstrip("\n")
142+
if output:
143+
print(output)
144+
break
145+
except CommandFailed as e:
146+
# Parse exit code from exception reason: "Returned code X"
147+
match = re.search(r"Returned code (\d+)", e.reason or "")
148+
exit_code = int(match.group(1)) if match else None
149+
150+
# Only retry on exit code 3 (infrastructure error)
151+
if exit_code == 3:
152+
session.log(
153+
f"Linting with Python {python_version} failed due to pre-commit infrastructure issue (exit code 3), trying next version"
154+
)
155+
continue
156+
else:
157+
# Real lint failure (exit code 1) or unknown error - re-raise
158+
output_file.seek(0)
159+
error_output = output_file.read().rstrip("\n")
160+
if error_output:
161+
print(error_output)
162+
raise
163+
else:
164+
session.error("Linting failed with all Python versions")
132165

133166

134167
@nox.session
@@ -221,19 +254,6 @@ def cog(session):
221254
"-r",
222255
*COG_FILES,
223256
)
224-
git_status = session.run("git", "status", "--porcelain", external=True, silent=True)
225-
if not any(cog_file in git_status for cog_file in COG_FILES):
226-
session.log("No changes to documentation files, skipping commit")
227-
return
228-
session.run("git", "add", *COG_FILES, external=True)
229-
session.run(
230-
"git",
231-
"commit",
232-
"-m",
233-
"auto-regenerate docs using cog",
234-
external=True,
235-
silent=True,
236-
)
237257

238258

239259
@nox.session

pyproject.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,15 @@ readme = "README.md"
2424
authors = [
2525
{ name = "Josh Thomas", email = "[email protected]" }
2626
]
27+
# [[[cog
28+
# import cog
29+
#
30+
# from noxfile import PY_DEFAULT
31+
#
32+
# cog.outl(f'requires-python = ">={PY_DEFAULT}"')
33+
# ]]] -->
2734
requires-python = ">=3.9"
35+
# [[[end]]]
2836
classifiers = [
2937
"Development Status :: 4 - Beta",
3038
"Framework :: Django",
@@ -65,6 +73,7 @@ classifiers = [
6573
"Programming Language :: Python :: 3.11",
6674
"Programming Language :: Python :: 3.12",
6775
"Programming Language :: Python :: 3.13",
76+
"Programming Language :: Python :: 3.14",
6877
# [[[end]]]
6978
"Programming Language :: Python :: Implementation :: CPython",
7079
"Programming Language :: Rust",
@@ -143,8 +152,6 @@ extend-include = ["*.pyi?"]
143152
indent-width = 4
144153
# Same as Black.
145154
line-length = 88
146-
# Assume Python 3.9
147-
target-version = "py39"
148155

149156
[tool.ruff.format]
150157
# Like Black, indent with spaces, rather than tabs.

0 commit comments

Comments
 (0)