diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index c6f08ff8a052ab..a2b76ed1303a33 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6837,6 +6837,24 @@ def test_get_type_hints_wrapped_decoratored_func(self): self.assertEqual(gth(ForRefExample.func), expects) self.assertEqual(gth(ForRefExample.nested), expects) + def test_get_type_hints_wrapped_cycle_self(self): + # gh-146553: __wrapped__ self-reference must raise ValueError, + # not loop forever. + def f(x: int) -> str: ... + f.__wrapped__ = f + with self.assertRaisesRegex(ValueError, 'wrapper loop'): + get_type_hints(f) + + def test_get_type_hints_wrapped_cycle_mutual(self): + # gh-146553: mutual __wrapped__ cycle (a -> b -> a) must raise + # ValueError, not loop forever. + def a(): ... + def b(): ... + a.__wrapped__ = b + b.__wrapped__ = a + with self.assertRaisesRegex(ValueError, 'wrapper loop'): + get_type_hints(a) + def test_get_type_hints_annotated(self): def foobar(x: List['X']): ... X = Annotated[int, (1, 10)] diff --git a/Lib/typing.py b/Lib/typing.py index e78fb8b71a996c..7e97e89e4ed1de 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -172,6 +172,16 @@ def __getattr__(self, attr): _lazy_annotationlib = _LazyAnnotationLib() +class _LazyInspect: + def __getattr__(self, attr): + global _lazy_inspect + import inspect + _lazy_inspect = inspect + return getattr(inspect, attr) + +_lazy_inspect = _LazyInspect() + + def _type_convert(arg, module=None, *, allow_special_forms=False, owner=None): """For converting None to type(None), and strings to ForwardRef.""" if arg is None: @@ -1962,16 +1972,6 @@ def _allow_reckless_class_checks(depth=2): } -@functools.cache -def _lazy_load_getattr_static(): - # Import getattr_static lazily so as not to slow down the import of typing.py - # Cache the result so we don't slow down _ProtocolMeta.__instancecheck__ unnecessarily - from inspect import getattr_static - return getattr_static - - -_cleanups.append(_lazy_load_getattr_static.cache_clear) - def _pickle_psargs(psargs): return ParamSpecArgs, (psargs.__origin__,) @@ -2104,7 +2104,7 @@ def __instancecheck__(cls, instance): if _abc_instancecheck(cls, instance): return True - getattr_static = _lazy_load_getattr_static() + getattr_static = _lazy_inspect.getattr_static for attr in cls.__protocol_attrs__: try: val = getattr_static(instance, attr) @@ -2483,10 +2483,8 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, if isinstance(obj, types.ModuleType): globalns = obj.__dict__ else: - nsobj = obj # Find globalns for the unwrapped object. - while hasattr(nsobj, '__wrapped__'): - nsobj = nsobj.__wrapped__ + nsobj = _lazy_inspect.unwrap(obj) globalns = getattr(nsobj, '__globals__', {}) if localns is None: localns = globalns diff --git a/Misc/NEWS.d/next/Library/2026-03-28-11-43-25.gh-issue-146553.mXaWza.rst b/Misc/NEWS.d/next/Library/2026-03-28-11-43-25.gh-issue-146553.mXaWza.rst new file mode 100644 index 00000000000000..b75cdc14725a0c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-28-11-43-25.gh-issue-146553.mXaWza.rst @@ -0,0 +1,4 @@ +Fix :func:`typing.get_type_hints` hanging indefinitely when a callable has a +circular ``__wrapped__`` chain (e.g. ``f.__wrapped__ = f``). A +:exc:`ValueError` is now raised on cycle detection, matching the behavior of +:func:`inspect.unwrap`.