Skip to content

Commit 37c60c8

Browse files
Merge branch 'main' into pep728
2 parents a95fd46 + 7def253 commit 37c60c8

File tree

6 files changed

+227
-6
lines changed

6 files changed

+227
-6
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ aliases that have a `Concatenate` special form as their argument.
2121
Patch by [Daraan](https://github.com/Daraan).
2222
- Extended the `Concatenate` backport for Python 3.8-3.10 to now accept
2323
`Ellipsis` as an argument. Patch by [Daraan](https://github.com/Daraan).
24+
- Fix backport of `get_type_hints` to reflect Python 3.11+ behavior which does not add
25+
`Union[..., NoneType]` to annotations that have a `None` default value anymore.
26+
This fixes wrapping of `Annotated` in an unwanted `Optional` in such cases.
27+
Patch by [Daraan](https://github.com/Daraan).
2428
- Fix error in subscription of `Unpack` aliases causing nested Unpacks
2529
to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan).
2630
- Backport CPython PR [#124795](https://github.com/python/cpython/pull/124795):
@@ -34,6 +38,13 @@ aliases that have a `Concatenate` special form as their argument.
3438
Patch by [Daraan](https://github.com/Daraan).
3539
- Fix error on Python 3.10 when using `typing.Concatenate` and
3640
`typing_extensions.Concatenate` together. Patch by [Daraan](https://github.com/Daraan).
41+
- Backport of CPython PR [#109544](https://github.com/python/cpython/pull/109544)
42+
to reflect Python 3.13+ behavior: A value assigned to `__total__` in the class body of a
43+
`TypedDict` will be overwritten by the `total` argument of the `TypedDict` constructor.
44+
Patch by [Daraan](https://github.com/Daraan), backporting a CPython PR by Jelle Zijlstra.
45+
- Fix for Python 3.11 that now `isinstance(typing_extensions.Unpack[...], TypeVar)`
46+
evaluates to `False`, however still `True` for <3.11.
47+
Patch by [Daraan](https://github.com/Daraan)
3748

3849
# Release 4.12.2 (June 7, 2024)
3950

doc/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Example usage::
133133
False
134134
>>> is_literal(get_origin(typing.Literal[42]))
135135
True
136-
>>> is_literal(get_origin(typing_extensions.Final[42]))
136+
>>> is_literal(get_origin(typing_extensions.Final[int]))
137137
False
138138

139139
Python version support

pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Build system requirements.
22
[build-system]
3-
requires = ["flit_core >=3.4,<4"]
3+
requires = ["flit_core >=3.11,<4"]
44
build-backend = "flit_core.buildapi"
55

66
# Project metadata
@@ -10,7 +10,8 @@ version = "4.12.2"
1010
description = "Backported and Experimental Type Hints for Python 3.8+"
1111
readme = "README.md"
1212
requires-python = ">=3.8"
13-
license = { text = "PSF-2.0" }
13+
license = "PSF-2.0"
14+
license-files = ["LICENSE"]
1415
keywords = [
1516
"annotations",
1617
"backport",
@@ -30,7 +31,6 @@ classifiers = [
3031
"Development Status :: 5 - Production/Stable",
3132
"Environment :: Console",
3233
"Intended Audience :: Developers",
33-
"License :: OSI Approved :: Python Software Foundation License",
3434
"Operating System :: OS Independent",
3535
"Programming Language :: Python :: 3",
3636
"Programming Language :: Python :: 3 :: Only",
@@ -93,6 +93,8 @@ ignore = [
9393
"UP038",
9494
# Not relevant here
9595
"RUF012",
96+
"RUF022",
97+
"RUF023",
9698
]
9799

98100
[tool.ruff.lint.per-file-ignores]

src/test_typing_extensions.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,6 +1650,95 @@ def test_final_forward_ref(self):
16501650
self.assertNotEqual(gth(Loop, globals())['attr'], Final[int])
16511651
self.assertNotEqual(gth(Loop, globals())['attr'], Final)
16521652

1653+
def test_annotation_and_optional_default(self):
1654+
annotation = Annotated[Union[int, None], "data"]
1655+
NoneAlias = None
1656+
StrAlias = str
1657+
T_default = TypeVar("T_default", default=None)
1658+
Ts = TypeVarTuple("Ts")
1659+
1660+
cases = {
1661+
# annotation: expected_type_hints
1662+
Annotated[None, "none"] : Annotated[None, "none"],
1663+
annotation : annotation,
1664+
Optional[int] : Optional[int],
1665+
Optional[List[str]] : Optional[List[str]],
1666+
Optional[annotation] : Optional[annotation],
1667+
Union[str, None, str] : Optional[str],
1668+
Unpack[Tuple[int, None]]: Unpack[Tuple[int, None]],
1669+
# Note: A starred *Ts will use typing.Unpack in 3.11+ see Issue #485
1670+
Unpack[Ts] : Unpack[Ts],
1671+
}
1672+
# contains a ForwardRef, TypeVar(~prefix) or no expression
1673+
do_not_stringify_cases = {
1674+
() : {}, # Special-cased below to create an unannotated parameter
1675+
int : int,
1676+
"int" : int,
1677+
None : type(None),
1678+
"NoneAlias" : type(None),
1679+
List["str"] : List[str],
1680+
Union[str, "str"] : str,
1681+
Union[str, None, "str"] : Optional[str],
1682+
Union[str, "NoneAlias", "StrAlias"]: Optional[str],
1683+
Union[str, "Union[None, StrAlias]"]: Optional[str],
1684+
Union["annotation", T_default] : Union[annotation, T_default],
1685+
Annotated["annotation", "nested"] : Annotated[Union[int, None], "data", "nested"],
1686+
}
1687+
if TYPING_3_10_0: # cannot construct UnionTypes before 3.10
1688+
do_not_stringify_cases["str | NoneAlias | StrAlias"] = str | None
1689+
cases[str | None] = Optional[str]
1690+
cases.update(do_not_stringify_cases)
1691+
for (annot, expected), none_default, as_str, wrap_optional in itertools.product(
1692+
cases.items(), (False, True), (False, True), (False, True)
1693+
):
1694+
# Special case:
1695+
skip_reason = None
1696+
annot_unchanged = annot
1697+
if sys.version_info[:2] == (3, 10) and annot == "str | NoneAlias | StrAlias" and none_default:
1698+
# In 3.10 converts Optional[str | None] to Optional[str] which has a different repr
1699+
skip_reason = "UnionType not preserved in 3.10"
1700+
if wrap_optional:
1701+
if annot_unchanged == ():
1702+
continue
1703+
annot = Optional[annot]
1704+
expected = {"x": Optional[expected]}
1705+
else:
1706+
expected = {"x": expected} if annot_unchanged != () else {}
1707+
if as_str:
1708+
if annot_unchanged in do_not_stringify_cases or annot_unchanged == ():
1709+
continue
1710+
annot = str(annot)
1711+
with self.subTest(
1712+
annotation=annot,
1713+
as_str=as_str,
1714+
wrap_optional=wrap_optional,
1715+
none_default=none_default,
1716+
expected_type_hints=expected,
1717+
):
1718+
# Create function to check
1719+
if annot_unchanged == ():
1720+
if none_default:
1721+
def func(x=None): pass
1722+
else:
1723+
def func(x): pass
1724+
elif none_default:
1725+
def func(x: annot = None): pass
1726+
else:
1727+
def func(x: annot): pass
1728+
type_hints = get_type_hints(func, globals(), locals(), include_extras=True)
1729+
# Equality
1730+
self.assertEqual(type_hints, expected)
1731+
# Hash
1732+
for k in type_hints.keys():
1733+
self.assertEqual(hash(type_hints[k]), hash(expected[k]))
1734+
# Test if UnionTypes are preserved
1735+
self.assertIs(type(type_hints[k]), type(expected[k]))
1736+
# Repr
1737+
with self.subTest("Check str and repr"):
1738+
if skip_reason == "UnionType not preserved in 3.10":
1739+
self.skipTest(skip_reason)
1740+
self.assertEqual(repr(type_hints), repr(expected))
1741+
16531742

16541743
class GetUtilitiesTestCase(TestCase):
16551744
def test_get_origin(self):
@@ -4158,6 +4247,37 @@ def test_total(self):
41584247
self.assertEqual(Options.__required_keys__, frozenset())
41594248
self.assertEqual(Options.__optional_keys__, {'log_level', 'log_path'})
41604249

4250+
def test_total_inherits_non_total(self):
4251+
class TD1(TypedDict, total=False):
4252+
a: int
4253+
4254+
self.assertIs(TD1.__total__, False)
4255+
4256+
class TD2(TD1):
4257+
b: str
4258+
4259+
self.assertIs(TD2.__total__, True)
4260+
4261+
def test_total_with_assigned_value(self):
4262+
class TD(TypedDict):
4263+
__total__ = "some_value"
4264+
4265+
self.assertIs(TD.__total__, True)
4266+
4267+
class TD2(TypedDict, total=True):
4268+
__total__ = "some_value"
4269+
4270+
self.assertIs(TD2.__total__, True)
4271+
4272+
class TD3(TypedDict, total=False):
4273+
__total__ = "some value"
4274+
4275+
self.assertIs(TD3.__total__, False)
4276+
4277+
TD4 = TypedDict('TD4', {'__total__': "some_value"}) # noqa: F821
4278+
self.assertIs(TD4.__total__, True)
4279+
4280+
41614281
def test_optional_keys(self):
41624282
class Point2Dor3D(Point2D, total=False):
41634283
z: int
@@ -6156,6 +6276,12 @@ def test_equivalent_nested_variadics(self):
61566276
self.assertEqual(nested_tuple_bare, TupleAliasTsT[Unpack[Tuple[str, int]], object])
61576277
self.assertEqual(nested_tuple_bare, TupleAliasTsT[Unpack[Tuple[str]], Unpack[Tuple[int]], object])
61586278

6279+
@skipUnless(TYPING_3_11_0, "Needed for backport")
6280+
def test_type_var_inheritance(self):
6281+
Ts = TypeVarTuple("Ts")
6282+
self.assertFalse(isinstance(Unpack[Ts], TypeVar))
6283+
self.assertFalse(isinstance(Unpack[Ts], typing.TypeVar))
6284+
61596285

61606286
class TypeVarTupleTests(BaseTestCase):
61616287

src/typing_extensions.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1313,10 +1313,90 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
13131313
)
13141314
else: # 3.8
13151315
hint = typing.get_type_hints(obj, globalns=globalns, localns=localns)
1316+
if sys.version_info < (3, 11):
1317+
_clean_optional(obj, hint, globalns, localns)
1318+
if sys.version_info < (3, 9):
1319+
# In 3.8 eval_type does not flatten Optional[ForwardRef] correctly
1320+
# This will recreate and and cache Unions.
1321+
hint = {
1322+
k: (t
1323+
if get_origin(t) != Union
1324+
else Union[t.__args__])
1325+
for k, t in hint.items()
1326+
}
13161327
if include_extras:
13171328
return hint
13181329
return {k: _strip_extras(t) for k, t in hint.items()}
13191330

1331+
_NoneType = type(None)
1332+
1333+
def _could_be_inserted_optional(t):
1334+
"""detects Union[..., None] pattern"""
1335+
# 3.8+ compatible checking before _UnionGenericAlias
1336+
if get_origin(t) is not Union:
1337+
return False
1338+
# Assume if last argument is not None they are user defined
1339+
if t.__args__[-1] is not _NoneType:
1340+
return False
1341+
return True
1342+
1343+
# < 3.11
1344+
def _clean_optional(obj, hints, globalns=None, localns=None):
1345+
# reverts injected Union[..., None] cases from typing.get_type_hints
1346+
# when a None default value is used.
1347+
# see https://github.com/python/typing_extensions/issues/310
1348+
if not hints or isinstance(obj, type):
1349+
return
1350+
defaults = typing._get_defaults(obj) # avoid accessing __annotations___
1351+
if not defaults:
1352+
return
1353+
original_hints = obj.__annotations__
1354+
for name, value in hints.items():
1355+
# Not a Union[..., None] or replacement conditions not fullfilled
1356+
if (not _could_be_inserted_optional(value)
1357+
or name not in defaults
1358+
or defaults[name] is not None
1359+
):
1360+
continue
1361+
original_value = original_hints[name]
1362+
# value=NoneType should have caused a skip above but check for safety
1363+
if original_value is None:
1364+
original_value = _NoneType
1365+
# Forward reference
1366+
if isinstance(original_value, str):
1367+
if globalns is None:
1368+
if isinstance(obj, _types.ModuleType):
1369+
globalns = obj.__dict__
1370+
else:
1371+
nsobj = obj
1372+
# Find globalns for the unwrapped object.
1373+
while hasattr(nsobj, '__wrapped__'):
1374+
nsobj = nsobj.__wrapped__
1375+
globalns = getattr(nsobj, '__globals__', {})
1376+
if localns is None:
1377+
localns = globalns
1378+
elif localns is None:
1379+
localns = globalns
1380+
if sys.version_info < (3, 9):
1381+
original_value = ForwardRef(original_value)
1382+
else:
1383+
original_value = ForwardRef(
1384+
original_value,
1385+
is_argument=not isinstance(obj, _types.ModuleType)
1386+
)
1387+
original_evaluated = typing._eval_type(original_value, globalns, localns)
1388+
if sys.version_info < (3, 9) and get_origin(original_evaluated) is Union:
1389+
# Union[str, None, "str"] is not reduced to Union[str, None]
1390+
original_evaluated = Union[original_evaluated.__args__]
1391+
# Compare if values differ. Note that even if equal
1392+
# value might be cached by typing._tp_cache contrary to original_evaluated
1393+
if original_evaluated != value or (
1394+
# 3.10: ForwardRefs of UnionType might be turned into _UnionGenericAlias
1395+
hasattr(_types, "UnionType")
1396+
and isinstance(original_evaluated, _types.UnionType)
1397+
and not isinstance(value, _types.UnionType)
1398+
):
1399+
hints[name] = original_evaluated
13201400

13211401
# Python 3.9+ has PEP 593 (Annotated)
13221402
if hasattr(typing, 'Annotated'):
@@ -2621,7 +2701,9 @@ def __init__(self, getitem):
26212701
self.__doc__ = _UNPACK_DOC
26222702

26232703
class _UnpackAlias(typing._GenericAlias, _root=True):
2624-
__class__ = typing.TypeVar
2704+
if sys.version_info < (3, 11):
2705+
# needed for compatibility with Generic[Unpack[Ts]]
2706+
__class__ = typing.TypeVar
26252707

26262708
@property
26272709
def __typing_unpacked_tuple_args__(self):

test-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ruff==0.4.5
1+
ruff==0.9.6

0 commit comments

Comments
 (0)