Skip to content

Commit 518565d

Browse files
Add Python 3.14 support (#59)
* test 3.14.0 * fix: handle descriptors on type-hint objects that are generic classes changed in 3.14 in get_type_hint_args(), get_type_hint_parameters() and get_type_hint_origin() * improvement: use `annotationlib.type_repr()` if available (since 3.14) * fix: our `get_annotations()` backport now reads the `__annotations__` attribute on types directly and supports calling the `__annotate__()` function * fix: test for resolving forward references to a Union returns a slightly different result in 3.14 * actually use `annotatelib.get_annotations()`
1 parent 033417a commit 518565d

4 files changed

Lines changed: 60 additions & 13 deletions

File tree

.github/workflows/python.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ jobs:
110110
"3.13.7",
111111
"3.13.8",
112112
"3.13.9",
113+
"3.14.0",
113114

114115
# manual additions
115116
"pypy-3.8",

src/typeapi/backport/inspect.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,28 @@ def get_annotations(
5959
although if obj is a wrapped function (using
6060
functools.update_wrapper()) it is first unwrapped.
6161
"""
62-
if isinstance(obj, type):
63-
# class
64-
obj_dict = getattr(obj, "__dict__", None)
65-
if obj_dict and hasattr(obj_dict, "get"):
66-
ann = obj_dict.get("__annotations__", None)
67-
if isinstance(ann, types.GetSetDescriptorType):
68-
ann = None
62+
63+
ann: Any = None
64+
65+
if sys.version_info[:2] >= (3, 14):
66+
from annotationlib import Format
67+
from annotationlib import get_annotations as _get_annotations
68+
69+
ann = _get_annotations(obj, format=Format.VALUE, eval_str=False)
70+
else:
71+
if isinstance(obj, type):
72+
# class
73+
obj_dict = getattr(obj, "__dict__", None)
74+
if obj_dict and hasattr(obj_dict, "get"):
75+
ann = obj_dict.get("__annotations__", None)
76+
if isinstance(ann, types.GetSetDescriptorType):
77+
ann = None
6978
else:
70-
ann = None
79+
ann = getattr(obj, "__annotations__", None)
80+
81+
# Determine the scope in which the annotations are to be evaluated.
7182

83+
if isinstance(obj, type):
7284
obj_globals = None
7385
module_name = getattr(obj, "__module__", None)
7486
if module_name:
@@ -78,16 +90,13 @@ def get_annotations(
7890
obj_locals = dict(vars(obj))
7991
unwrap = obj
8092
elif isinstance(obj, types.ModuleType):
81-
# module
82-
ann = getattr(obj, "__annotations__", None)
8393
obj_globals = getattr(obj, "__dict__")
8494
obj_locals = None
8595
unwrap = None
8696
elif callable(obj):
8797
# this includes types.Function, types.BuiltinFunctionType,
8898
# types.BuiltinMethodType, functools.partial, functools.singledispatch,
8999
# "class funclike" from Lib/test/test_inspect... on and on it goes.
90-
ann = getattr(obj, "__annotations__", None)
91100
obj_globals = getattr(obj, "__globals__", None)
92101
obj_locals = None
93102
unwrap = obj

src/typeapi/utils.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import collections
2+
import inspect
23
import sys
34
import types
45
import typing
@@ -16,6 +17,7 @@
1617
IS_PYTHON_AT_LEAST_3_7 = sys.version_info[:2] >= (3, 7)
1718
IS_PYTHON_AT_LEAST_3_9 = sys.version_info[:2] >= (3, 9)
1819
IS_PYTHON_AT_LEAST_3_10 = sys.version_info[:2] >= (3, 10)
20+
IS_PYTHON_AT_LAST_3_14 = sys.version_info[:2] >= (3, 14)
1921
TYPING_MODULE_NAMES = frozenset(["typing", "typing_extensions", "collections.abc"])
2022
T_contra = TypeVar("T_contra", contravariant=True)
2123
U_co = TypeVar("U_co", covariant=True)
@@ -45,6 +47,11 @@ def get_type_hint_origin_or_none(hint: object) -> "Any | None":
4547

4648
hint_origin = getattr(hint, "__origin__", None)
4749

50+
# With Python 3.14, the `__origin__` field on typing classes is a getset_descriptor; but we must treat it
51+
# as if the type hint has no origin. For completeness, we ignore any kind of descriptor.
52+
if IS_PYTHON_AT_LAST_3_14 and is_any_descriptor(hint_origin):
53+
hint_origin = None
54+
4855
# In Python 3.6, List[int].__origin__ points to List; but we can look for
4956
# the Python native type in its __bases__.
5057
if (
@@ -117,6 +124,8 @@ def get_type_hint_args(hint: object) -> Tuple[Any, ...]:
117124
"""
118125

119126
hint_args = getattr(hint, "__args__", None) or ()
127+
if IS_PYTHON_AT_LAST_3_14 and is_any_descriptor(hint_args):
128+
hint_args = ()
120129

121130
# In Python 3.7 and 3.8, generics like List and Tuple have a "_special"
122131
# but their __args__ contain type vars. For consistent results across
@@ -152,6 +161,8 @@ def get_type_hint_parameters(hint: object) -> Tuple[Any, ...]:
152161
"""
153162

154163
hint_params = getattr(hint, "__parameters__", None) or ()
164+
if IS_PYTHON_AT_LAST_3_14 and is_any_descriptor(hint_params):
165+
hint_params = ()
155166

156167
# In Python 3.9+, special generic aliases like List and Tuple don't store
157168
# their type variables as parameters anymore; we try to restore those.
@@ -271,7 +282,7 @@ def _populate(hint: Any) -> None:
271282
}
272283

273284

274-
def type_repr(obj: Any) -> str:
285+
def _type_repr_pre_3_14(obj: Any) -> str:
275286
"""#typing._type_repr() stolen from Python 3.8."""
276287

277288
if (getattr(obj, "__module__", None) or getattr(type(obj), "__module__", None)) in TYPING_MODULE_NAMES or hasattr(
@@ -292,6 +303,12 @@ def type_repr(obj: Any) -> str:
292303
return repr(obj)
293304

294305

306+
if sys.version_info[:2] >= (3, 14): # Can't use IS_PYTHON_AT_LEAST_3_14 because Mypy won't recognize it
307+
from annotationlib import type_repr
308+
else:
309+
type_repr = _type_repr_pre_3_14
310+
311+
295312
def get_annotations(
296313
obj: Union[Callable[..., Any], ModuleType, type],
297314
include_bases: bool = False,
@@ -398,3 +415,12 @@ def is_new_type(hint: Any) -> TypeGuard[NewTypeP]:
398415
# NOTE: Starting with Python 3.10, `typing.NewType` is actually a class instead of a function, but it is
399416
# still typed as a function in Mypy until 3.12.
400417
return hasattr(hint, "__name__") and hasattr(hint, "__supertype__")
418+
419+
420+
def is_any_descriptor(value: Any) -> bool:
421+
return (
422+
inspect.isdatadescriptor(value)
423+
or inspect.ismethoddescriptor(value)
424+
or inspect.isgetsetdescriptor(value)
425+
or inspect.ismethoddescriptor(value)
426+
)

src/typeapi/utils_test.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# type: ignore
22

33
import collections.abc
4+
import inspect
45
import sys
56
import typing as t
67
from typing import Any, Dict, Generic, List, Mapping, MutableMapping, Optional, TypeVar, Union
@@ -9,6 +10,7 @@
910
import typing_extensions
1011

1112
from typeapi.utils import (
13+
IS_PYTHON_AT_LAST_3_14,
1214
IS_PYTHON_AT_LEAST_3_7,
1315
IS_PYTHON_AT_LEAST_3_9,
1416
ForwardRef,
@@ -292,6 +294,8 @@ def test__typing_Union__introspection():
292294
if sys.version_info[:2] <= (3, 6):
293295
assert Union.__origin__ is None
294296
assert Union[int, str].__origin__ is Union
297+
elif IS_PYTHON_AT_LAST_3_14:
298+
assert inspect.isgetsetdescriptor(Union.__origin__)
295299
else:
296300
assert not hasattr(Union, "__origin__")
297301
assert Union[int, str].__origin__ is Union
@@ -304,6 +308,9 @@ def test__typing_Union__introspection():
304308
if sys.version_info[:2] <= (3, 6):
305309
assert Union.__args__ is None
306310
assert Union.__parameters__ is None
311+
elif IS_PYTHON_AT_LAST_3_14:
312+
assert inspect.ismemberdescriptor(Union.__args__)
313+
assert inspect.isgetsetdescriptor(Union.__parameters__)
307314
else:
308315
assert not hasattr(Union, "__args__")
309316
assert not hasattr(Union, "__parameters__")
@@ -451,9 +458,13 @@ class A:
451458
annotations = get_annotations(A)
452459
assert annotations == {"a": Optional[str]}
453460

461+
if IS_PYTHON_AT_LAST_3_14:
462+
# from typing import Union
463+
assert type(annotations["a"]) is Union
464+
454465
# NOTE(@NiklasRosenstein): Even though `str | None` is of type `types.UnionType` in Python 3.10+,
455466
# our fake evaluation will still return legacy type hints.
456-
if IS_PYTHON_AT_LEAST_3_9:
467+
elif IS_PYTHON_AT_LEAST_3_9:
457468
from typing import _UnionGenericAlias # type: ignore
458469

459470
assert type(annotations["a"]) is _UnionGenericAlias

0 commit comments

Comments
 (0)