Enable py3.14 and deprecate some alias functionality#225
Enable py3.14 and deprecate some alias functionality#225nstarman wants to merge 6 commits intobeartype:masterfrom
Conversation
Pull Request Test Coverage Report for Build 23405591937Details
💛 - Coveralls |
99374d0 to
baee207
Compare
|
@wesselb I don't know how to resolve this serious problem. |
|
Looking at https://github.com/python/cpython/blob/425f24e4fad672c211307a9f0018c8d39c4db9de/Objects/unionobject.c#L279 I kind of doubt the union alias functionality can continue to work this way. Meanwhile can I make this a no-op for Python 3.14+? |
Absolutely. Let's do this for now until we figure out whether this is still possible for 3.14+. |
|
Fully functional until py3.14 then they turn into deprecated no-ops. |
|
Tests need adjustment |
|
I can work on this after #179 |
|
@nstarman that would be amazing! :) |
23b77fd to
76e1a75
Compare
There was a problem hiding this comment.
Pull request overview
Enables Python 3.14 support by avoiding typing.Union monkeypatching (now immutable) and shifting union-alias handling to Plum’s own formatting/registry, with updated tests/docs/CI to cover the new behavior.
Changes:
- Add Python 3.14 test coverage and split union-alias tests by Python version.
- Update union aliasing internals to use
TypeAliasTypeon 3.14+ and apply alias-aware formatting inrepr_short/repr_type. - Adjust dependency constraints and documentation examples for the new 3.14+ behavior.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/plum/_alias.py |
Adds 3.14+ path using TypeAliasType, deprecates activation APIs, and introduces _transform_union_alias. |
src/plum/repr.py |
Uses _transform_union_alias so repr_short/repr_type can display registered union aliases. |
src/plum/_signature.py |
Refactors beartype type-hint wrapping usage (import/usage cleanup). |
src/plum/_type.py |
Small refactor in resolve_type_hint and adjusts UNION_TYPES container type. |
tests/test_alias_upto313.py |
Skips these tests on 3.14+ and routes activation calls via plum.*. |
tests/test_alias_314plus.py |
New 3.14+ focused tests for alias formatting and deprecated APIs. |
tests/test_util.py |
Updates expected typing.Union[...] short repr for 3.14+. |
tests/conftest.py |
Adds an autouse fixture to isolate/clear the union-alias registry per test. |
docs/union_aliases.md |
Adds version-gated examples for the changed Union repr behavior in 3.14+. |
docs/comparison.md |
Adds version-gated example output to match 3.14+ Union repr changes. |
pyproject.toml |
Adds conditional beartype minimum versions keyed on Python version. |
.github/workflows/ci.yml |
Adds Python 3.14 to CI and pre-release beartype test runs. |
.pre-commit-config.yaml |
Removes default Python language version pinning. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@leycec one part of this PR is to change the type hint repr machinery in |
|
@wesselb, currently this PR uses the TypeAliasType repr directly, but we might want to preserve the old behaviour of transforming it to |
|
Ho, ho, ho. I have no what idea what's happening here. Yet, I love to see it. @wesselb's Plum is alive and well! You've even attracted a tightly-knit coterie of like-minded Plumthynistas voluntarily maintaining and building out functionality for the next generation. In the modern amped-up LLM era, you've even got Copilot running circles with automated code review. Wonderful. Simply wonderful. Since I have no idea what's happening here, I will now begin monologuing from a place of profound ignorance. I didn't even know that the Plum API provided union aliases. Now that I do, they vaguely share more than a passing resemblance to:
Of course, the latter example isn't quite right. @beartype-specific I could keep typing, but probably shouldn't. PEP 695 |
This PR deprecates scalar_types = tuple(np.sctypeDict.values()) # All NumPy scalar types
type Scalar = Union[scalar_types]
@dispatch
def add(x: Scalar, y: Scalar):
return x + yBut these aliases are "local" to the context they are used. With # imaginary_library (I don't control)
two_elt_dtype = np.dtype(...)
def add(x: Fraction | tuple[float | np.floating, float | np.floating] | two_elt_dtype, y: Fraction | tuple[float | np.floating, float | np.floating] | two_elt_dtype) -> Fraction:
return Fraction(x) + Fraction(y)
# my_library (I do control)
plum.dispatch(imaginary_library.add) # register in their method
type Scalar = Union[scalar_types] # love it
@plum.dispatch
def add(x: Scalar, y: Scalar) -> Scalar: # register in our method
return x + y
# IDE
>>> add.methods
List of X method(s):
[0] add(x: Scalar, y: Scalar] -> Scalar: ...
[1] add(x: Fraction | tuple[float | np.floating, float | np.floating] | two_elt_dtype, y: Fraction | tuple[float | np.floating, float | np.floating] | two_elt_dtype) -> Fraction: ...
>>> plum.set_union_alias(Fraction | tuple[float | np.floating, float | np.floating] | two_elt_dtype, "FractionLike")
>>> add.methods
List of X method(s):
[0] add(x: Scalar, y: Scalar] -> Scalar: ...
[1] add(x: FractionLike, y: FractionLike) -> Fraction: ...
That sounds like a good application. Ideally Python itself would give a means to herd unruly type annotations into formation, e.g. in |
@wesselb we could actually keep |
|
Hey @nstarman and @leycec! Apologies for the radio silence. Work's been really busy. @nstarman got this exactly right! The example of @nstarman, thanks a lot for putting together this PR. It looks really, really good. I'll do an in-depth review ASAP. (I won't be so slow this time!)
I think I like the new behaviour, so I'd be happy to keep it the way you implemented it now. What do you think?
That is a good suggestion. Let's do that! |
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| args_set = set(args) | ||
| # Look for a matching alias in the registry | ||
| for union_args, type_alias in _ALIASED_UNIONS.items(): | ||
| if set(union_args) == args_set: |
There was a problem hiding this comment.
On Python < 3.14, _ALIASED_UNIONS maps to str aliases, but _transform_union_alias() blindly returns the dict value. When this is called from repr_type() it will turn a union into a plain string and Rich will render it with quotes (e.g. 'IntStr') instead of a type representation. Consider making _transform_union_alias() only return when the stored value is a TypeAliasType (or make the <3.14 registry store TypeAliasTypes), and otherwise leave x unchanged for the legacy monkeypatch-based path.
| if set(union_args) == args_set: | |
| # On Python < 3.14, the registry may store string aliases; only | |
| # return actual TypeAliasType instances to avoid turning the union | |
| # into a plain string for representation. | |
| if set(union_args) == args_set and isinstance(type_alias, TypeAliasType): |
There was a problem hiding this comment.
I'm not sure this is correct. The tests all pass.
This is a subtle one.
Ping @wesselb
wesselb
left a comment
There was a problem hiding this comment.
This is great, @nstarman! Thanks so much for putting this together. I've left an array of minor suggestions to keep the style in sync with the rest of the code. I hope you don't mind.
If I understand correctly, the algorithm for TypeAliasType checks if a union occurs in _ALIASED_UNIONS and replaces it with the alias if there is a match. This works if the union is exactly contained in _ALIASED_UNIONS, but doesn't if it is a union of two alias unions or if there are other types in the union too.
For example, (str, Scalar) is expanded to (str, float, int, Number, ...), which would ideally be aliased back to (str, Scalar). The case of the if-statement for before Python 3.14 has some logic to handle the general case. Do you think it would be possible to do this for the Python 3.14 and later case too?
I'm happy to jump in and hack around myself to see if I can make this work. It seems like the logic to determine which aliases are contained in a union could possibly be shared.
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>
I agree that would be a nice upgrade.
That would be great! |
|
Ping @wesselb :) |
|
Apologies, @nstarman! Last couple of months have been very busy. Perhaps we could also merge the current approach for now and open an issue to extend it to more general unions. How would that sound? |
|
SGTM. It should only make repr changes, not actual dispatch changes, so most people won't notice. |


Aliases are now always turned on. The alias machinery dynamically creates
TypeAliasTypeobjects that are ONLY used in string representations, never in the actual type.Fixes #224
Fixes #227