Skip to content

Commit a3474f9

Browse files
authored
Merge branch 'main' into TypedDict/backport__total__
2 parents 0c9ef48 + caf24b4 commit a3474f9

File tree

6 files changed

+180
-5
lines changed

6 files changed

+180
-5
lines changed

CHANGELOG.md

Lines changed: 4 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):

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: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,6 +1647,95 @@ def test_final_forward_ref(self):
16471647
self.assertNotEqual(gth(Loop, globals())['attr'], Final[int])
16481648
self.assertNotEqual(gth(Loop, globals())['attr'], Final)
16491649

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

16511740
class GetUtilitiesTestCase(TestCase):
16521741
def test_get_origin(self):

src/typing_extensions.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,10 +1240,90 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
12401240
)
12411241
else: # 3.8
12421242
hint = typing.get_type_hints(obj, globalns=globalns, localns=localns)
1243+
if sys.version_info < (3, 11):
1244+
_clean_optional(obj, hint, globalns, localns)
1245+
if sys.version_info < (3, 9):
1246+
# In 3.8 eval_type does not flatten Optional[ForwardRef] correctly
1247+
# This will recreate and and cache Unions.
1248+
hint = {
1249+
k: (t
1250+
if get_origin(t) != Union
1251+
else Union[t.__args__])
1252+
for k, t in hint.items()
1253+
}
12431254
if include_extras:
12441255
return hint
12451256
return {k: _strip_extras(t) for k, t in hint.items()}
12461257

1258+
_NoneType = type(None)
1259+
1260+
def _could_be_inserted_optional(t):
1261+
"""detects Union[..., None] pattern"""
1262+
# 3.8+ compatible checking before _UnionGenericAlias
1263+
if get_origin(t) is not Union:
1264+
return False
1265+
# Assume if last argument is not None they are user defined
1266+
if t.__args__[-1] is not _NoneType:
1267+
return False
1268+
return True
1269+
1270+
# < 3.11
1271+
def _clean_optional(obj, hints, globalns=None, localns=None):
1272+
# reverts injected Union[..., None] cases from typing.get_type_hints
1273+
# when a None default value is used.
1274+
# see https://github.com/python/typing_extensions/issues/310
1275+
if not hints or isinstance(obj, type):
1276+
return
1277+
defaults = typing._get_defaults(obj) # avoid accessing __annotations___
1278+
if not defaults:
1279+
return
1280+
original_hints = obj.__annotations__
1281+
for name, value in hints.items():
1282+
# Not a Union[..., None] or replacement conditions not fullfilled
1283+
if (not _could_be_inserted_optional(value)
1284+
or name not in defaults
1285+
or defaults[name] is not None
1286+
):
1287+
continue
1288+
original_value = original_hints[name]
1289+
# value=NoneType should have caused a skip above but check for safety
1290+
if original_value is None:
1291+
original_value = _NoneType
1292+
# Forward reference
1293+
if isinstance(original_value, str):
1294+
if globalns is None:
1295+
if isinstance(obj, _types.ModuleType):
1296+
globalns = obj.__dict__
1297+
else:
1298+
nsobj = obj
1299+
# Find globalns for the unwrapped object.
1300+
while hasattr(nsobj, '__wrapped__'):
1301+
nsobj = nsobj.__wrapped__
1302+
globalns = getattr(nsobj, '__globals__', {})
1303+
if localns is None:
1304+
localns = globalns
1305+
elif localns is None:
1306+
localns = globalns
1307+
if sys.version_info < (3, 9):
1308+
original_value = ForwardRef(original_value)
1309+
else:
1310+
original_value = ForwardRef(
1311+
original_value,
1312+
is_argument=not isinstance(obj, _types.ModuleType)
1313+
)
1314+
original_evaluated = typing._eval_type(original_value, globalns, localns)
1315+
if sys.version_info < (3, 9) and get_origin(original_evaluated) is Union:
1316+
# Union[str, None, "str"] is not reduced to Union[str, None]
1317+
original_evaluated = Union[original_evaluated.__args__]
1318+
# Compare if values differ. Note that even if equal
1319+
# value might be cached by typing._tp_cache contrary to original_evaluated
1320+
if original_evaluated != value or (
1321+
# 3.10: ForwardRefs of UnionType might be turned into _UnionGenericAlias
1322+
hasattr(_types, "UnionType")
1323+
and isinstance(original_evaluated, _types.UnionType)
1324+
and not isinstance(value, _types.UnionType)
1325+
):
1326+
hints[name] = original_evaluated
12471327

12481328
# Python 3.9+ has PEP 593 (Annotated)
12491329
if hasattr(typing, 'Annotated'):

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)