Skip to content

Commit 376ae56

Browse files
committed
Fix get_type_hints with None default interaction
1 parent 08d866b commit 376ae56

File tree

3 files changed

+100
-0
lines changed

3 files changed

+100
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
subscripted objects) had wrong parameters if they were directly
1717
subscripted with an `Unpack` object.
1818
Patch by [Daraan](https://github.com/Daraan).
19+
- Fix backport of `get_type_hints` to reflect Python 3.11+ behavior which does not add
20+
`Union[..., NoneType]` to annotations that have a `None` default value anymore.
21+
This fixes wrapping of `Annotated` in an unwanted `Optional` in such cases.
22+
Patch by [Daraan](https://github.com/Daraan).
1923

2024
# Release 4.12.2 (June 7, 2024)
2125

src/test_typing_extensions.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4993,6 +4993,37 @@ def test_nested_annotated_with_unhashable_metadata(self):
49934993
self.assertEqual(X.__origin__, List[Annotated[str, {"unhashable_metadata"}]])
49944994
self.assertEqual(X.__metadata__, ("metadata",))
49954995

4996+
def test_get_type_hints(self):
4997+
annotation = Annotated[Union[int, None], "data"]
4998+
optional_annotation = Optional[annotation]
4999+
5000+
def wanted_optional(bar: optional_annotation): ...
5001+
def wanted_optional_default(bar: optional_annotation = None): ...
5002+
def wanted_optional_ref(bar: 'Optional[Annotated[Union[int, None], "data"]]'): ...
5003+
5004+
def no_optional(bar: annotation): ...
5005+
def no_optional_default(bar: annotation = None): ...
5006+
def no_optional_defaultT(bar: Union[annotation, T] = None): ...
5007+
def no_optional_defaultT_ref(bar: "Union[annotation, T]" = None): ...
5008+
5009+
for func in(wanted_optional, wanted_optional_default, wanted_optional_ref):
5010+
self.assertEqual(
5011+
get_type_hints(func, include_extras=True),
5012+
{"bar": optional_annotation}
5013+
)
5014+
5015+
for func in (no_optional, no_optional_default):
5016+
self.assertEqual(
5017+
get_type_hints(func, include_extras=True),
5018+
{"bar": annotation}
5019+
)
5020+
5021+
for func in (no_optional_defaultT, no_optional_defaultT_ref):
5022+
self.assertEqual(
5023+
get_type_hints(func, globals(), locals(), include_extras=True),
5024+
{"bar": Union[annotation, T]}
5025+
)
5026+
49965027

49975028
class GetTypeHintsTests(BaseTestCase):
49985029
def test_get_type_hints(self):

src/typing_extensions.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,10 +1236,75 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
12361236
)
12371237
else: # 3.8
12381238
hint = typing.get_type_hints(obj, globalns=globalns, localns=localns)
1239+
if sys.version_info < (3, 11) and hint:
1240+
hint = _clean_optional(obj, hint, globalns, localns)
12391241
if include_extras:
12401242
return hint
12411243
return {k: _strip_extras(t) for k, t in hint.items()}
12421244

1245+
_NoneType = type(None)
1246+
1247+
def _could_be_inserted_optional(t):
1248+
"""detects Union[..., None] pattern"""
1249+
# 3.8+ compatible checking before _UnionGenericAlias
1250+
if not hasattr(t, "__origin__") or t.__origin__ is not Union:
1251+
return False
1252+
# Assume if last argument is not None they are user defined
1253+
if t.__args__[-1] is not _NoneType:
1254+
return False
1255+
return True
1256+
1257+
# < 3.11
1258+
def _clean_optional(obj, hints, globalns=None, localns=None):
1259+
# reverts injected Union[..., None] cases from typing.get_type_hints
1260+
# when a None default value is used.
1261+
# see https://github.com/python/typing_extensions/issues/310
1262+
original_hints = getattr(obj, '__annotations__', None)
1263+
defaults = typing._get_defaults(obj)
1264+
for name, value in hints.items():
1265+
# Not a Union[..., None] or replacement conditions not fullfilled
1266+
if (not _could_be_inserted_optional(value)
1267+
or name not in defaults
1268+
or defaults[name] is not None
1269+
):
1270+
continue
1271+
original_value = original_hints[name]
1272+
if original_value is None:
1273+
original_value = _NoneType
1274+
# Forward reference
1275+
if isinstance(original_value, str):
1276+
if globalns is None:
1277+
if isinstance(obj, _types.ModuleType):
1278+
globalns = obj.__dict__
1279+
else:
1280+
nsobj = obj
1281+
# Find globalns for the unwrapped object.
1282+
while hasattr(nsobj, '__wrapped__'):
1283+
nsobj = nsobj.__wrapped__
1284+
globalns = getattr(nsobj, '__globals__', {})
1285+
if localns is None:
1286+
localns = globalns
1287+
elif localns is None:
1288+
localns = globalns
1289+
if sys.version_info < (3, 9):
1290+
ref = ForwardRef(original_value)
1291+
else:
1292+
ref = ForwardRef(
1293+
original_value,
1294+
is_argument=not isinstance(obj, _types.ModuleType)
1295+
)
1296+
original_value = typing._eval_type(ref, globalns, localns)
1297+
# Values was not modified or original is already Optional
1298+
if original_value == value or _could_be_inserted_optional(original_value):
1299+
continue
1300+
# NoneType was added to value
1301+
if len(value.__args__) == 2:
1302+
hints[name] = value.__args__[0] # not a Union
1303+
else:
1304+
hints[name] = Union[value.__args__[:-1]] # still a Union
1305+
1306+
return hints
1307+
12431308

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

0 commit comments

Comments
 (0)