Skip to content

Commit 75211b5

Browse files
committed
Lazy dispatch
1 parent ecef0b0 commit 75211b5

5 files changed

Lines changed: 153 additions & 6 deletions

File tree

docs/source/resolution.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,19 @@ For example, here's a function that takes a ``float`` followed by any number
161161
>>> f(2.0, '4', 6, 8)
162162
20.0
163163
164+
Lazy Dispatch
165+
-------------
166+
167+
You may need to refer to your own class while defining it. Just use its name as
168+
a string and ``multipledispatch`` will resolve a name to a class during runtime
169+
170+
.. code::
171+
172+
class MyInteger(int):
173+
@dispatch('MyInteger')
174+
def add(self, x):
175+
return self + x
176+
164177
Ambiguities
165178
-----------
166179

multipledispatch/conflict.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import itertools
2+
13
from .utils import _toposort, groupby
24
from .variadic import isvariadic
35

@@ -8,6 +10,9 @@ class AmbiguityWarning(Warning):
810

911
def supercedes(a, b):
1012
""" A is consistent and strictly more specific than B """
13+
if any(isinstance(x, str) for x in itertools.chain(a, b)):
14+
# skip due to lazy types
15+
return False
1116
if len(a) < len(b):
1217
# only case is if a is empty and b is variadic
1318
return not a and len(b) == 1 and isvariadic(b[-1])

multipledispatch/dispatcher.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,16 @@ class Dispatcher(object):
117117
>>> f(3.0)
118118
2.0
119119
"""
120-
__slots__ = '__name__', 'name', 'funcs', '_ordering', '_cache', 'doc'
120+
__slots__ = '__name__', 'name', 'funcs', '_ordering', '_cache', 'doc', \
121+
'_lazy'
121122

122123
def __init__(self, name, doc=None):
123124
self.name = self.__name__ = name
124125
self.funcs = {}
125126
self.doc = doc
126127

127128
self._cache = {}
129+
self._lazy = False
128130

129131
def register(self, *types, **kwargs):
130132
""" register dispatcher with new implementation
@@ -214,9 +216,8 @@ def add(self, signature, func):
214216
return
215217

216218
new_signature = []
217-
218219
for index, typ in enumerate(signature, start=1):
219-
if not isinstance(typ, (type, list)):
220+
if not isinstance(typ, (type, list, str)):
220221
str_sig = ', '.join(c.__name__ if isinstance(c, type)
221222
else str(c) for c in signature)
222223
raise TypeError("Tried to dispatch on non-type: %s\n"
@@ -237,9 +238,12 @@ def add(self, signature, func):
237238
'To use a variadic union type place the desired types '
238239
'inside of a tuple, e.g., [(int, str)]'
239240
)
240-
new_signature.append(Variadic[typ[0]])
241-
else:
242-
new_signature.append(typ)
241+
typ = Variadic[typ[0]]
242+
243+
if isinstance(typ, str):
244+
self._lazy = True
245+
246+
new_signature.append(typ)
243247

244248
self.funcs[tuple(new_signature)] = func
245249
self._cache.clear()
@@ -264,6 +268,9 @@ def reorder(self, on_ambiguity=ambiguity_warn):
264268
return od
265269

266270
def __call__(self, *args, **kwargs):
271+
if self._lazy:
272+
self._unlazy()
273+
267274
types = tuple([type(arg) for arg in args])
268275
try:
269276
func = self._cache[types]
@@ -359,6 +366,7 @@ def __setstate__(self, d):
359366
self.funcs = d['funcs']
360367
self._ordering = ordering(self.funcs)
361368
self._cache = dict()
369+
self._lazy = any(isinstance(t, str) for t in itl.chain(*d['funcs']))
362370

363371
@property
364372
def __doc__(self):
@@ -400,6 +408,31 @@ def source(self, *args, **kwargs):
400408
""" Print source code for the function corresponding to inputs """
401409
print(self._source(*args))
402410

411+
def _unlazy(self):
412+
funcs = {}
413+
for signature, func in self.funcs.items():
414+
new_signature = []
415+
for typ in signature:
416+
if isinstance(typ, str):
417+
for frame_info in inspect.stack():
418+
frame = frame_info[0]
419+
scope = dict(frame.f_globals)
420+
scope.update(frame.f_locals)
421+
if typ in scope:
422+
typ = scope[typ]
423+
break
424+
else:
425+
raise NameError("name '%s' is not defined" % typ)
426+
new_signature.append(typ)
427+
428+
new_signature = tuple(new_signature)
429+
funcs[new_signature] = func
430+
431+
self.funcs = funcs
432+
self.reorder()
433+
434+
self._lazy = False
435+
403436

404437
def source(func):
405438
s = 'File: %s\n\n' % inspect.getsourcefile(func)
@@ -427,6 +460,9 @@ def __get__(self, instance, owner):
427460
return self
428461

429462
def __call__(self, *args, **kwargs):
463+
if self._lazy:
464+
self._unlazy()
465+
430466
types = tuple([type(arg) for arg in args])
431467
func = self.dispatch(*types)
432468
if not func:

multipledispatch/tests/test_dispatcher.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
import warnings
33

4+
from multipledispatch import dispatch
45
from multipledispatch.dispatcher import (Dispatcher, MDNotImplementedError,
56
MethodDispatcher)
67
from multipledispatch.conflict import ambiguities
@@ -421,3 +422,74 @@ def _3(*objects):
421422
assert f('a', ['a']) == 2
422423
assert f(1) == 3
423424
assert f() == 3
425+
426+
427+
def test_lazy_methods():
428+
class A(object):
429+
@dispatch(int)
430+
def get(self, _):
431+
return 'int'
432+
433+
@dispatch('A')
434+
def get(self, _):
435+
"""Self reference"""
436+
return 'A'
437+
438+
@dispatch('B')
439+
def get(self, _):
440+
"""Yet undeclared type"""
441+
return 'B'
442+
443+
class B(object):
444+
pass
445+
446+
class C(A):
447+
@dispatch('D')
448+
def get(self, _):
449+
"""Non-existent type"""
450+
return 'D'
451+
452+
a = A()
453+
b = B()
454+
c = C()
455+
456+
assert a.get(1) == 'int'
457+
assert a.get(a) == 'A'
458+
assert a.get(b) == 'B'
459+
assert raises(NameError, lambda: c.get(1))
460+
461+
462+
def test_lazy_functions():
463+
f = Dispatcher('f')
464+
f.add((int,), inc)
465+
f.add(('Int',), dec)
466+
467+
assert raises(NameError, lambda: f(1))
468+
469+
class Int(int):
470+
pass
471+
472+
assert f(1) == 2
473+
assert f(Int(1)) == 0
474+
475+
476+
def test_lazy_serializable():
477+
f = Dispatcher('f')
478+
f.add((int,), inc)
479+
f.add(('Int',), dec)
480+
481+
import pickle
482+
assert isinstance(pickle.dumps(f), (str, bytes))
483+
484+
g = pickle.loads(pickle.dumps(f))
485+
486+
assert f.funcs == g.funcs
487+
assert f._lazy == g._lazy
488+
489+
assert raises(NameError, lambda: f(1))
490+
491+
class Int(int):
492+
pass
493+
494+
assert g(1) == 2
495+
assert g(Int(1)) == 0

multipledispatch/tests/test_dispatcher_3only.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from multipledispatch import dispatch
66
from multipledispatch.dispatcher import Dispatcher
7+
from multipledispatch.utils import raises
78

89

910
def test_function_annotation_register():
@@ -92,3 +93,23 @@ def inc(x: int):
9293

9394
assert inc(1) == 2
9495
assert inc(1.0) == 0.0
96+
97+
98+
def test_lazy_annotations():
99+
f = Dispatcher('f')
100+
101+
@f.register()
102+
def inc(x: int):
103+
return x + 1
104+
105+
@f.register()
106+
def dec(x: 'Int'):
107+
return x - 1
108+
109+
assert raises(NameError, lambda: f(1))
110+
111+
class Int(int):
112+
pass
113+
114+
assert f(1) == 2
115+
assert f(Int(1)) == 0

0 commit comments

Comments
 (0)