From 4dfc9703d28bbec830b173a8ec9caa4e6217b41e Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 17 Apr 2022 21:26:57 -0700 Subject: [PATCH 1/4] gh-91621: Fix typing.get_type_hints for collections.abc.Callable This mirrors logic in typing.get_args. The trickiness comes from how we flatten args in collections.abc.Callable, see https://bugs.python.org/issue42195 --- Lib/test/test_typing.py | 6 ++++++ Lib/typing.py | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index d4808474e4fcee..2277c4a3502b98 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4408,6 +4408,12 @@ def test_get_type_hints_typeddict(self): 'a': Annotated[Required[int], "a", "b", "c"] }) + def test_get_type_hints_collections_abc_callable(self): + # https://github.com/python/cpython/issues/91621 + def f(x: collections.abc.Callable[[int], int]): ... + + self.assertEqual(get_type_hints(f), {'x': collections.abc.Callable[[int], int]}) + class GetUtilitiesTestCase(TestCase): def test_get_origin(self): diff --git a/Lib/typing.py b/Lib/typing.py index 3e0fbdb9891557..ae2f30ffcfad95 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -350,7 +350,11 @@ def _eval_type(t, globalns, localns, recursive_guard=frozenset()): ForwardRef(arg) if isinstance(arg, str) else arg for arg in t.__args__ ) - t = t.__origin__[args] + if (t.__origin__ is collections.abc.Callable + and not (len(args) == 2 and _is_param_expr(args[0]))): + t = t.__origin__[(args[:-1], args[-1])] + else: + t = t.__origin__[args] ev_args = tuple(_eval_type(a, globalns, localns, recursive_guard) for a in t.__args__) if ev_args == t.__args__: return t From e8c44016a3c5862e6de9a9946432e754f98280e1 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 18 Apr 2022 18:55:24 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2022-04-18-18-55-21.gh-issue-91621.ACNlda.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2022-04-18-18-55-21.gh-issue-91621.ACNlda.rst diff --git a/Misc/NEWS.d/next/Library/2022-04-18-18-55-21.gh-issue-91621.ACNlda.rst b/Misc/NEWS.d/next/Library/2022-04-18-18-55-21.gh-issue-91621.ACNlda.rst new file mode 100644 index 00000000000000..b9e68d225a25bd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-04-18-18-55-21.gh-issue-91621.ACNlda.rst @@ -0,0 +1 @@ +Fix :func:`typing.get_type_hints` for :class:`collections.abc.Callable`. Patch by Shantanu Jain. From 6fecdfa0b3d5f55bea3ac11f7e84c1eff7cf19de Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 18 Apr 2022 13:48:00 -0700 Subject: [PATCH 3/4] more tests --- Lib/test/test_typing.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 2277c4a3502b98..76eb578f9e3911 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4410,9 +4410,14 @@ def test_get_type_hints_typeddict(self): def test_get_type_hints_collections_abc_callable(self): # https://github.com/python/cpython/issues/91621 + P = ParamSpec('P') def f(x: collections.abc.Callable[[int], int]): ... + def g(x: collections.abc.Callable[..., int]): ... + def h(x: collections.abc.Callable[P, int]): ... self.assertEqual(get_type_hints(f), {'x': collections.abc.Callable[[int], int]}) + self.assertEqual(get_type_hints(g), {'x': collections.abc.Callable[..., int]}) + self.assertEqual(get_type_hints(h), {'x': collections.abc.Callable[P, int]}) class GetUtilitiesTestCase(TestCase): From b45397fb82d88804e29f94efe4d9ad3541d7cdc5 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 2 May 2022 14:25:40 -0600 Subject: [PATCH 4/4] add helper --- Lib/typing.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index ae2f30ffcfad95..2d68438d9fe7ba 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -202,6 +202,24 @@ def _is_param_expr(arg): (tuple, list, ParamSpec, _ConcatenateGenericAlias)) +def _should_unflatten_callable_args(typ, args): + """Internal helper for munging collections.abc.Callable's __args__. + + The canonical representation for a Callable's __args__ flattens the + argument types, see https://bugs.python.org/issue42195. For example: + + collections.abc.Callable[[int, int], str].__args__ == (int, int, str) + collections.abc.Callable[ParamSpec, str].__args__ == (ParamSpec, str) + + As a result, if we need to reconstruct the Callable from its __args__, + we need to unflatten it. + """ + return ( + typ.__origin__ is collections.abc.Callable + and not (len(args) == 2 and _is_param_expr(args[0])) + ) + + def _type_repr(obj): """Return the repr() of an object, special-casing types (internal helper). @@ -350,8 +368,7 @@ def _eval_type(t, globalns, localns, recursive_guard=frozenset()): ForwardRef(arg) if isinstance(arg, str) else arg for arg in t.__args__ ) - if (t.__origin__ is collections.abc.Callable - and not (len(args) == 2 and _is_param_expr(args[0]))): + if _should_unflatten_callable_args(t, args): t = t.__origin__[(args[:-1], args[-1])] else: t = t.__origin__[args] @@ -2350,8 +2367,7 @@ def get_args(tp): return (tp.__origin__,) + tp.__metadata__ if isinstance(tp, (_GenericAlias, GenericAlias)): res = tp.__args__ - if (tp.__origin__ is collections.abc.Callable - and not (len(res) == 2 and _is_param_expr(res[0]))): + if _should_unflatten_callable_args(tp, res): res = (list(res[:-1]), res[-1]) return res if isinstance(tp, types.UnionType):