Skip to content

Commit 0d72cec

Browse files
committed
Deprecate singledispatch registration by return annotation
1 parent 371bff5 commit 0d72cec

6 files changed

Lines changed: 77 additions & 215 deletions

File tree

Doc/deprecations/pending-removal-in-3.18.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,11 @@ Pending removal in Python 3.18
77
specifier ``'N'``, which is only supported in the :mod:`!decimal` module's
88
C implementation, has been deprecated since Python 3.13.
99
(Contributed by Serhiy Storchaka in :gh:`89902`.)
10+
11+
* :mod:`functools`:
12+
13+
* Using a return annotation to infer the dispatch type for
14+
:func:`functools.singledispatch` or
15+
:class:`functools.singledispatchmethod` is deprecated since Python 3.16
16+
and will raise :exc:`TypeError` in Python 3.18. Annotate the dispatch
17+
parameter or pass the dispatch type explicitly instead.

Doc/library/functools.rst

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,9 @@ The :mod:`!functools` module defines the following functions:
506506
:no-typesetting:
507507

508508
To add overloaded implementations to the function, use the :func:`!register`
509-
attribute of the generic function, which can be used as a decorator. For
510-
functions annotated with types, the decorator will infer the type of the
511-
first argument automatically::
509+
attribute of the generic function, which can be used as a decorator. For
510+
functions annotated with types, the decorator can infer the dispatch type
511+
automatically::
512512

513513
>>> @fun.register
514514
... def _(arg: int, verbose=False):
@@ -540,8 +540,9 @@ The :mod:`!functools` module defines the following functions:
540540
... print(i, elem)
541541
...
542542

543-
For code which doesn't use type annotations, the appropriate type
544-
argument can be passed explicitly to the decorator itself::
543+
For code which doesn't use type annotations, or where the first resolved
544+
annotation does not belong to the dispatch argument, the appropriate type
545+
can be passed explicitly to the decorator itself::
545546

546547
>>> @fun.register(complex)
547548
... def _(arg, verbose=False):
@@ -662,6 +663,12 @@ The :mod:`!functools` module defines the following functions:
662663
The :func:`~singledispatch.register` attribute now supports
663664
:class:`typing.Union` as a type annotation.
664665

666+
.. deprecated:: 3.16
667+
Using a return annotation to infer the dispatch type is deprecated and
668+
will raise :exc:`TypeError` in Python 3.18. Annotate the dispatch
669+
parameter or pass the dispatch type explicitly to :func:`!register`
670+
instead.
671+
665672

666673
.. class:: singledispatchmethod(func)
667674

@@ -686,6 +693,20 @@ The :mod:`!functools` module defines the following functions:
686693
def _(self, arg: bool):
687694
return not arg
688695

696+
When *self* or *cls* is annotated, pass the dispatch type explicitly rather
697+
than relying on annotation inference::
698+
699+
from typing import Self
700+
701+
class Negator:
702+
@singledispatchmethod
703+
def neg(self, arg):
704+
raise NotImplementedError("Cannot negate a")
705+
706+
@neg.register(int)
707+
def _(self: Self, arg: int):
708+
return -arg
709+
689710
``@singledispatchmethod`` supports nesting with other decorators such as
690711
:deco:`classmethod`. Note that to allow for
691712
``dispatcher.register``, ``singledispatchmethod`` must be the *outer most*

Doc/whatsnew/3.15.rst

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -748,13 +748,6 @@ functools
748748
if it wraps a regular method and is called as a class attribute.
749749
(Contributed by Bartosz Sławecki in :gh:`143535`.)
750750

751-
* When registering a callable or descriptor to :func:`functools.singledispatch`
752-
or :func:`functools.singledispatchmethod` via a type annotation,
753-
it is now validated that the first non-*self* or non-*cls* parameter is used.
754-
Previously, a callable could be registered based on its return type
755-
annotation or an irrelevant parameter type annotation.
756-
(Contributed by Bartosz Sławecki in :gh:`84644`.)
757-
758751

759752
hashlib
760753
-------

Lib/functools.py

Lines changed: 15 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -893,45 +893,6 @@ def _find_impl(cls, registry):
893893
match = t
894894
return registry.get(match)
895895

896-
def _get_singledispatch_annotated_param(func, role):
897-
"""Find the first positional and user-specified parameter in a callable
898-
or descriptor.
899-
900-
Used by singledispatch for registration by type annotation of the parameter.
901-
"""
902-
if isinstance(func, staticmethod):
903-
idx = 0
904-
func = func.__func__
905-
elif isinstance(func, (classmethod, MethodType)):
906-
idx = 1 # Skip parameter bound by instance-level access.
907-
func = func.__func__
908-
else:
909-
# Assume and skip bound parameter when singledispatchmethod is used.
910-
idx = 1 if role == "method" else 0
911-
# Fast path: emulate `inspect._signature_from_function` if possible.
912-
if isinstance(func, FunctionType) and not hasattr(func, "__wrapped__"):
913-
func_code = func.__code__
914-
try:
915-
return func_code.co_varnames[:func_code.co_argcount][idx]
916-
except IndexError:
917-
pass
918-
# Fall back to `inspect.signature` (slower, but complete).
919-
import inspect
920-
params = list(inspect.signature(func).parameters.values())
921-
try:
922-
param = params[idx]
923-
except IndexError:
924-
pass
925-
else:
926-
# Allow variadic positional '(*args)' parameters for backward compatibility.
927-
if param.kind not in (inspect.Parameter.KEYWORD_ONLY,
928-
inspect.Parameter.VAR_KEYWORD):
929-
return param.name
930-
raise TypeError(
931-
f"Invalid first argument to `register()`: {func!r} "
932-
f"does not accept positional arguments."
933-
)
934-
935896
def singledispatch(func):
936897
"""Single-dispatch generic function decorator.
937898
@@ -979,7 +940,7 @@ def _is_valid_dispatch_type(cls):
979940
return (isinstance(cls, UnionType) and
980941
all(isinstance(arg, type) for arg in cls.__args__))
981942

982-
def register(cls, func=None, *, _role="function"):
943+
def register(cls, func=None):
983944
"""generic_func.register(cls, func) -> func
984945
985946
Registers a new implementation for the given *cls* on a *generic_func*.
@@ -1004,22 +965,11 @@ def register(cls, func=None, *, _role="function"):
1004965
)
1005966
func = cls
1006967

1007-
argname = _get_singledispatch_annotated_param(func, role=_role)
1008-
1009968
# only import typing if annotation parsing is necessary
1010969
from typing import get_type_hints
1011970
from annotationlib import Format, ForwardRef
1012-
annotations = get_type_hints(func, format=Format.FORWARDREF)
1013-
1014-
try:
1015-
cls = annotations[argname]
1016-
except KeyError:
1017-
raise TypeError(
1018-
f"Invalid first argument to `register()`: {func!r}. "
1019-
f"Use either `@register(some_class)` or add a type "
1020-
f"annotation to parameter {argname!r} of your callable."
1021-
) from None
1022-
971+
argname, cls = next(iter(get_type_hints(
972+
func, format=Format.FORWARDREF).items()))
1023973
if not _is_valid_dispatch_type(cls):
1024974
if isinstance(cls, UnionType):
1025975
raise TypeError(
@@ -1036,6 +986,17 @@ def register(cls, func=None, *, _role="function"):
1036986
f"Invalid annotation for {argname!r}. "
1037987
f"{cls!r} is not a class."
1038988
)
989+
if argname == 'return':
990+
import os
991+
import warnings
992+
warnings.warn(
993+
"Using the return annotation to infer the dispatch type "
994+
"is deprecated and will raise TypeError in Python 3.18. "
995+
"Annotate the dispatch parameter or pass the dispatch "
996+
"type explicitly to register().",
997+
DeprecationWarning,
998+
skip_file_prefixes=(os.path.dirname(__file__),),
999+
)
10391000

10401001
if isinstance(cls, UnionType):
10411002
for arg in cls.__args__:
@@ -1056,7 +1017,6 @@ def wrapper(*args, **kw):
10561017
funcname = getattr(func, '__name__', 'singledispatch function')
10571018
registry[object] = func
10581019
wrapper.register = register
1059-
wrapper.register.__text_signature__ = "(cls, func=None)" # Hide _role from help().
10601020
wrapper.dispatch = dispatch
10611021
wrapper.registry = MappingProxyType(registry)
10621022
wrapper._clear_cache = dispatch_cache.clear
@@ -1084,7 +1044,7 @@ def register(self, cls, method=None):
10841044
10851045
Registers a new implementation for the given *cls* on a *generic_method*.
10861046
"""
1087-
return self.dispatcher.register(cls, func=method, _role="method")
1047+
return self.dispatcher.register(cls, func=method)
10881048

10891049
def __get__(self, obj, cls=None):
10901050
return _singledispatchmethod_get(self, obj, cls)

0 commit comments

Comments
 (0)