diff options
| author | Michele Simionato <michele.simionato@gmail.com> | 2015-07-23 13:56:12 +0200 |
|---|---|---|
| committer | Michele Simionato <michele.simionato@gmail.com> | 2015-07-23 13:56:12 +0200 |
| commit | 6d099d1b69144e5bd491e457ec493b8f100480a1 (patch) | |
| tree | d74b5ac3b65557d5b8da1c5acbbbbd5bfed94c3f /src/tests | |
| parent | 80072c50a765547736c03810cfb3e4f5afcb928d (diff) | |
| download | python-decorator-git-6d099d1b69144e5bd491e457ec493b8f100480a1.tar.gz | |
Fixed the mros algorithm
Diffstat (limited to 'src/tests')
| -rw-r--r-- | src/tests/documentation.py | 143 | ||||
| -rw-r--r-- | src/tests/test.py | 53 |
2 files changed, 113 insertions, 83 deletions
diff --git a/src/tests/documentation.py b/src/tests/documentation.py index 6ce800b..4591611 100644 --- a/src/tests/documentation.py +++ b/src/tests/documentation.py @@ -666,18 +666,19 @@ functions dispatching on any argument; moreover it can manage dispatching on more than one argument and, of course, it is signature-preserving. -Here I will give a very concrete example where it is desiderable to -dispatch on the second argument. Suppose you have an XMLWriter class, -which is instantiated with some configuration parameters and has -a ``.write`` method which is able to serialize objects to XML: +Here I will give a very concrete example (taken from a real-life use +case) where it is desiderable to dispatch on the second +argument. Suppose you have an XMLWriter class, which is instantiated +with some configuration parameters and has a ``.write`` method which +is able to serialize objects to XML: $$XMLWriter Here you want to dispatch on the second argument since the first, ``self`` -is already taken. The `dispatch_on` facility allows you to specify +is already taken. The ``dispatch_on`` decorator factory allows you to specify the dispatch argument by simply passing its name as a string (notice that if you mispell the name you will get an error). The function -decorated with `dispatch_on` is turned into a generic function +decorated is turned into a generic function and it is the one which is called if there are no more specialized implementations. Usually such default function should raise a ``NotImplementedError``, thus forcing people to register some implementation. @@ -747,10 +748,9 @@ Here is the result: Generic functions and virtual ancestors ------------------------------------------------- -Generic function implementations in Python are -complicated by the existence of "virtual ancestors", i.e. superclasses -which are not in the class hierarchy. -Consider for instance this class: +Generic function implementations in Python are complicated by the +existence of "virtual ancestors", i.e. superclasses which are not in +the class hierarchy. Consider for instance this class: $$WithLength @@ -762,8 +762,8 @@ considered to be a subclass of the abstract base class ``collections.Sized``: >>> issubclass(WithLength, collections.Sized) True -However, ``collections.Sized`` is not an ancestor of ``WithLength``. -Any implementation of generic functions, even +However, ``collections.Sized`` is not in the MRO of ``WithLength``, it +is not a true ancestor. Any implementation of generic functions, even with single dispatch, must go through some contorsion to take into account the virtual ancestors. @@ -775,26 +775,22 @@ implemented on all classes with a length $$get_length_sized -then ``get_length`` must be defined on ``WithLength`` instances: +then ``get_length`` must be defined on ``WithLength`` instances .. code-block:: python >>> get_length(WithLength()) 0 -You can find the virtual ancestors of a given set of classes as follows: - - >> get_length.vancestors(WithLength,) - [[<class 'collections.abc.Sized'>]] - +even if ``collections.Sized`` is not a true ancestor of ``WithLength``. Of course this is a contrived example since you could just use the builtin ``len``, but you should get the idea. -The implementation of generic functions in the decorator module is -still experimental. In this initial phase implicity was preferred -over consistency with the way ``functools.singledispatch`` works in -the standard library. So there some subtle differences in special -cases. I will only show an example. +Since in Python it is possible to consider any instance of ABCMeta +as a virtual ancestor of any other class (it is enough to register it +as ``ancestor.register(cls)``), any implementation of generic functions +must take this feature into account. Let me give an example. + Suppose you are using a third party set-like class like the following: @@ -803,14 +799,14 @@ $$SomeSet Here the author of ``SomeSet`` made a mistake by not inheriting from ``collections.Set``, but only from ``collections.Sized``. -This is not a problem since we can register *a posteriori* -``collections.Set`` as a virtual ancestor of ``SomeSet`` (in -general any instance of ``abc.ABCMeta`` can be registered to work -as a virtual ancestor): +This is not a problem since you can register *a posteriori* +``collections.Set`` as a virtual ancestor of ``SomeSet``: .. code-block:: python - >>> _ = collections.Set.register(SomeSet) # issubclass(SomeSet, Set) + >>> _ = collections.Set.register(SomeSet) + >>> issubclass(SomeSet, collections.Set) + True Now, let us define an implementation of ``get_length`` specific to set: @@ -823,34 +819,32 @@ implementation for ``Set`` is taken: .. code-block:: python >>> get_length(SomeSet()) - Traceback (most recent call last): - ... - TypeError: Cannot create a consistent method resolution - order (MRO) for bases Sized, Set - -Sometimes it is impossible to find the right implementation. Here is a -situation with a type conflict. First of all, let us register - -.. code-block:: python - - >>> @get_length.register(collections.Iterable) - ... def get_length_iterable(obj): - ... raise TypeError('Cannot get the length of an iterable') - - -Since ``SomeSet`` is now both a (virtual) subclass -of ``collections.Iterable`` and of ``collections.Sized``, which are -not related by subclassing, it is impossible -to decide which implementation should be taken. Consistently with -the *refuse the temptation to guess* philosophy, an error is raised. + 1 - >>> get_length(SomeSet()) - Traceback (most recent call last): - ... - TypeError: Cannot create a consistent method resolution - order (MRO) for bases Iterable, Sized, Set -``functools.singledispatch`` would raise a similar error in this case. +The implementation of generic functions in the decorator module is +still experimental. In this initial phase implicity was preferred +over perfect consistency with the way ``functools.singledispatch`` works in +the standard library. So there are some subtle differences in special +cases. + +Considered a class ``C`` registered both as ``collections.Iterable`` +and ``collections.Sized`` and define a generic function ``g`` with +implementations both for ``collections.Iterable`` and +``collections.Sized``. It is impossible to decide which implementation +to use, and the following code will fail with a RuntimeError: + +$$singledispatch_example + +This is consistent with the "refuse the temptation to guess" +philosophy. ``functools.singledispatch`` will raise a similar error. +It would be easy to rely on the order of registration to decide the +precedence order. This is reasonable, but also fragile: if during some +refactoring you change the registration order by mistake, a different +implementation could be taken. If implementations of the generic +functions are distributed across modules, and you change the import +order, a different implementation could be taken. So the decorator +module is using the same approach of the standard library. Finally let me notice that the decorator module implementation does not use any cache, whereas the one in ``singledispatch`` has a cache. @@ -1456,3 +1450,44 @@ def get_length_sized(obj): @get_length.register(collections.Set) def get_length_set(obj): return 1 + + +@contextmanager +def assertRaises(etype): + """This works in Python 2.6 too""" + try: + yield + except etype: + pass + else: + raise Exception('Expected %s' % etype.__name__) + + +class C(object): + "Registered as Sized and Iterable" +collections.Sized.register(C) +collections.Iterable.register(C) + + +def singledispatch_example(): + singledispatch = dispatch_on('obj') + + @singledispatch + def g(obj): + raise NotImplementedError(type(g)) + + @g.register(collections.Sized) + def g_sized(object): + return "sized" + + @g.register(collections.Iterable) + def g_iterable(object): + return "iterable" + + with assertRaises(RuntimeError): + g(C()) # Ambiguous dispatch: Iterable or Sized? + + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/src/tests/test.py b/src/tests/test.py index af5998b..8413a58 100644 --- a/src/tests/test.py +++ b/src/tests/test.py @@ -4,35 +4,29 @@ import doctest import unittest import decimal import inspect +import functools import collections -from decorator import dispatch_on, contextmanager +from decorator import dispatch_on try: - from . import documentation + from . import documentation as doc except (SystemError, ValueError): - import documentation - - -@contextmanager -def assertRaises(etype): - """This works in Python 2.6 too""" - try: - yield - except etype: - pass - else: - raise Exception('Expected %s' % etype.__name__) + import documentation as doc class DocumentationTestCase(unittest.TestCase): def test(self): - err = doctest.testmod(documentation)[0] + err = doctest.testmod(doc)[0] self.assertEqual(err, 0) + def test_singledispatch(self): + if hasattr(functools, 'singledispatch'): + doc.singledispatch_example() + class ExtraTestCase(unittest.TestCase): def test_signature(self): if hasattr(inspect, 'signature'): - sig = inspect.signature(documentation.f1) + sig = inspect.signature(doc.f1) self.assertEqual(str(sig), '(x)') # ################### test dispatch_on ############################# # @@ -100,7 +94,8 @@ class TestSingleDispatch(unittest.TestCase): def g(obj): return "base" - with assertRaises(TypeError): + with doc.assertRaises(TypeError): + # wrong number of arguments @g.register(int) def g_int(): return "int" @@ -264,10 +259,10 @@ class TestSingleDispatch(unittest.TestCase): c.Iterable.register(O) self.assertEqual(g(o), "sized") c.Container.register(O) - self.assertEqual(g(o), "sized") + with doc.assertRaises(RuntimeError): + self.assertEqual(g(o), "sized") c.Set.register(O) - with assertRaises(TypeError): # was ok - self.assertEqual(g(o), "set") + self.assertEqual(g(o), "set") class P(object): pass @@ -277,8 +272,8 @@ class TestSingleDispatch(unittest.TestCase): self.assertEqual(g(p), "iterable") c.Container.register(P) - #with assertRaises(RuntimeError): - self.assertEqual(g(p), "iterable") + with doc.assertRaises(RuntimeError): + self.assertEqual(g(p), "iterable") class Q(c.Sized): def __len__(self): @@ -288,9 +283,8 @@ class TestSingleDispatch(unittest.TestCase): c.Iterable.register(Q) self.assertEqual(g(q), "sized") c.Set.register(Q) - # self.assertEqual(g(q), "sized") - # could be because c.Set is a subclass of - # c.Sized and c.Iterable + self.assertEqual(g(q), "set") + # because c.Set is a subclass of c.Sized and c.Iterable @singledispatch def h(obj): @@ -307,8 +301,8 @@ class TestSingleDispatch(unittest.TestCase): # this ABC is implicitly registered on defaultdict which makes all of # MutableMapping's bases implicit as well from defaultdict's # perspective. - #with assertRaises(RuntimeError): - h(c.defaultdict(lambda: 0)) + with doc.assertRaises(RuntimeError): + self.assertEqual(h(c.defaultdict(lambda: 0)), "sized") class R(c.defaultdict): pass @@ -326,7 +320,7 @@ class TestSingleDispatch(unittest.TestCase): def i_sequence(arg): return "sequence" r = R() - self.assertEqual(i(r), "mapping") # was sequence + #self.assertEqual(i(r), "mapping") # was sequence class S(object): pass @@ -350,7 +344,8 @@ class TestSingleDispatch(unittest.TestCase): c.Container.register(U) # There is preference for registered versus inferred ABCs. - self.assertEqual(h(u), "sized") # was conflict + with doc.assertRaises(RuntimeError): + self.assertEqual(h(u), "sized") class V(c.Sized, S): def __len__(self): |
