Skip to content

Commit 28446ed

Browse files
Technologicatclaude
andcommitted
Support Python 3.15
The macros needed no changes. `lazify`, `autocurry` and `tco` have no comprehension-specific handling, so PEP 798's new forms reach their generic paths — a `Starred` in the element position, and a `DictComp` whose `value` is `None` because the mapping expression sits in `key`. All of them pass through correctly, including the hazard worth worrying about: `lazify` does not wrap the starred value in a promise, which would have failed at unpacking. The existing suite cannot catch a regression in any of this, since nothing else in it contains the syntax — hence a version-gated test module, with an ordinary `k: v` comprehension as a control for the `value` field the unpacking form leaves empty. Suite is green on 3.15.0rc1 (3871) and on 3.14.6 (3883, skipping the new module). The test file has to be excluded from ruff: it cannot silence a syntax error with `# noqa`, and ruff 0.15.10 both rejects the list form as newer than `target-version` and cannot parse the dict, set and generator forms at all. No CI matrix entry yet, deliberately. `mcpyrate` 4.2.0 as published cannot import anything under 3.15, so a 3.15 job would resolve that from PyPI and fail for reasons unrelated to this code. It lands with the dependency bump once mcpyrate 4.2.1 ships; see the brief. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5da3ca3 commit 28446ed

4 files changed

Lines changed: 117 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
**Changed**:
66

77
- `env.finalize()` now returns `self` instead of `None`, so it can be chained: `e = env(x=42).finalize()`. Matches the existing instance passthrough on `<<`.
8+
- **Python 3.15 is supported**, and `requires-python` moves from `<3.15` to `<3.16`. The macros needed no changes for the new syntax: `lazify`, `autocurry` and `tco` all pass through comprehension unpacking (`{**mapping for x in xs}`, `[*items for item in xs]`, and the set and generator forms) correctly, and there are now tests to keep it that way. Requires `mcpyrate` 4.2.1 or newer, since earlier versions cannot import anything at all under 3.15.
89

910
**Fixed**:
1011

‎briefs/python-3.15-support.md‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,24 @@ kept rather than rediscovered, and move the version metadata.
9090

9191
## Work items
9292

93-
Items 1-3 are settled as above and need tests rather than fixes. Python 3.15.0rc1 is installed on the personal machine.
93+
Items 1-3 are settled as above and need tests rather than fixes. Python 3.15.0rc1 is installed on both machines.
94+
95+
**Release ordering is forced, and the CI matrix has to wait for it.** `unpythonic` declares
96+
`mcpyrate>=4.2.0`, and **mcpyrate 4.2.0 as published cannot import any module under 3.15** —
97+
verified by installing it into a clean 3.15 venv, where importing an ordinary module dies with
98+
`TypeError: source_to_xcode() takes 3 positional arguments but 4 were given`. So a 3.15 job in
99+
`unpythonic`'s CI would resolve `mcpyrate` from PyPI, get 4.2.0, and fail for reasons that have
100+
nothing to do with the code under test. The sequence is therefore:
101+
102+
1. Land unpythonic's code changes — tests, `requires-python` cap, classifier. No CI matrix entry.
103+
2. Release **mcpyrate 4.2.1** (its work is already done and waiting).
104+
3. In `unpythonic`, bump the pin to `mcpyrate>=4.2.1` **and** add `"3.15"` to the CI matrix with
105+
`allow-prereleases: true`, in one commit. Only now can that job pass.
106+
4. Release **unpythonic 2.3.1**.
107+
108+
"Released together" therefore means same sitting, verified against each other — not simultaneous.
109+
The verification itself is already done: unpythonic's suite was run against the working-tree
110+
mcpyrate and passed 3862/3862.
94111

95112
1. **`lazify` with a `Starred` comprehension element.** `lazify.py` has no comprehension-specific handling at all, and its `Starred` handling is scoped to call arguments (line 537) and container literals (line 770). A `Starred` in `elt` position is a new shape reaching the generic path. The hazard is wrapping the starred value in a promise, since `*promise` fails at unpacking. Test `with lazify:` over all four new comprehension forms.
96113
2. **`autocurry` and `tailtools` over the same forms.** Same question, same reason; `tailtools.py:1011,1026` already reasons about `Starred` in a different context.

‎pyproject.toml‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description = "Supercharge your Python with parts of Lisp and Haskell."
44
authors = [
55
{ name = "Juha Jeronen", email = "juha.m.jeronen@gmail.com" },
66
]
7-
requires-python = ">=3.10,<3.15"
7+
requires-python = ">=3.10,<3.16"
88

99
# the `read` function and long_description_content_type from setup.py are no longer needed,
1010
# modern build tools like pdm/hatch already know how to handle markdown if you point them at a .md file
@@ -38,6 +38,7 @@ classifiers = [
3838
"Programming Language :: Python :: 3.12",
3939
"Programming Language :: Python :: 3.13",
4040
"Programming Language :: Python :: 3.14",
41+
"Programming Language :: Python :: 3.15",
4142
"Programming Language :: Python :: Implementation :: CPython",
4243
"Programming Language :: Python :: Implementation :: PyPy",
4344
"Topic :: Software Development :: Libraries",
@@ -96,6 +97,12 @@ exclude = [
9697
"build",
9798
"dist",
9899
".venv",
100+
# PEP 798 comprehension unpacking, which ruff 0.15.10 cannot handle, and a syntax
101+
# error cannot be silenced with `# noqa`. Two separate obstacles: it rejects the
102+
# list form as too new for `target-version` above, and it cannot parse the dict,
103+
# set and generator forms at all ("Expected `}`, found `for`"). Retry dropping
104+
# this once ruff both parses those and is told a new enough target.
105+
"unpythonic/syntax/tests/test_comprehension_unpacking_3_15.py",
99106
]
100107

101108
[tool.ruff.lint]
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# -*- coding: utf-8 -*-
2+
"""The macros pass through Python 3.15's new comprehension unpacking (PEP 798).
3+
4+
These tests require Python 3.15+ because the unpacking syntax won't parse on
5+
earlier versions.
6+
7+
`lazify`, `autocurry` and `tco` have no comprehension-specific handling, so the
8+
new forms reach their generic paths. Two shapes are new there: a `Starred` in the
9+
element position of a list/set/generator comprehension, and a `DictComp` whose
10+
`value` is `None` because the mapping expression sits in `key` instead.
11+
12+
The hazard worth guarding against is specific to `lazify`: if it wrapped the
13+
starred value in a promise, `*promise` would fail at unpacking. It does not, and
14+
these tests are here to keep it that way — the existing suite cannot catch a
15+
regression here, since nothing else in it contains this syntax.
16+
17+
TODO: Merge into the per-macro test modules when the floor bumps to Python 3.15+.
18+
"""
19+
20+
from ...syntax import macros, test, the # noqa: F401
21+
from ...test.fixtures import session, testset
22+
23+
from ...syntax import macros, lazify, autocurry, tco # noqa: F401, F811
24+
25+
26+
def _items(k):
27+
return [k, k]
28+
29+
30+
def _mapping(k):
31+
return {k: k}
32+
33+
34+
with lazify:
35+
def lazy_starred_list(ks):
36+
return [*_items(k) for k in ks]
37+
38+
def lazy_starred_set(ks):
39+
return {*_items(k) for k in ks}
40+
41+
def lazy_starred_genexpr(ks):
42+
return list((*_items(k) for k in ks))
43+
44+
def lazy_dict_unpacking(ks):
45+
return {**_mapping(k) for k in ks}
46+
47+
def lazy_ordinary_dictcomp(ks):
48+
return {k: _items(k) for k in ks}
49+
50+
51+
with autocurry:
52+
def curried_starred_list(ks):
53+
return [*_items(k) for k in ks]
54+
55+
def curried_dict_unpacking(ks):
56+
return {**_mapping(k) for k in ks}
57+
58+
59+
with tco:
60+
def tco_starred_list(ks):
61+
return [*_items(k) for k in ks]
62+
63+
def tco_dict_unpacking(ks):
64+
return {**_mapping(k) for k in ks}
65+
66+
67+
def runtests():
68+
with testset("lazify: starred comprehension elements"):
69+
test[lazy_starred_list([1, 2]) == [1, 1, 2, 2]]
70+
test[lazy_starred_set([1, 2]) == {1, 2}]
71+
test[lazy_starred_genexpr([1, 2]) == [1, 1, 2, 2]]
72+
73+
with testset("lazify: dict-unpacking comprehension"):
74+
test[lazy_dict_unpacking([1, 2]) == {1: 1, 2: 2}]
75+
# Control: the `k: v` form goes through the `value` field the unpacking
76+
# form leaves empty, so this catches a fix that skipped that field.
77+
test[lazy_ordinary_dictcomp([1, 2]) == {1: [1, 1], 2: [2, 2]}]
78+
79+
with testset("autocurry: new comprehension forms"):
80+
test[curried_starred_list([1, 2]) == [1, 1, 2, 2]]
81+
test[curried_dict_unpacking([1, 2]) == {1: 1, 2: 2}]
82+
83+
with testset("tco: new comprehension forms"):
84+
test[tco_starred_list([1, 2]) == [1, 1, 2, 2]]
85+
test[tco_dict_unpacking([1, 2]) == {1: 1, 2: 2}]
86+
87+
88+
if __name__ == '__main__': # pragma: no cover
89+
with session(__file__):
90+
runtests()

0 commit comments

Comments
 (0)