diff --git a/Lib/functools.py b/Lib/functools.py index a92844ba7227b0..0f2b5b23d9f7b1 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -935,6 +935,32 @@ def _is_valid_dispatch_type(cls): return (isinstance(cls, UnionType) and all(isinstance(arg, type) for arg in cls.__args__)) + 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( + 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 +977,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_func_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 f7e09fd771eaf2..2853c6a53a984c 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]): 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 00000000000000..3e4e97a9c8a70f --- /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`.