]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.9] bpo-45678: Fix `singledispatchmethod` `classmethod`/`staticmethod` bug (GH...
authorAlex Waygood <Alex.Waygood@Gmail.com>
Thu, 4 Nov 2021 19:34:14 +0000 (19:34 +0000)
committerGitHub <noreply@github.com>
Thu, 4 Nov 2021 19:34:14 +0000 (20:34 +0100)
This PR fixes a bug in the 3.9 branch where
``functools.singledispatchmethod`` did not properly wrap attributes such as
``__name__``, ``__doc__`` and ``__module__`` of the target method. It also
backports tests already merged into the 3.11 and 3.10 branches in #29328 and
#29390.

Co-authored-by: Ɓukasz Langa <lukasz@langa.pl>
Lib/functools.py
Lib/test/test_functools.py
Misc/NEWS.d/next/Library/2021-11-03-17-28-43.bpo-45678.Zj_O8j.rst [new file with mode: 0644]

index 5054e281ad281496c4911437040fc41aef62a3cd..1a290e1c02461be4fdf00330595a5a33e82d7347 100644 (file)
@@ -901,6 +901,13 @@ class singledispatchmethod:
         self.dispatcher = singledispatch(func)
         self.func = func
 
+        # bpo-45678: special-casing for classmethod/staticmethod in Python <=3.9,
+        # as functools.update_wrapper doesn't work properly in singledispatchmethod.__get__
+        # if it is applied to an unbound classmethod/staticmethod
+        if isinstance(func, (staticmethod, classmethod)):
+            self._wrapped_func = func.__func__
+        else:
+            self._wrapped_func = func
     def register(self, cls, method=None):
         """generic_method.register(cls, func) -> func
 
@@ -921,7 +928,7 @@ class singledispatchmethod:
 
         _method.__isabstractmethod__ = self.__isabstractmethod__
         _method.register = self.register
-        update_wrapper(_method, self.func)
+        update_wrapper(_method, self._wrapped_func)
         return _method
 
     @property
index 96e93ed8eab344f6ebcfe18c19dbe30d029c0038..b2a7e5a88b422b2a38045eea18855e3fbac67eb8 100644 (file)
@@ -2401,7 +2401,7 @@ class TestSingleDispatch(unittest.TestCase):
         self.assertEqual(A.t(0.0).arg, "base")
 
     def test_abstractmethod_register(self):
-        class Abstract(abc.ABCMeta):
+        class Abstract(metaclass=abc.ABCMeta):
 
             @functools.singledispatchmethod
             @abc.abstractmethod
@@ -2409,6 +2409,10 @@ class TestSingleDispatch(unittest.TestCase):
                 pass
 
         self.assertTrue(Abstract.add.__isabstractmethod__)
+        self.assertTrue(Abstract.__dict__['add'].__isabstractmethod__)
+
+        with self.assertRaises(TypeError):
+            Abstract()
 
     def test_type_ann_register(self):
         class A:
@@ -2469,6 +2473,141 @@ class TestSingleDispatch(unittest.TestCase):
         self.assertEqual(A.t('').arg, "str")
         self.assertEqual(A.t(0.0).arg, "base")
 
+    def test_method_wrapping_attributes(self):
+        class A:
+            @functools.singledispatchmethod
+            def func(self, arg: int) -> str:
+                """My function docstring"""
+                return str(arg)
+            @functools.singledispatchmethod
+            @classmethod
+            def cls_func(cls, arg: int) -> str:
+                """My function docstring"""
+                return str(arg)
+            @functools.singledispatchmethod
+            @staticmethod
+            def static_func(arg: int) -> str:
+                """My function docstring"""
+                return str(arg)
+
+        for meth in (
+            A.func,
+            A().func,
+            A.cls_func,
+            A().cls_func,
+            A.static_func,
+            A().static_func
+        ):
+            with self.subTest(meth=meth):
+                self.assertEqual(meth.__doc__, 'My function docstring')
+                self.assertEqual(meth.__annotations__['arg'], int)
+
+        self.assertEqual(A.func.__name__, 'func')
+        self.assertEqual(A().func.__name__, 'func')
+        self.assertEqual(A.cls_func.__name__, 'cls_func')
+        self.assertEqual(A().cls_func.__name__, 'cls_func')
+        self.assertEqual(A.static_func.__name__, 'static_func')
+        self.assertEqual(A().static_func.__name__, 'static_func')
+
+    def test_double_wrapped_methods(self):
+        def classmethod_friendly_decorator(func):
+            wrapped = func.__func__
+            @classmethod
+            @functools.wraps(wrapped)
+            def wrapper(*args, **kwargs):
+                return wrapped(*args, **kwargs)
+            return wrapper
+
+        class WithoutSingleDispatch:
+            @classmethod
+            @contextlib.contextmanager
+            def cls_context_manager(cls, arg: int) -> str:
+                try:
+                    yield str(arg)
+                finally:
+                    return 'Done'
+
+            @classmethod_friendly_decorator
+            @classmethod
+            def decorated_classmethod(cls, arg: int) -> str:
+                return str(arg)
+
+        class WithSingleDispatch:
+            @functools.singledispatchmethod
+            @classmethod
+            @contextlib.contextmanager
+            def cls_context_manager(cls, arg: int) -> str:
+                """My function docstring"""
+                try:
+                    yield str(arg)
+                finally:
+                    return 'Done'
+
+            @functools.singledispatchmethod
+            @classmethod_friendly_decorator
+            @classmethod
+            def decorated_classmethod(cls, arg: int) -> str:
+                """My function docstring"""
+                return str(arg)
+
+        # These are sanity checks
+        # to test the test itself is working as expected
+        with WithoutSingleDispatch.cls_context_manager(5) as foo:
+            without_single_dispatch_foo = foo
+
+        with WithSingleDispatch.cls_context_manager(5) as foo:
+            single_dispatch_foo = foo
+
+        self.assertEqual(without_single_dispatch_foo, single_dispatch_foo)
+        self.assertEqual(single_dispatch_foo, '5')
+        
+        self.assertEqual(
+            WithoutSingleDispatch.decorated_classmethod(5),
+            WithSingleDispatch.decorated_classmethod(5)
+        )
+
+        self.assertEqual(WithSingleDispatch.decorated_classmethod(5), '5')
+
+        # Behavioural checks now follow
+        for method_name in ('cls_context_manager', 'decorated_classmethod'):
+            with self.subTest(method=method_name):
+                self.assertEqual(
+                    getattr(WithSingleDispatch, method_name).__name__,
+                    getattr(WithoutSingleDispatch, method_name).__name__
+                )
+
+                self.assertEqual(
+                    getattr(WithSingleDispatch(), method_name).__name__,
+                    getattr(WithoutSingleDispatch(), method_name).__name__
+                )
+
+        for meth in (
+            WithSingleDispatch.cls_context_manager,
+            WithSingleDispatch().cls_context_manager,
+            WithSingleDispatch.decorated_classmethod,
+            WithSingleDispatch().decorated_classmethod
+        ):
+            with self.subTest(meth=meth):
+                self.assertEqual(meth.__doc__, 'My function docstring')
+                self.assertEqual(meth.__annotations__['arg'], int)
+
+        self.assertEqual(
+            WithSingleDispatch.cls_context_manager.__name__,
+            'cls_context_manager'
+        )
+        self.assertEqual(
+            WithSingleDispatch().cls_context_manager.__name__,
+            'cls_context_manager'
+        )
+        self.assertEqual(
+            WithSingleDispatch.decorated_classmethod.__name__,
+            'decorated_classmethod'
+        )
+        self.assertEqual(
+            WithSingleDispatch().decorated_classmethod.__name__,
+            'decorated_classmethod'
+        )
+
     def test_invalid_registrations(self):
         msg_prefix = "Invalid first argument to `register()`: "
         msg_suffix = (
diff --git a/Misc/NEWS.d/next/Library/2021-11-03-17-28-43.bpo-45678.Zj_O8j.rst b/Misc/NEWS.d/next/Library/2021-11-03-17-28-43.bpo-45678.Zj_O8j.rst
new file mode 100644 (file)
index 0000000..f00707c
--- /dev/null
@@ -0,0 +1,2 @@
+Fix bug in Python 3.9 that meant ``functools.singledispatchmethod`` failed
+to properly wrap the attributes of the target method. Patch by Alex Waygood.