From b156968f990d85410253ca92b101108eecf419dc Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Thu, 25 Jun 2026 00:28:05 +0200 Subject: [PATCH] fix: MethodDispatcher.__get__ now returns an independent BoundMethodDispatcher MethodDispatcher.__get__ mutated self.obj and self.cls on the descriptor itself and returned self, so every attribute lookup shared the same object. Accessing other_obj.method after storing bound = obj.method would silently overwrite the stored instance, causing bound(...) to dispatch to the wrong object. Fix: __get__ now returns a new BoundMethodDispatcher instance that captures the specific (instance, owner) pair for that lookup, matching the semantics of Python's ordinary bound-method protocol. Add test_method_dispatcher_bound_independence to cover the regression. --- multipledispatch/dispatcher.py | 47 +++++++++++++++-------- multipledispatch/tests/test_dispatcher.py | 40 ++++++++++++++++++- 2 files changed, 71 insertions(+), 16 deletions(-) diff --git a/multipledispatch/dispatcher.py b/multipledispatch/dispatcher.py index ff1329f..6cc2b82 100644 --- a/multipledispatch/dispatcher.py +++ b/multipledispatch/dispatcher.py @@ -408,6 +408,37 @@ def source(func): return s +class BoundMethodDispatcher(object): + """A bound method dispatcher that captures a specific instance. + + Returned by ``MethodDispatcher.__get__`` so that each attribute lookup + produces an independent object that holds its own ``obj`` reference. + This ensures that storing ``bound = obj.method`` and later calling + ``bound(...)`` always dispatches to the correct instance, even after + ``other_obj.method`` has been accessed in the meantime. + """ + + __slots__ = ("dispatcher", "obj", "cls") + + def __init__(self, dispatcher, obj, cls): + self.dispatcher = dispatcher + self.obj = obj + self.cls = cls + + def __call__(self, *args, **kwargs): + types = tuple([type(arg) for arg in args]) + func = self.dispatcher.dispatch(*types) + if not func: + raise NotImplementedError( + "Could not find signature for %s: <%s>" + % (self.dispatcher.name, str_signature(types)) + ) + return func(self.obj, *args, **kwargs) + + def __repr__(self): + return "" % (self.dispatcher, self.obj) + + class MethodDispatcher(Dispatcher): """Dispatch methods based on type signature @@ -415,8 +446,6 @@ class MethodDispatcher(Dispatcher): Dispatcher """ - __slots__ = ("obj", "cls") - @classmethod def get_func_params(cls, func): if hasattr(inspect, "signature"): @@ -424,19 +453,7 @@ def get_func_params(cls, func): return itl.islice(sig.parameters.values(), 1, None) def __get__(self, instance, owner): - self.obj = instance - self.cls = owner - return self - - def __call__(self, *args, **kwargs): - types = tuple([type(arg) for arg in args]) - func = self.dispatch(*types) - if not func: - raise NotImplementedError( - "Could not find signature for %s: <%s>" - % (self.name, str_signature(types)) - ) - return func(self.obj, *args, **kwargs) + return BoundMethodDispatcher(self, instance, owner) def str_signature(sig): diff --git a/multipledispatch/tests/test_dispatcher.py b/multipledispatch/tests/test_dispatcher.py index 1ae25cb..b52ba04 100644 --- a/multipledispatch/tests/test_dispatcher.py +++ b/multipledispatch/tests/test_dispatcher.py @@ -418,4 +418,42 @@ def _3(*objects): assert f("a") == 2 assert f("a", ["a"]) == 2 assert f(1) == 3 - assert f() == 3 + + +def test_method_dispatcher_bound_independence(): + """Each attribute lookup on a different instance must return an independent + bound dispatcher so that storing ``bound = obj.method`` and later calling + ``bound(...)`` always dispatches to the original instance. + + Previously ``MethodDispatcher.__get__`` stored ``obj`` on *self* (the + descriptor), so accessing ``other.method`` would silently overwrite the + stored instance and cause subsequent calls via the earlier bound reference + to be dispatched to the wrong object. + """ + from multipledispatch import dispatch + + class Counter(object): + def __init__(self): + self.count = 0 + + @dispatch(int) + def increment(self, x): + self.count += x + + a = Counter() + b = Counter() + + bound_a = a.increment + bound_b = b.increment # must not clobber bound_a's instance + + # Two attribute lookups must yield distinct objects. + assert bound_a is not bound_b + + # Calling the earlier-obtained bound method must affect only ``a``. + bound_a(10) + assert a.count == 10 + assert b.count == 0 + + bound_b(5) + assert a.count == 10 + assert b.count == 5