Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 32 additions & 15 deletions multipledispatch/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,35 +408,52 @@ 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 "<bound %s of %r>" % (self.dispatcher, self.obj)


class MethodDispatcher(Dispatcher):
"""Dispatch methods based on type signature

See Also:
Dispatcher
"""

__slots__ = ("obj", "cls")

@classmethod
def get_func_params(cls, func):
if hasattr(inspect, "signature"):
sig = inspect.signature(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):
Expand Down
40 changes: 39 additions & 1 deletion multipledispatch/tests/test_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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