diff --git a/Lib/functools.py b/Lib/functools.py index 714070c6ac9460..b5d73ed8fd194e 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -869,15 +869,7 @@ def _find_impl(cls, registry): match = t return registry.get(match) -def singledispatch(func): - """Single-dispatch generic function decorator. - - Transforms a function into a generic function, which can have different - behaviours depending upon the type of its first argument. The decorated - function acts as the default implementation, and additional - implementations can be registered using the register() attribute of the - generic function. - """ +def _singledispatchimpl(func, *, is_method): # There are many programs that use functools without singledispatch, so we # trade-off making singledispatch marginally slower for the benefit of # making start-up of such applications slightly faster. @@ -916,6 +908,32 @@ def _is_valid_dispatch_type(cls): return (isinstance(cls, UnionType) and all(isinstance(arg, type) for arg in cls.__args__)) + def _skip_self_type(argname, cls, hints_iter): + # GH-130827: Methods are sometimes annotated with + # typing.Self. We should skip that when it's a valid type. + from typing import Self + if cls is not Self: + return argname, cls + if not is_method: + # typing.Self is not valid in a normal function + raise TypeError( + f"Invalid annotation for {argname!r}. " + "typing.Self can only be used with singledispatchmethod()" + ) + try: + argname, cls = next(hints_iter) + return argname, cls + except StopIteration: + # The method is one of some invalid edge cases: + # 1. method(self: Self) -> ... + # 2. method(self, weird: Self) -> ... + # 3. method(self: Self, unannotated) -> ... + raise TypeError( + f"Invalid annotation for {argname!r}. " + "typing.Self must be the first annotation and must " + "have a second parameter with an annotation" + ) from None + def register(cls, func=None): """generic_func.register(cls, func) -> func @@ -944,7 +962,10 @@ def register(cls, func=None): # 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())) + hints_iter = iter(get_type_hints(func, format=Format.FORWARDREF).items()) + argname, cls = next(hints_iter) + argname, cls = _skip_self_type(argname, cls, hints_iter) + if not _is_valid_dispatch_type(cls): if isinstance(cls, UnionType): raise TypeError( @@ -987,6 +1008,16 @@ def wrapper(*args, **kw): update_wrapper(wrapper, func) return wrapper +def singledispatch(func): + """Single-dispatch generic function decorator. + + Transforms a function into a generic function, which can have different + behaviours depending upon the type of its first argument. The decorated + function acts as the default implementation, and additional + implementations can be registered using the register() attribute of the + generic function. + """ + return _singledispatchimpl(func, is_method=False) # Descriptor version class singledispatchmethod: @@ -1000,7 +1031,7 @@ def __init__(self, func): if not callable(func) and not hasattr(func, "__get__"): raise TypeError(f"{func!r} is not callable or a descriptor") - self.dispatcher = singledispatch(func) + self.dispatcher = _singledispatchimpl(func, is_method=True) self.func = func def register(self, cls, method=None): diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 4794a7465f0b66..bd3414083293f8 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3454,6 +3454,62 @@ def _(item, arg: bytes) -> str: self.assertEqual(str(Signature.from_callable(A.static_func)), '(item, arg: int) -> str') + def test_typing_self(self): + # gh-130827: typing.Self with singledispatchmethod() didn't work + class Foo: + @functools.singledispatchmethod + def bar(self: typing.Self, arg: int | str) -> int | str: ... + + @bar.register + def _(self: typing.Self, arg: int) -> int: + return arg + + + foo = Foo() + self.assertEqual(foo.bar(42), 42) + + # But, it shouldn't work on singledispatch() + @functools.singledispatch + def test(self: typing.Self, arg: int | str) -> int | str: + pass + with self.assertRaises(TypeError): + @test.register + def silly(self: typing.Self, arg: int | str) -> int | str: + pass + + # typing.Self cannot be the only annotation + with self.assertRaises(TypeError): + class Foo: + @functools.singledispatchmethod + def bar(self: typing.Self, arg: int | str): + pass + + @bar.register + def _(self: typing.Self, arg): + return arg + + # typing.Self can only be used in the first parameter + with self.assertRaises(TypeError): + class Foo: + @functools.singledispatchmethod + def bar(self, arg: int | str): + pass + + @bar.register + def _(self, arg: typing.Self): + return arg + + # 'self' cannot be the only parameter + with self.assertRaises(TypeError): + class Foo: + @functools.singledispatchmethod + def bar(self: typing.Self, arg: int | str): + pass + + @bar.register + def _(self: typing.Self): + pass + class CachedCostItem: _cost = 1 diff --git a/Misc/NEWS.d/next/Library/2025-03-04-06-57-56.gh-issue-130827.XZacmc.rst b/Misc/NEWS.d/next/Library/2025-03-04-06-57-56.gh-issue-130827.XZacmc.rst new file mode 100644 index 00000000000000..3d51ffba47b284 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-04-06-57-56.gh-issue-130827.XZacmc.rst @@ -0,0 +1,2 @@ +Fix :exc:`TypeError` when using :func:`functools.singledispatchmethod` with +a :class:`typing.Self` annotation.