summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMichele Simionato <michele.simionato@gmail.com>2015-07-24 08:43:16 +0200
committerMichele Simionato <michele.simionato@gmail.com>2015-07-24 08:43:16 +0200
commit55f28695e817fd7eac8870330843b3dff8f49eaa (patch)
treeaa941ca832e82153ae8499ec557c445e86e51058 /src
parentae96a8f0844770bfb592012ac3ad06cdc7f8e3f9 (diff)
downloadpython-decorator-git-55f28695e817fd7eac8870330843b3dff8f49eaa.tar.gz
Introduced dispatch_info
Diffstat (limited to 'src')
-rw-r--r--src/decorator.py45
-rw-r--r--src/tests/documentation.py101
-rw-r--r--src/tests/test.py2
3 files changed, 102 insertions, 46 deletions
diff --git a/src/decorator.py b/src/decorator.py
index 7e03d42..8faee1f 100644
--- a/src/decorator.py
+++ b/src/decorator.py
@@ -38,6 +38,7 @@ __version__ = '4.0.0'
import re
import sys
import inspect
+import operator
import itertools
import collections
@@ -318,11 +319,11 @@ def dispatch_on(*dispatch_args):
assert dispatch_args, 'No dispatch args passed'
dispatch_str = '(%s,)' % ', '.join(dispatch_args)
- def check(types):
- """Make use one passes the expected number of types"""
- if len(types) != len(dispatch_args):
- raise TypeError('Expected %d types, got %d' %
- (len(dispatch_args), len(types)))
+ def check(arguments, wrong=operator.ne, msg=''):
+ """Make sure one passes the expected number of arguments"""
+ if wrong(len(arguments), len(dispatch_args)):
+ raise TypeError('Expected %d arguments, got %d%s' %
+ (len(dispatch_args), len(arguments), msg))
def gen_func_dec(func):
"""Decorator turning a function into a generic function"""
@@ -346,7 +347,7 @@ def dispatch_on(*dispatch_args):
append(type_, ra)
return [set(ra) for ra in ras]
- def vmros(*types):
+ def ancestors(*types):
"""
Get a list of virtual MROs, one for each type
"""
@@ -362,25 +363,31 @@ def dispatch_on(*dispatch_args):
mro = type('t', (t, va), {}).__mro__[1:]
else:
mro = t.__mro__
- lists.append(mro[:-1]) # discard object
+ lists.append(mro[:-1]) # discard t and object
return lists
def register(*types):
- "Decorator to register an implementation for the given types"
+ """
+ Decorator to register an implementation for the given types
+ """
check(types)
-
def dec(f):
- n_args = len(getfullargspec(f).args)
- if n_args < len(dispatch_args):
- raise TypeError(
- '%s has not enough arguments (got %d, expected %d)' %
- (f, n_args, len(dispatch_args)))
+ check(getfullargspec(f).args, operator.lt, ' in ' + f.__name__)
typemap[types] = f
return f
return dec
+ def dispatch_info(*types):
+ """
+ An utility to introspect the dispatch algorithm
+ """
+ check(types)
+ lst = []
+ for anc in itertools.product(*ancestors(*types)):
+ lst.append(tuple(a.__name__ for a in anc))
+ return lst
+
def _dispatch(dispatch_args, *args, **kw):
- "Dispatcher function"
types = tuple(type(arg) for arg in dispatch_args)
try: # fast path
f = typemap[types]
@@ -388,7 +395,9 @@ def dispatch_on(*dispatch_args):
pass
else:
return f(*args, **kw)
- for types_ in itertools.product(*vmros(*types)):
+ combinations = itertools.product(*ancestors(*types))
+ next(combinations) # the first one has been already tried
+ for types_ in combinations:
f = typemap.get(types_)
if f is not None:
return f(*args, **kw)
@@ -399,8 +408,8 @@ def dispatch_on(*dispatch_args):
return FunctionMaker.create(
func, 'return _f_(%s, %%(shortsignature)s)' % dispatch_str,
dict(_f_=_dispatch), register=register, default=func,
- typemap=typemap, vancestors=vancestors, vmros=vmros,
- __wrapped__=func)
+ typemap=typemap, vancestors=vancestors, ancestors=ancestors,
+ dispatch_info=dispatch_info, __wrapped__=func)
gen_func_dec.__name__ = 'dispatch_on' + dispatch_str
return gen_func_dec
diff --git a/src/tests/documentation.py b/src/tests/documentation.py
index 89dd6ec..04ee70e 100644
--- a/src/tests/documentation.py
+++ b/src/tests/documentation.py
@@ -700,18 +700,18 @@ to dispatch on more than one argument (for instance once I implemented
a database-access library where the first dispatching argument was the
the database driver and the second one was the database record),
but here I prefer to follow the tradition and show the time-honored
-Rock-Paper-Scissor example:
+Rock-Paper-Scissors example:
$$Rock
$$Paper
-$$Scissor
+$$Scissors
-I have added an ordinal to the Rock-Paper-Scissor classes to simplify
+I have added an ordinal to the Rock-Paper-Scissors classes to simplify
the implementation. The idea is to define a generic function ``win(a,
b)`` of two arguments corresponding to the moves of the first and
second player respectively. The moves are instances of the classes
-Rock, Paper and Scissors; Paper wins over Rock, Scissor wins over
-Paper and Rock wins over Scissor. The function will return +1 for a
+Rock, Paper and Scissors; Paper wins over Rock, Scissors wins over
+Paper and Rock wins over Scissors. The function will return +1 for a
win, -1 for a loss and 0 for parity. There are 9 combinations, however
combinations with the same ordinal (i.e. the same class) return 0;
moreover by exchanging the order of the arguments the sign of the
@@ -720,8 +720,8 @@ implementations:
$$win
$$winRockPaper
-$$winPaperScissor
-$$winRockScissor
+$$winPaperScissors
+$$winRockScissors
Here is the result:
@@ -729,23 +729,46 @@ Here is the result:
>>> win(Paper(), Rock())
1
- >>> win(Scissor(), Paper())
+ >>> win(Scissors(), Paper())
1
- >>> win(Rock(), Scissor())
+ >>> win(Rock(), Scissors())
1
>>> win(Paper(), Paper())
0
>>> win(Rock(), Rock())
0
- >>> win(Scissor(), Scissor())
+ >>> win(Scissors(), Scissors())
0
>>> win(Rock(), Paper())
-1
- >>> win(Paper(), Scissor())
+ >>> win(Paper(), Scissors())
-1
- >>> win(Scissor(), Rock())
+ >>> win(Scissors(), Rock())
-1
+The point of generic functions is that they play well with subclassing.
+For instance, suppose we define a StrongRock which does not lose against
+Paper:
+
+$$StrongRock
+$$winStrongRockPaper
+
+Then we do not need to define other implementations, since they are
+inherited from the parent:
+
+.. code-block:: python
+
+ >>> win(StrongRock(), Scissors())
+ 1
+
+You can introspect the precedence used by the dispath algorithm by
+calling ``.dispatch_info(*types)``:
+
+.. code-block:: python
+
+ >>> win.dispatch_info(StrongRock, Scissors)
+ [('StrongRock', 'Scissors'), ('Rock', 'Scissors')]
+
Generic functions and virtual ancestors
-------------------------------------------------
@@ -855,11 +878,24 @@ I will give an example showing the difference:
$$singledispatch_example2
If you play with this example and replace the ``singledispatch`` definition
-with ``functools.singledispatch``, you will see the output change from ``"s"``
-to ``"container"``, because ``functools.singledispatch``
+with ``functools.singledispatch``, the assert will break: ``g`` will return
+``"container"`` instead of ``"s"``, because ``functools.singledispatch``
will insert the ``Container`` class right before ``S``.
+The only way to understand what is happening here is to scratch your
+head by looking at the implementations. I will just notice that
+``.dispatch_info`` is quite useful:
-Finally let me notice that the decorator module implementation does
+.. code-block:: python
+
+ >>> g, V = singledispatch_example2()
+ >>> g.dispatch_info(V)
+ [('V',), ('Sized',), ('S',), ('Container',)]
+
+The current implementation does not implement any kind of cooperation
+between generic functions, i.e. there is nothing akin to call-next-method
+in Lisp, nor akin to ``super`` in Python.
+
+Finally, let me notice that the decorator module implementation does
not use any cache, whereas the one in ``singledispatch`` has a cache.
Caveats and limitations
@@ -1411,10 +1447,14 @@ class Paper(object):
ordinal = 1
-class Scissor(object):
+class Scissors(object):
ordinal = 2
+class StrongRock(Rock):
+ pass
+
+
@dispatch_on('a', 'b')
def win(a, b):
if a.ordinal == b.ordinal:
@@ -1429,16 +1469,21 @@ def winRockPaper(a, b):
return -1
-@win.register(Rock, Scissor)
-def winRockScissor(a, b):
+@win.register(Rock, Scissors)
+def winRockScissors(a, b):
return 1
-@win.register(Paper, Scissor)
-def winPaperScissor(a, b):
+@win.register(Paper, Scissors)
+def winPaperScissors(a, b):
return -1
+@win.register(StrongRock, Paper)
+def winStrongRockPaper(a, b):
+ return 0
+
+
class WithLength(object):
def __len__(self):
return 0
@@ -1502,21 +1547,23 @@ def singledispatch_example2():
return 0
@singledispatch
- def j(arg):
+ def g(arg):
return "base"
- @j.register(S)
- def j_s(arg):
+ @g.register(S)
+ def g_s(arg):
return "s"
- @j.register(c.Container)
- def j_container(arg):
+ @g.register(c.Container)
+ def g_container(arg):
return "container"
v = V()
- assert j(v) == "s"
+ assert g(v) == "s"
c.Container.register(V) # add c.Container to the virtual mro of V
- return j(v) # "s", since the virtual mro is V, Sized, S, Container
+ assert g(v) == "s" # since the virtual mro is V, Sized, S, Container
+ return g, V
+
if __name__ == '__main__':
import doctest
diff --git a/src/tests/test.py b/src/tests/test.py
index 1e1cb3f..951958c 100644
--- a/src/tests/test.py
+++ b/src/tests/test.py
@@ -36,7 +36,7 @@ class DocumentationTestCase(unittest.TestCase):
def test_singledispatch2(self):
if hasattr(functools, 'singledispatch'):
- self.assertEqual(doc.singledispatch_example2(), "s")
+ doc.singledispatch_example2()
class ExtraTestCase(unittest.TestCase):