Skip to content

bpo-46195: Do not add Optional in get_type_hints #30304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2030,9 +2030,7 @@ Introspection helpers

This is often the same as ``obj.__annotations__``. In addition,
forward references encoded as string literals are handled by evaluating
them in ``globals`` and ``locals`` namespaces. If necessary,
``Optional[t]`` is added for function and method annotations if a default
value equal to ``None`` is set. For a class ``C``, return
them in ``globals`` and ``locals`` namespaces. For a class ``C``, return
a dictionary constructed by merging all the ``__annotations__`` along
``C.__mro__`` in reverse order.

Expand All @@ -2059,6 +2057,11 @@ Introspection helpers
.. versionchanged:: 3.9
Added ``include_extras`` parameter as part of :pep:`593`.

.. versionchanged:: 3.11
Previously, ``Optional[t]`` was added for function and method annotations
if a default value equal to ``None`` was set.
Now the annotation is returned unchanged.

.. function:: get_args(tp)
.. function:: get_origin(tp)

Expand Down
19 changes: 15 additions & 4 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2586,16 +2586,15 @@ def add_right(self, node: 'Node[T]' = None):
t = Node[int]
both_hints = get_type_hints(t.add_both, globals(), locals())
self.assertEqual(both_hints['left'], Optional[Node[T]])
self.assertEqual(both_hints['right'], Optional[Node[T]])
self.assertEqual(both_hints['left'], both_hints['right'])
self.assertEqual(both_hints['stuff'], Optional[int])
self.assertEqual(both_hints['right'], Node[T])
self.assertEqual(both_hints['stuff'], int)
self.assertNotIn('blah', both_hints)

left_hints = get_type_hints(t.add_left, globals(), locals())
self.assertEqual(left_hints['node'], Optional[Node[T]])

right_hints = get_type_hints(t.add_right, globals(), locals())
self.assertEqual(right_hints['node'], Optional[Node[T]])
self.assertEqual(right_hints['node'], Node[T])

def test_forwardref_instance_type_error(self):
fr = typing.ForwardRef('int')
Expand Down Expand Up @@ -3259,6 +3258,18 @@ def __iand__(self, other: Const["MySet[T]"]) -> "MySet[T]":
{'other': MySet[T], 'return': MySet[T]}
)

def test_get_type_hints_annotated_with_none_default(self):
# See: https://bugs.python.org/issue46195
def annotated_with_none_default(x: Annotated[int, 'data'] = None): ...
self.assertEqual(
get_type_hints(annotated_with_none_default),
{'x': int},
)
self.assertEqual(
get_type_hints(annotated_with_none_default, include_extras=True),
{'x': Annotated[int, 'data']},
)

def test_get_type_hints_classes_str_annotations(self):
class Foo:
y = str
Expand Down
29 changes: 2 additions & 27 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1728,26 +1728,6 @@ def cast(typ, val):
return val


def _get_defaults(func):
"""Internal helper to extract the default arguments, by name."""
try:
code = func.__code__
except AttributeError:
# Some built-in functions don't have __code__, __defaults__, etc.
return {}
pos_count = code.co_argcount
arg_names = code.co_varnames
arg_names = arg_names[:pos_count]
defaults = func.__defaults__ or ()
kwdefaults = func.__kwdefaults__
res = dict(kwdefaults) if kwdefaults else {}
pos_offset = pos_count - len(defaults)
for name, value in zip(arg_names[pos_offset:], defaults):
assert name not in res
res[name] = value
return res


_allowed_types = (types.FunctionType, types.BuiltinFunctionType,
types.MethodType, types.ModuleType,
WrapperDescriptorType, MethodWrapperType, MethodDescriptorType)
Expand All @@ -1757,8 +1737,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
"""Return type hints for an object.

This is often the same as obj.__annotations__, but it handles
forward references encoded as string literals, adds Optional[t] if a
default value equal to None is set and recursively replaces all
forward references encoded as string literals and recursively replaces all
'Annotated[T, ...]' with 'T' (unless 'include_extras=True').

The argument may be a module, class, method, or function. The annotations
Expand Down Expand Up @@ -1838,7 +1817,6 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
else:
raise TypeError('{!r} is not a module, class, method, '
'or function.'.format(obj))
defaults = _get_defaults(obj)
hints = dict(hints)
for name, value in hints.items():
if value is None:
Expand All @@ -1851,10 +1829,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
is_argument=not isinstance(obj, types.ModuleType),
is_class=False,
)
value = _eval_type(value, globalns, localns)
if name in defaults and defaults[name] is None:
value = Optional[value]
hints[name] = value
hints[name] = _eval_type(value, globalns, localns)
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:func:`typing.get_type_hints` no longer adds ``Optional`` to parameters with
``None`` as a default. This aligns to changes to PEP 484 in
https://github.com/python/peps/pull/689