Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
bpo-46195: get_type_hints does not add Optional anymore
  • Loading branch information
sobolevn committed Dec 30, 2021
commit dd3a750c0118cbc02307f2fc2a2854d2956aaeaf
4 changes: 1 addition & 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,
Comment thread
gpshead marked this conversation as resolved.
``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 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,4 @@
Now :func:`typing.get_type_hints` does not add ``Optional`` to types with
explicit ``None`` default value.

This was changed in https://github.com/python/peps/pull/689
Comment thread
sobolevn marked this conversation as resolved.
Outdated