From dfdf7ac438f43c3c89b8eec4981b81e2c3329091 Mon Sep 17 00:00:00 2001 From: bswck Date: Fri, 17 Oct 2025 15:14:23 +0200 Subject: [PATCH 1/3] Don't single-dispatch on return types --- Lib/functools.py | 41 ++++++++++++++++++++++++++++---------- Lib/test/test_functools.py | 12 +++++++++++ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index a92844ba722..218185c03a2 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -935,6 +935,31 @@ def _is_valid_dispatch_type(cls): return (isinstance(cls, UnionType) and all(isinstance(arg, type) for arg in cls.__args__)) + def _get_type_hints(func): + ann = getattr(func, '__annotate__', None) + if ann is None: + raise TypeError( + f"Invalid first argument to `register()`: {func!r}. " + f"Use either `@register(some_class)` or plain `@register` " + f"on an annotated function." + ) + + # only import typing if annotation parsing is necessary + from typing import get_type_hints + from annotationlib import Format + + type_hints = get_type_hints(func, format=Format.FORWARDREF) + type_hints.pop("return", None) # don't dispatch on return types + + if not type_hints: + raise TypeError( + f"Invalid first argument to `register()`: {func!r}. " + f"Use either `@register(some_class)` or plain `@register` " + f"on a function with annotated parameters." + ) + + return type_hints + def register(cls, func=None): """generic_func.register(cls, func) -> func @@ -951,20 +976,14 @@ def register(cls, func=None): f"Invalid first argument to `register()`. " f"{cls!r} is not a class or union type." ) - ann = getattr(cls, '__annotate__', None) - if ann is None: - raise TypeError( - f"Invalid first argument to `register()`: {cls!r}. " - f"Use either `@register(some_class)` or plain `@register` " - f"on an annotated function." - ) func = cls + type_hints = _get_type_hints(func) + + argname, cls = next(iter(type_hints.items())) - # only import typing if annotation parsing is necessary - from typing import get_type_hints - from annotationlib import Format, ForwardRef - argname, cls = next(iter(get_type_hints(func, format=Format.FORWARDREF).items())) if not _is_valid_dispatch_type(cls): + from annotationlib import ForwardRef + if isinstance(cls, UnionType): raise TypeError( f"Invalid annotation for {argname!r}. " diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index f7e09fd771e..2853c6a53a9 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3180,6 +3180,18 @@ def _(arg): ) self.assertEndsWith(str(exc.exception), msg_suffix) + with self.assertRaises(TypeError) as exc: + @i.register + def _(arg) -> str: + return "I only have a return type annotation" + self.assertStartsWith(str(exc.exception), msg_prefix + + "._" + ) + self.assertEndsWith(str(exc.exception), + ". Use either `@register(some_class)` or plain `@register` on " + "a function with annotated parameters." + ) + with self.assertRaises(TypeError) as exc: @i.register def _(arg: typing.Iterable[str]): From cf1da17c23e652ba64c1fbe5b8077c6605104628 Mon Sep 17 00:00:00 2001 From: bswck Date: Fri, 17 Oct 2025 15:29:12 +0200 Subject: [PATCH 2/3] Add news entry --- .../Library/2025-10-17-15-25-38.gh-issue-84644.eAEJXy.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-10-17-15-25-38.gh-issue-84644.eAEJXy.rst diff --git a/Misc/NEWS.d/next/Library/2025-10-17-15-25-38.gh-issue-84644.eAEJXy.rst b/Misc/NEWS.d/next/Library/2025-10-17-15-25-38.gh-issue-84644.eAEJXy.rst new file mode 100644 index 00000000000..3e4e97a9c8a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-17-15-25-38.gh-issue-84644.eAEJXy.rst @@ -0,0 +1,4 @@ +A :exc:`TypeError` is raised by :py:func:`functools.singledispatch` +if it is attempted to register function that only annotates its return type. + +Contributed by Bartosz Sławecki in :gh:`84644`. From 041ceaa1fbbcb456aea8050f57ae1ab6acb96587 Mon Sep 17 00:00:00 2001 From: bswck Date: Fri, 17 Oct 2025 16:20:18 +0200 Subject: [PATCH 3/3] Change function name and add docs --- Lib/functools.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index 218185c03a2..0f2b5b23d9f 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -935,7 +935,8 @@ def _is_valid_dispatch_type(cls): return (isinstance(cls, UnionType) and all(isinstance(arg, type) for arg in cls.__args__)) - def _get_type_hints(func): + def _get_func_type_hints(func): + """Called when type hints are needed to choose the first argument to dispatch on.""" ann = getattr(func, '__annotate__', None) if ann is None: raise TypeError( @@ -977,7 +978,7 @@ def register(cls, func=None): f"{cls!r} is not a class or union type." ) func = cls - type_hints = _get_type_hints(func) + type_hints = _get_func_type_hints(func) argname, cls = next(iter(type_hints.items()))