From 73433f7434e9a69a5aca683a5aa094b6dbab4f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Sun, 3 May 2020 00:30:42 +0100 Subject: [PATCH 1/2] bpo-40464: fix return annotation being used as the arg annotation in singledispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Lib/functools.py | 11 ++++++++--- .../Library/2020-05-02-23-46-25.bpo-40464.MxGkfE.rst | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-05-02-23-46-25.bpo-40464.MxGkfE.rst diff --git a/Lib/functools.py b/Lib/functools.py index b1f1fe8d9a6f27..a86a579e863777 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -837,7 +837,7 @@ def dispatch(cls): dispatch_cache[cls] = impl return impl - def register(cls, func=None): + def register(cls, func=None, _arg_index=0): """generic_func.register(cls, func) -> func Registers a new implementation for the given *cls* on a *generic_func*. @@ -847,8 +847,13 @@ def register(cls, func=None): if func is None: if isinstance(cls, type): return lambda f: register(cls, f) + import inspect + try: + args = list(inspect.signature(cls).parameters.keys()) + except TypeError: + args = [] ann = getattr(cls, '__annotations__', {}) - if not ann: + if len(args) <= _arg_index or args[_arg_index] not in ann: raise TypeError( f"Invalid first argument to `register()`: {cls!r}. " f"Use either `@register(some_class)` or plain `@register` " @@ -907,7 +912,7 @@ def register(self, cls, method=None): Registers a new implementation for the given *cls* on a *generic_method*. """ - return self.dispatcher.register(cls, func=method) + return self.dispatcher.register(cls, func=method, _arg_index=1) def __get__(self, obj, cls=None): def _method(*args, **kwargs): diff --git a/Misc/NEWS.d/next/Library/2020-05-02-23-46-25.bpo-40464.MxGkfE.rst b/Misc/NEWS.d/next/Library/2020-05-02-23-46-25.bpo-40464.MxGkfE.rst new file mode 100644 index 00000000000000..b56816638aa34c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-02-23-46-25.bpo-40464.MxGkfE.rst @@ -0,0 +1 @@ +Fix return annotation being used as the arg annotation in functools.singledispatch \ No newline at end of file From d8305d46c5e75368b9043977dcbf93d455db063d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Sun, 3 May 2020 00:52:09 +0100 Subject: [PATCH 2/2] test: functools: add test for bpo-40464 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Lib/test/test_functools.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index bee9f9112bf183..ad65538d977317 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -2441,6 +2441,39 @@ def _(arg: typing.Iterable[str]): 'typing.Iterable[str] is not a class.' )) + with self.assertRaises(TypeError) as exc: + @i.register + def _(arg) -> str: + # At runtime, dispatching on generics is impossible. + # When registering implementations with singledispatch, avoid + # types from `typing`. Instead, annotate with regular types + # or ABCs. + return "I annotated with a generic collection" + self.assertTrue(str(exc.exception).startswith( + "Invalid first argument to `register()`:" + )) + self.assertTrue(str(exc.exception).endswith( + 'Use either `@register(some_class)` or plain `@register` on an annotated function.' + )) + + @functools.singledispatch + def i(arg1, arg2): + return "base" + with self.assertRaises(TypeError) as exc: + @i.register + def _(arg1, arg2: int) -> str: + # At runtime, dispatching on generics is impossible. + # When registering implementations with singledispatch, avoid + # types from `typing`. Instead, annotate with regular types + # or ABCs. + return "I annotated with a generic collection" + self.assertTrue(str(exc.exception).startswith( + "Invalid first argument to `register()`:" + )) + self.assertTrue(str(exc.exception).endswith( + 'Use either `@register(some_class)` or plain `@register` on an annotated function.' + )) + def test_invalid_positional_argument(self): @functools.singledispatch def f(*args):