| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -744,6 +744,10 @@ functools | |||
| 744 | 744 | callables. | |
| 745 | 745 | (Contributed by Serhiy Storchaka in :gh:`140873`.) | |
| 746 | 746 | ||
| 747 | + * :func:`~functools.singledispatchmethod` now dispatches on the second argument | ||
| 748 | + if it wraps a regular method and is called as a class attribute. | ||
| 749 | + (Contributed by Bartosz Sławecki in :gh:`143535`.) | ||
| 750 | + | ||
| 747 | 751 | ||
| 748 | 752 | hashlib | |
| 749 | 753 | ------- | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -19,7 +19,7 @@ | |||
| 19 | 19 | # import weakref # Deferred to single_dispatch() | |
| 20 | 20 | from operator import itemgetter | |
| 21 | 21 | from reprlib import recursive_repr | |
| 22 | - from types import GenericAlias, MethodType, MappingProxyType, UnionType | ||
| 22 | + from types import FunctionType, GenericAlias, MethodType, MappingProxyType, UnionType | ||
| 23 | 23 | from _thread import RLock | |
| 24 | 24 | ||
| 25 | 25 | ################################################################################ | |
@@ -1060,6 +1060,11 @@ def __init__(self, unbound, obj, cls): | |||
| 1060 | 1060 | # Set instance attributes which cannot be handled in __getattr__() | |
| 1061 | 1061 | # because they conflict with type descriptors. | |
| 1062 | 1062 | func = unbound.func | |
| 1063 | + | ||
| 1064 | + # Dispatch on the second argument if a generic method turns into | ||
| 1065 | + # a bound method on instance-level access. See GH-143535. | ||
| 1066 | + self._dispatch_arg_index = 1 if obj is None and isinstance(func, FunctionType) else 0 | ||
| 1067 | + | ||
| 1063 | 1068 | try: | |
| 1064 | 1069 | self.__module__ = func.__module__ | |
| 1065 | 1070 | except AttributeError: | |
@@ -1088,9 +1093,22 @@ def __call__(self, /, *args, **kwargs): | |||
| 1088 | 1093 | 'singledispatchmethod method') | |
| 1089 | 1094 | raise TypeError(f'{funcname} requires at least ' | |
| 1090 | 1095 | '1 positional argument') | |
| 1091 | - method = self._dispatch(args[0].__class__) | ||
| 1096 | + method = self._dispatch(args[self._dispatch_arg_index].__class__) | ||
| 1097 | + | ||
| 1092 | 1098 | if hasattr(method, "__get__"): | |
| 1099 | + # If the method is a descriptor, it might be necessary | ||
| 1100 | + # to drop the first argument before calling | ||
| 1101 | + # as it can be no longer expected after descriptor access. | ||
| 1102 | + skip_bound_arg = False | ||
| 1103 | + if isinstance(method, staticmethod): | ||
| 1104 | + skip_bound_arg = self._dispatch_arg_index == 1 | ||
| 1105 | + | ||
| 1093 | 1106 | method = method.__get__(self._obj, self._cls) | |
| 1107 | + if isinstance(method, MethodType): | ||
| 1108 | + skip_bound_arg = self._dispatch_arg_index == 1 | ||
| 1109 | + | ||
| 1110 | + if skip_bound_arg: | ||
| 1111 | + return method(*args[1:], **kwargs) | ||
| 1094 | 1112 | return method(*args, **kwargs) | |
| 1095 | 1113 | ||
| 1096 | 1114 | def __getattr__(self, name): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3005,6 +3005,57 @@ def static_func(arg: int) -> str: | |||
| 3005 | 3005 | self.assertEqual(A.static_func.__name__, 'static_func') | |
| 3006 | 3006 | self.assertEqual(A().static_func.__name__, 'static_func') | |
| 3007 | 3007 | ||
| 3008 | + def test_method_classlevel_calls(self): | ||
| 3009 | + """Regression test for GH-143535.""" | ||
| 3010 | + class C: | ||
| 3011 | + @functools.singledispatchmethod | ||
| 3012 | + def generic(self, x: object): | ||
| 3013 | + return "generic" | ||
| 3014 | + | ||
| 3015 | + @generic.register | ||
| 3016 | + def special1(self, x: int): | ||
| 3017 | + return "special1" | ||
| 3018 | + | ||
| 3019 | + @generic.register | ||
| 3020 | + @classmethod | ||
| 3021 | + def special2(self, x: float): | ||
| 3022 | + return "special2" | ||
| 3023 | + | ||
| 3024 | + @generic.register | ||
| 3025 | + @staticmethod | ||
| 3026 | + def special3(x: complex): | ||
| 3027 | + return "special3" | ||
| 3028 | + | ||
| 3029 | + def special4(self, x): | ||
| 3030 | + return "special4" | ||
| 3031 | + | ||
| 3032 | + class D1: | ||
| 3033 | + def __get__(self, _, owner): | ||
| 3034 | + return lambda inst, x: owner.special4(inst, x) | ||
| 3035 | + | ||
| 3036 | + generic.register(D1, D1()) | ||
| 3037 | + | ||
| 3038 | + def special5(self, x): | ||
| 3039 | + return "special5" | ||
| 3040 | + | ||
| 3041 | + class D2: | ||
| 3042 | + def __get__(self, inst, owner): | ||
| 3043 | + # Different instance bound to the returned method | ||
| 3044 | + # doesn't cause it to receive the original instance | ||
| 3045 | + # as a separate argument. | ||
| 3046 | + # To work around this, wrap the returned bound method | ||
| 3047 | + # with `functools.partial`. | ||
| 3048 | + return C().special5 | ||
| 3049 | + | ||
| 3050 | + generic.register(D2, D2()) | ||
| 3051 | + | ||
| 3052 | + self.assertEqual(C.generic(C(), "foo"), "generic") | ||
| 3053 | + self.assertEqual(C.generic(C(), 1), "special1") | ||
| 3054 | + self.assertEqual(C.generic(C(), 2.0), "special2") | ||
| 3055 | + self.assertEqual(C.generic(C(), 3j), "special3") | ||
| 3056 | + self.assertEqual(C.generic(C(), C.D1()), "special4") | ||
| 3057 | + self.assertEqual(C.generic(C(), C.D2()), "special5") | ||
| 3058 | + | ||
| 3008 | 3059 | def test_method_repr(self): | |
| 3009 | 3060 | class Callable: | |
| 3010 | 3061 | def __call__(self, *args): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,3 @@ | |||
| 1 | + Methods directly decorated with :deco:`functools.singledispatchmethod` now | ||
| 2 | + dispatch on the second argument when called after being accessed as class | ||
| 3 | + attributes. Patch by Bartosz Sławecki. | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments