Skip to content

Commit 863138d

Browse files
nstarmanwesselb
andauthored
Enable py3.14 and deprecate some alias functionality (#225)
* test: py3.14 Signed-off-by: nstarman <nstarman@users.noreply.github.com> * feat: deprecate union aliasing Signed-off-by: nstarman <nstarman@users.noreply.github.com> * refactor: some simplifications Signed-off-by: nstarman <nstarman@users.noreply.github.com> * feat: union alias in python 3.14 Signed-off-by: nstarman <nstarman@users.noreply.github.com> * feat: de-deprecate (de)activate_union_aliases Signed-off-by: nstarman <nstarman@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Wessel <wessel.p.bruinsma@gmail.com> Co-authored-by: Nathaniel Starkman <nstarman@users.noreply.github.com> Signed-off-by: nstarman <nstarman@users.noreply.github.com> --------- Signed-off-by: nstarman <nstarman@users.noreply.github.com> Co-authored-by: Wessel <wessel.p.bruinsma@gmail.com>
1 parent ad54d37 commit 863138d

14 files changed

Lines changed: 828 additions & 338 deletions

.github/workflows/ci.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,18 @@ jobs:
2828
- name: "3.12"
2929
python-version: "3.12"
3030
extra-install: ""
31-
- name: "3.12-pre-beartype"
32-
python-version: "3.12"
33-
extra-install: "uv pip install --upgrade --pre beartype"
3431
- name: "3.13"
3532
python-version: "3.13"
3633
extra-install: ""
3734
- name: "3.13-pre-beartype"
3835
python-version: "3.13"
3936
extra-install: "uv pip install --upgrade --pre beartype"
37+
- name: "3.14"
38+
python-version: "3.14"
39+
extra-install: ""
40+
- name: "3.14-pre-beartype"
41+
python-version: "3.14"
42+
extra-install: "uv pip install --upgrade --pre beartype"
4043

4144
name: Test ${{ matrix.value.name }}
4245
steps:

.pre-commit-config.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ ci:
22
autoupdate_commit_msg: "chore: update pre-commit hooks"
33
autofix_commit_msg: "style: pre-commit fixes"
44

5-
default_language_version:
6-
python: "3.10"
7-
85
repos:
96
- repo: meta
107
hooks:

docs/comparison.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,27 @@ def f(x: int, y: Number):
8585
return "second"
8686
```
8787

88+
% invisible-code-block: python
89+
%
90+
% import sys
91+
92+
% skip: start if(sys.version_info < (3, 14), reason="Union repr changed in Python 3.14+")
93+
94+
```python
95+
>>> try: f(1, 1)
96+
... except Exception as e: print(f"{type(e).__name__}: {e}")
97+
AmbiguousLookupError: `f(1, 1)` is ambiguous.
98+
Candidates:
99+
f(x: int | numbers.Number, y: int)
100+
<function f at ...> @ ...
101+
f(x: int, y: numbers.Number)
102+
<function f at ...> @ ...
103+
```
104+
105+
% skip: end
106+
107+
% skip: start if(sys.version_info >= (3, 14), reason="Union repr changed in Python 3.14+")
108+
88109
```python
89110
>>> try: f(1, 1)
90111
... except Exception as e: print(f"{type(e).__name__}: {e}")
@@ -96,6 +117,8 @@ Candidates:
96117
<function f at ...> @ ...
97118
```
98119

120+
% skip: end
121+
99122
Just to sanity check that things are indeed working correctly:
100123

101124
```python

docs/union_aliases.md

Lines changed: 157 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,157 @@
1-
(union-aliases)=
2-
# Union Aliases
3-
4-
To understand what union aliases are and what problem they solve, consider the
5-
following example.
6-
Suppose that we would want to implement a special addition function, and we would
7-
want to implement it for all NumPy scalar types:
8-
9-
```python
10-
import numpy as np
11-
12-
from typing import Union
13-
from plum import dispatch
14-
15-
16-
scalar_types = tuple(np.sctypeDict.values()) # All NumPy scalar types
17-
Scalar = Union[scalar_types] # Union of all NumPy scalar types
18-
19-
20-
@dispatch
21-
def add(x: Scalar, y: Scalar):
22-
return x + y
23-
```
24-
25-
This looks all fine, until you look at the documentation.
26-
In particular, `help(add)` prints
27-
28-
29-
```
30-
Help on Function in module __main__:
31-
32-
add(x: Union[numpy.int8, numpy.int16, numpy.int32, numpy.int64, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, numpy.float16, numpy.float32, numpy.float64, numpy.float128, numpy.complex64, numpy.complex128, numpy.complex256, bool, object, bytes, str, numpy.void], y: Union[numpy.int8, numpy.int16, numpy.int32, numpy.int64, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, numpy.float16, numpy.float32, numpy.float64, numpy.float128, numpy.complex64, numpy.complex128, numpy.complex256, bool, object, bytes, str, numpy.void])
33-
```
34-
35-
While the documentation is accurate, it is not at all helpful to expand the union in
36-
its many elements, because it obscures the key message: `add(x, y)` is implemented
37-
for all _scalars_.
38-
A better option would be to print `add(x: Scalar, y: Scalar)`.
39-
This is precisely what union aliases do:
40-
by aliasing a union, you change the way it is displayed.
41-
Union aliases must be activated explicitly, because the feature
42-
monkeypatches `Union.__str__` and `Union.__repr__`.
43-
44-
```python
45-
>>> from plum import activate_union_aliases, set_union_alias
46-
47-
>>> activate_union_aliases()
48-
49-
>>> set_union_alias(Scalar, alias="Scalar")
50-
typing.Union[Scalar]
51-
```
52-
53-
After this, `help(add)` now prints the following:
54-
55-
% skip: next "Example"
56-
57-
```python
58-
Help on Function in module __main__:
59-
60-
add(x: Union[Scalar], y: Union[Scalar])
61-
```
62-
63-
Hurray!
64-
Note that the documentation prints `Union[Scalar]` rather than just `Scalar`.
65-
This is intentional: it is to prevent breaking code that depends on how unions
66-
print.
67-
For example, printing just `Scalar` would omit the type parameter(s).
68-
69-
Let's see with a few more examples how this works:
70-
71-
```python
72-
>>> Scalar
73-
typing.Union[Scalar]
74-
75-
>>> Union[tuple(scalar_types)]
76-
typing.Union[Scalar]
77-
78-
>>> Union[tuple(scalar_types) + (tuple,)] # Scalar or tuple
79-
typing.Union[Scalar, tuple]
80-
81-
>>> Union[tuple(scalar_types) + (tuple, list)] # Scalar or tuple or list
82-
typing.Union[Scalar, tuple, list]
83-
```
84-
85-
If we don't include all of `scalar_types`, we won't see `Scalar`, as desired:
86-
87-
% invisible-code-block: python
88-
%
89-
% import sys
90-
91-
% skip: next "Result depends on NumPy version."
92-
93-
```python
94-
>>> Union[tuple(scalar_types[:-1])]
95-
typing.Union[numpy.int8, numpy.int16, numpy.int32, numpy.longlong, numpy.int64, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, numpy.ulonglong, numpy.float16, numpy.float32, numpy.float64, numpy.longdouble, numpy.complex64, numpy.complex128, numpy.clongdouble, numpy.str_, numpy.bytes_, numpy.void, numpy.bool]
96-
```
97-
98-
You can deactivate union aliases with `deactivate_union_aliases`:
99-
100-
```python
101-
>>> from plum import deactivate_union_aliases
102-
103-
>>> deactivate_union_aliases()
104-
105-
% skip: next "Result depends on NumPy version."
106-
>>> Scalar
107-
typing.Union[numpy.int8, numpy.int16, numpy.int32, numpy.longlong, numpy.int64, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, numpy.ulonglong, numpy.float16, numpy.float32, numpy.float64, numpy.longdouble, numpy.complex64, numpy.complex128, numpy.clongdouble, numpy.str_, numpy.bytes_, numpy.void, numpy.bool, numpy.object_]
108-
```
1+
(union-aliases)=
2+
# Union Aliases
3+
4+
To understand what union aliases are and what problem they solve, consider the
5+
following example.
6+
Suppose that we would want to implement a special addition function, and we would
7+
want to implement it for all NumPy scalar types:
8+
9+
```python
10+
import numpy as np
11+
12+
from typing import Union
13+
from plum import dispatch
14+
15+
16+
scalar_types = tuple(np.sctypeDict.values()) # All NumPy scalar types
17+
Scalar = Union[scalar_types] # Union of all NumPy scalar types
18+
19+
20+
@dispatch
21+
def add(x: Scalar, y: Scalar):
22+
return x + y
23+
```
24+
25+
This looks all fine, until you look at the documentation.
26+
In particular, `help(add)` prints
27+
28+
29+
```
30+
Help on Function in module __main__:
31+
32+
add(x: Union[numpy.int8, numpy.int16, numpy.int32, numpy.int64, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, numpy.float16, numpy.float32, numpy.float64, numpy.float128, numpy.complex64, numpy.complex128, numpy.complex256, bool, object, bytes, str, numpy.void], y: Union[numpy.int8, numpy.int16, numpy.int32, numpy.int64, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, numpy.float16, numpy.float32, numpy.float64, numpy.float128, numpy.complex64, numpy.complex128, numpy.complex256, bool, object, bytes, str, numpy.void])
33+
```
34+
35+
While the documentation is accurate, it is not at all helpful to expand the
36+
union in its many elements, because it obscures the key message: `add(x, y)` is
37+
implemented for all _scalars_. A better option would be to print `add(x:
38+
Scalar, y: Scalar)`. This is precisely what union aliases do: by aliasing a
39+
union, you change the way it is displayed. On Python 3.13 and earlier, union
40+
aliases work by monkeypatching `typing.Union.__str__` and
41+
`typing.Union.__repr__`, and therefore must be activated explicitly. On Python
42+
3.14 and later, `typing.Union`'s representation can no longer be monkeypatched;
43+
union aliases instead only affect how Plum formats unions in its own printed
44+
output.
45+
46+
% invisible-code-block: python
47+
%
48+
% import sys
49+
50+
% skip: start if(sys.version_info < (3, 14), reason="Union repr changed in Python 3.14+")
51+
52+
```python
53+
>>> from plum import set_union_alias
54+
55+
>>> set_union_alias(Scalar, alias="Scalar")
56+
numpy.bool | numpy.float16 | ...
57+
```
58+
59+
% skip: end
60+
61+
% skip: start if(sys.version_info >= (3, 14), reason="Representation of unions changed in Python 3.14.")
62+
63+
```python
64+
>>> from plum import activate_union_aliases, set_union_alias
65+
66+
>>> activate_union_aliases()
67+
68+
>>> set_union_alias(Scalar, alias="Scalar")
69+
typing.Union[Scalar]
70+
```
71+
72+
% skip: end
73+
74+
After this, `help(add)` now prints the following:
75+
76+
% skip: next "Example"
77+
78+
```python
79+
Help on Function in module __main__:
80+
81+
add(x: Union[Scalar], y: Union[Scalar])
82+
```
83+
84+
Hurray!
85+
Note that the documentation prints `Union[Scalar]` rather than just `Scalar`.
86+
This is intentional: it is to prevent breaking code that depends on how unions
87+
print.
88+
For example, printing just `Scalar` would omit the type parameter(s).
89+
90+
Let's see with a few more examples how this works:
91+
92+
% invisible-code-block: python
93+
%
94+
% import sys
95+
96+
% skip: start if(sys.version_info < (3, 14), reason="Representation of unions changed in Python 3.14.")
97+
98+
```python
99+
>>> Scalar
100+
numpy.bool | numpy.float16 | ...
101+
102+
>>> Union[tuple(scalar_types)]
103+
numpy.bool | numpy.float16 | ...
104+
105+
>>> Union[tuple(scalar_types) + (tuple,)] # Scalar or tuple
106+
numpy.bool | numpy.float16 | ... | tuple
107+
108+
>>> Union[tuple(scalar_types) + (tuple, list)] # Scalar or tuple or list
109+
numpy.bool | numpy.float16 | ... | tuple | list
110+
```
111+
112+
% skip: end
113+
114+
% skip: start if(sys.version_info >= (3, 14), reason="Representation of unions changed in Python 3.14.")
115+
116+
```python
117+
>>> Scalar
118+
typing.Union[Scalar]
119+
120+
>>> Union[tuple(scalar_types)]
121+
typing.Union[Scalar]
122+
123+
>>> Union[tuple(scalar_types) + (tuple,)] # Scalar or tuple
124+
typing.Union[Scalar, tuple]
125+
126+
>>> Union[tuple(scalar_types) + (tuple, list)] # Scalar or tuple or list
127+
typing.Union[Scalar, tuple, list]
128+
```
129+
130+
% skip: end
131+
132+
If we don't include all of `scalar_types`, we won't see `Scalar`, as desired:
133+
134+
% invisible-code-block: python
135+
%
136+
% import sys
137+
138+
% skip: next "Result depends on NumPy version."
139+
140+
```python
141+
>>> Union[tuple(scalar_types[:-1])]
142+
typing.Union[numpy.int8, numpy.int16, numpy.int32, numpy.longlong, numpy.int64, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, numpy.ulonglong, numpy.float16, numpy.float32, numpy.float64, numpy.longdouble, numpy.complex64, numpy.complex128, numpy.clongdouble, numpy.str_, numpy.bytes_, numpy.void, numpy.bool]
143+
```
144+
145+
You can deactivate union aliases with `deactivate_union_aliases`:
146+
147+
```python
148+
>>> import warnings
149+
150+
>>> from plum import deactivate_union_aliases
151+
152+
>>> deactivate_union_aliases()
153+
154+
% skip: next "Result depends on NumPy version."
155+
>>> Scalar
156+
typing.Union[numpy.int8, numpy.int16, numpy.int32, numpy.longlong, numpy.int64, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, numpy.ulonglong, numpy.float16, numpy.float32, numpy.float64, numpy.longdouble, numpy.complex64, numpy.complex128, numpy.clongdouble, numpy.str_, numpy.bytes_, numpy.void, numpy.bool, numpy.object_]
157+
```

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ dynamic = ["version"]
1818

1919
requires-python = ">=3.10"
2020
dependencies = [
21-
"beartype>=0.16.2",
21+
"beartype>=0.22.2; python_version>='3.14'",
22+
"beartype>=0.16.2; python_version<'3.14'",
2223
"typing-extensions>=4.9.0",
2324
"rich>=10.0"
2425
]

0 commit comments

Comments
 (0)