summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author?ukasz Langa <lukasz@langa.pl>2013-05-25 23:22:05 +0200
committer?ukasz Langa <lukasz@langa.pl>2013-05-25 23:22:05 +0200
commit608f97f22cb57c6f96efa9c19e27e400cb6b289e (patch)
tree5a8f234c3a620ddebd277e3de57dd051fd9ef448
downloadsingledispatch-608f97f22cb57c6f96efa9c19e27e400cb6b289e.tar.gz
initial commit
-rw-r--r--.travis.yml6
-rw-r--r--MANIFEST.in1
-rw-r--r--README.rst167
-rw-r--r--setup.py78
-rw-r--r--singledispatch.py117
-rw-r--r--test_singledispatch.py354
-rw-r--r--tox.ini12
7 files changed, 735 insertions, 0 deletions
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..b6e1860
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,6 @@
+language: python
+
+before_install:
+ - pip install tox
+
+script: tox
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..9561fb1
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include README.rst
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..802ab5e
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,167 @@
+==============
+singledispatch
+==============
+
+`PEP 443 <http://www.python.org/dev/peps/pep-0443/>`_ proposed to expose
+a mechanism in the ``functools`` standard library module in Python 3.4
+that provides a simple form of generic programming known as
+single-dispatch generic functions.
+
+This library is a backport of this functionality to Python 2.6 - 3.3.
+
+To define a generic function, decorate it with the ``@singledispatch``
+decorator. Note that the dispatch happens on the type of the first
+argument, create your function accordingly::
+
+ >>> from functools import singledispatch
+ >>> @singledispatch
+ ... def fun(arg, verbose=False):
+ ... if verbose:
+ ... print("Let me just say,", end=" ")
+ ... print(arg)
+
+To add overloaded implementations to the function, use the
+``register()`` attribute of the generic function. It takes a type
+parameter::
+
+ >>> @fun.register(int)
+ ... def _(arg, verbose=False):
+ ... if verbose:
+ ... print("Strength in numbers, eh?", end=" ")
+ ... print(arg)
+ ...
+ >>> @fun.register(list)
+ ... def _(arg, verbose=False):
+ ... if verbose:
+ ... print("Enumerate this:")
+ ... for i, elem in enumerate(arg):
+ ... print(i, elem)
+
+To enable registering lambdas and pre-existing functions, the
+``register()`` attribute can be used in a functional form::
+
+ >>> def nothing(arg, verbose=False):
+ ... print("Nothing.")
+ ...
+ >>> fun.register(type(None), nothing)
+
+The ``register()`` attribute returns the undecorated function which
+enables decorator stacking, pickling, as well as creating unit tests for
+each variant independently::
+
+ >>> @fun.register(float)
+ ... @fun.register(Decimal)
+ ... def fun_num(arg, verbose=False):
+ ... if verbose:
+ ... print("Half of your number:", end=" ")
+ ... print(arg / 2)
+ ...
+ >>> fun_num is fun
+ False
+
+When called, the generic function dispatches on the first argument::
+
+ >>> fun("Hello, world.")
+ Hello, world.
+ >>> fun("test.", verbose=True)
+ Let me just say, test.
+ >>> fun(42, verbose=True)
+ Strength in numbers, eh? 42
+ >>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)
+ Enumerate this:
+ 0 spam
+ 1 spam
+ 2 eggs
+ 3 spam
+ >>> fun(None)
+ Nothing.
+ >>> fun(1.23)
+ 0.615
+
+To get the implementation for a specific type, use the ``dispatch()``
+attribute::
+
+ >>> fun.dispatch(float)
+ <function fun_num at 0x104319058>
+ >>> fun.dispatch(dict)
+ <function fun at 0x103fe4788>
+
+To access all registered overloads, use the read-only ``registry``
+attribute::
+
+ >>> fun.registry.keys()
+ dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>,
+ <class 'decimal.Decimal'>, <class 'list'>,
+ <class 'float'>])
+ >>> fun.registry[float]
+ <function fun_num at 0x1035a2840>
+ >>> fun.registry[object]
+ <function fun at 0x103170788>
+
+The vanilla documentation is available at
+http://docs.python.org/3/library/functools.html#functools.singledispatch.
+
+
+Versioning
+----------
+
+This backport is intended to keep 100% compatibility with the vanilla
+release in Python 3.4+. To help maintaining a version you want and
+expect, a versioning scheme is used where:
+
+* the first three numbers indicate the version of Python 3.x from which the
+ backport is done
+
+* a backport release number is provided after the last dot
+
+For example, ``3.4.0.0`` is the **first** release of ``singledispatch``
+compatible with the library found in Python **3.4.0**.
+
+A single exception from the 100% compatibility principle is that bugs
+fixed before releasing another minor Python 3.x.y version **will be
+included** in the backport releases done in the mean time. This rule
+applies to bugs only.
+
+
+Maintenance
+-----------
+
+This backport is maintained on BitBucket by Łukasz Langa, one of the
+members of the core CPython team:
+
+* `singledispatch Mercurial repository <https://bitbucket.org/ambv/singledispatch>`_
+
+* `singledispatch issue tracker <https://bitbucket.org/ambv/singledispatch/issues>`_
+
+
+Change Log
+----------
+
+3.4.0.0
+~~~~~~~
+
+* the first public release compatible with 3.4.0
+
+
+Conversion Process
+------------------
+
+This section is technical and should bother you only if you are
+wondering how this backport is produced. If the implementation details
+of this backport are not important for you, feel free to ignore the
+following content.
+
+``singledispatch`` is converted using `six
+<http://pypi.python.org/pypi/six>`_ so that a single codebase can be
+used for all compatible Python versions. Because a fully automatic
+conversion was not doable, I took the following branching approach:
+
+* the ``upstream`` branch holds unchanged files synchronized from the
+ upstream CPython repository. The synchronization is currently done by
+ manually copying the required code parts and stating from which
+ CPython changeset they come from. The tests should pass on Python 3.4
+ on this branch.
+
+* the ``default`` branch holds the manually translated version and this
+ is where all tests are run for all supported Python versions using
+ Tox.
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..834e9f1
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,78 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""This library brings functools.singledispatch from Python 3.4 to Python 2.6-3.3."""
+
+# Copyright (C) 2013 by Łukasz Langa
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+import os
+import sys
+import codecs
+from setuptools import setup, find_packages
+
+PY3 = sys.version_info[0] == 3
+
+if not PY3:
+ reload(sys)
+ sys.setdefaultencoding('utf8')
+
+with codecs.open(
+ os.path.join(os.path.dirname(__file__), 'README.rst'), 'r', 'utf8',
+) as ld_file:
+ long_description = ld_file.read()
+# We let it die a horrible tracebacking death if reading the file fails.
+# We couldn't sensibly recover anyway: we need the long description.
+
+setup (
+ name = 'singledispatch',
+ version = '3.4.0.0',
+ author = 'Łukasz Langa',
+ author_email = 'lukasz@langa.pl',
+ description = __doc__,
+ long_description = long_description,
+ url = 'http://docs.python.org/3/library/functools.html'
+ '#functools.singledispatch',
+ keywords = 'single dispatch generic functions singledispatch '
+ 'genericfunctions decorator backport',
+ platforms = ['any'],
+ license = 'MIT',
+ py_modules = ('singledispatch',),
+ zip_safe = True,
+ install_requires = [
+ 'six',
+ ],
+ classifiers = [
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Natural Language :: English',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: 3.3',
+ 'Topic :: Software Development :: Libraries',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ ]
+)
diff --git a/singledispatch.py b/singledispatch.py
new file mode 100644
index 0000000..e0de4ca
--- /dev/null
+++ b/singledispatch.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+__all__ = ['singledispatch']
+
+from abc import get_cache_token
+from functools import update_wrapper
+from types import MappingProxyType
+from weakref import WeakKeyDictionary
+
+
+################################################################################
+### singledispatch() - single-dispatch generic function decorator
+################################################################################
+
+def _compose_mro(cls, haystack):
+ """Calculates the MRO for a given class `cls`, including relevant abstract
+ base classes from `haystack`."""
+ bases = set(cls.__mro__)
+ mro = list(cls.__mro__)
+ for regcls in haystack:
+ if regcls in bases or not issubclass(cls, regcls):
+ continue # either present in the __mro__ already or unrelated
+ for index, base in enumerate(mro):
+ if not issubclass(base, regcls):
+ break
+ if base in bases and not issubclass(regcls, base):
+ # Conflict resolution: put classes present in __mro__ and their
+ # subclasses first. See test_mro_conflicts() in test_functools.py
+ # for examples.
+ index += 1
+ mro.insert(index, regcls)
+ return mro
+
+def singledispatch(func):
+ """Single-dispatch generic function decorator.
+
+ Transforms a function into a generic function, which can have different
+ behaviours depending upon the type of its first argument. The decorated
+ function acts as the default implementation, and additional
+ implementations can be registered using the 'register()' attribute of
+ the generic function.
+
+ """
+ registry = {}
+ dispatch_cache = WeakKeyDictionary()
+ cache_token = None
+
+ def dispatch(cls):
+ """generic_func.dispatch(type) -> <function implementation>
+
+ Runs the dispatch algorithm to return the best available implementation
+ for the given `type` registered on `generic_func`.
+
+ """
+ if cache_token is not None:
+ mro = _compose_mro(cls, registry.keys())
+ match = None
+ for t in mro:
+ if not match:
+ if t in registry:
+ match = t
+ continue
+ if (t in registry and not issubclass(match, t)
+ and match not in cls.__mro__):
+ # `match` is an ABC but there is another unrelated, equally
+ # matching ABC. Refuse the temptation to guess.
+ raise RuntimeError("Ambiguous dispatch: {} or {}".format(
+ match, t))
+ return registry[match]
+ else:
+ for t in cls.__mro__:
+ if t in registry:
+ return registry[t]
+ return func
+
+ def wrapper(*args, **kw):
+ nonlocal cache_token
+ if cache_token is not None:
+ current_token = get_cache_token()
+ if cache_token != current_token:
+ dispatch_cache.clear()
+ cache_token = current_token
+ cls = args[0].__class__
+ try:
+ impl = dispatch_cache[cls]
+ except KeyError:
+ impl = dispatch_cache[cls] = dispatch(cls)
+ return impl(*args, **kw)
+
+ def register(typ, func=None):
+ """generic_func.register(type, func) -> func
+
+ Registers a new overload for the given `type` on a `generic_func`.
+
+ """
+ nonlocal cache_token
+ if func is None:
+ return lambda f: register(typ, f)
+ registry[typ] = func
+ if cache_token is None and hasattr(typ, '__abstractmethods__'):
+ cache_token = get_cache_token()
+ dispatch_cache.clear()
+ return func
+
+ registry[object] = func
+ wrapper.register = register
+ wrapper.dispatch = dispatch
+ wrapper.registry = MappingProxyType(registry)
+ update_wrapper(wrapper, func)
+ return wrapper
+
diff --git a/test_singledispatch.py b/test_singledispatch.py
new file mode 100644
index 0000000..2d562cd
--- /dev/null
+++ b/test_singledispatch.py
@@ -0,0 +1,354 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import collections
+import decimal
+from itertools import permutations
+import singledispatch as functools
+import unittest
+
+
+class TestSingleDispatch(unittest.TestCase):
+ def test_simple_overloads(self):
+ @functools.singledispatch
+ def g(obj):
+ return "base"
+ def g_int(i):
+ return "integer"
+ g.register(int, g_int)
+ self.assertEqual(g("str"), "base")
+ self.assertEqual(g(1), "integer")
+ self.assertEqual(g([1,2,3]), "base")
+
+ def test_mro(self):
+ @functools.singledispatch
+ def g(obj):
+ return "base"
+ class C:
+ pass
+ class D(C):
+ pass
+ def g_C(c):
+ return "C"
+ g.register(C, g_C)
+ self.assertEqual(g(C()), "C")
+ self.assertEqual(g(D()), "C")
+
+ def test_classic_classes(self):
+ @functools.singledispatch
+ def g(obj):
+ return "base"
+ class C:
+ pass
+ class D(C):
+ pass
+ def g_C(c):
+ return "C"
+ g.register(C, g_C)
+ self.assertEqual(g(C()), "C")
+ self.assertEqual(g(D()), "C")
+
+ def test_register_decorator(self):
+ @functools.singledispatch
+ def g(obj):
+ return "base"
+ @g.register(int)
+ def g_int(i):
+ return "int %s" % (i,)
+ self.assertEqual(g(""), "base")
+ self.assertEqual(g(12), "int 12")
+ self.assertIs(g.dispatch(int), g_int)
+ self.assertIs(g.dispatch(object), g.dispatch(str))
+ # Note: in the assert above this is not g.
+ # @singledispatch returns the wrapper.
+
+ def test_wrapping_attributes(self):
+ @functools.singledispatch
+ def g(obj):
+ "Simple test"
+ return "Test"
+ self.assertEqual(g.__name__, "g")
+ self.assertEqual(g.__doc__, "Simple test")
+
+ @unittest.skipUnless(decimal, 'requires _decimal')
+ def test_c_classes(self):
+ @functools.singledispatch
+ def g(obj):
+ return "base"
+ @g.register(decimal.DecimalException)
+ def _(obj):
+ return obj.args
+ subn = decimal.Subnormal("Exponent < Emin")
+ rnd = decimal.Rounded("Number got rounded")
+ self.assertEqual(g(subn), ("Exponent < Emin",))
+ self.assertEqual(g(rnd), ("Number got rounded",))
+ @g.register(decimal.Subnormal)
+ def _(obj):
+ return "Too small to care."
+ self.assertEqual(g(subn), "Too small to care.")
+ self.assertEqual(g(rnd), ("Number got rounded",))
+
+ def test_compose_mro(self):
+ c = collections
+ mro = functools._compose_mro
+ bases = [c.Sequence, c.MutableMapping, c.Mapping, c.Set]
+ for haystack in permutations(bases):
+ m = mro(dict, haystack)
+ self.assertEqual(m, [dict, c.MutableMapping, c.Mapping, object])
+ bases = [c.Container, c.Mapping, c.MutableMapping, c.OrderedDict]
+ for haystack in permutations(bases):
+ m = mro(c.ChainMap, haystack)
+ self.assertEqual(m, [c.ChainMap, c.MutableMapping, c.Mapping,
+ c.Sized, c.Iterable, c.Container, object])
+ # Note: The MRO order below depends on haystack ordering.
+ m = mro(c.defaultdict, [c.Sized, c.Container, str])
+ self.assertEqual(m, [c.defaultdict, dict, c.Container, c.Sized, object])
+ m = mro(c.defaultdict, [c.Container, c.Sized, str])
+ self.assertEqual(m, [c.defaultdict, dict, c.Sized, c.Container, object])
+
+ def test_register_abc(self):
+ c = collections
+ d = {"a": "b"}
+ l = [1, 2, 3]
+ s = {object(), None}
+ f = frozenset(s)
+ t = (1, 2, 3)
+ @functools.singledispatch
+ def g(obj):
+ return "base"
+ self.assertEqual(g(d), "base")
+ self.assertEqual(g(l), "base")
+ self.assertEqual(g(s), "base")
+ self.assertEqual(g(f), "base")
+ self.assertEqual(g(t), "base")
+ g.register(c.Sized, lambda obj: "sized")
+ self.assertEqual(g(d), "sized")
+ self.assertEqual(g(l), "sized")
+ self.assertEqual(g(s), "sized")
+ self.assertEqual(g(f), "sized")
+ self.assertEqual(g(t), "sized")
+ g.register(c.MutableMapping, lambda obj: "mutablemapping")
+ self.assertEqual(g(d), "mutablemapping")
+ self.assertEqual(g(l), "sized")
+ self.assertEqual(g(s), "sized")
+ self.assertEqual(g(f), "sized")
+ self.assertEqual(g(t), "sized")
+ g.register(c.ChainMap, lambda obj: "chainmap")
+ self.assertEqual(g(d), "mutablemapping") # irrelevant ABCs registered
+ self.assertEqual(g(l), "sized")
+ self.assertEqual(g(s), "sized")
+ self.assertEqual(g(f), "sized")
+ self.assertEqual(g(t), "sized")
+ g.register(c.MutableSequence, lambda obj: "mutablesequence")
+ self.assertEqual(g(d), "mutablemapping")
+ self.assertEqual(g(l), "mutablesequence")
+ self.assertEqual(g(s), "sized")
+ self.assertEqual(g(f), "sized")
+ self.assertEqual(g(t), "sized")
+ g.register(c.MutableSet, lambda obj: "mutableset")
+ self.assertEqual(g(d), "mutablemapping")
+ self.assertEqual(g(l), "mutablesequence")
+ self.assertEqual(g(s), "mutableset")
+ self.assertEqual(g(f), "sized")
+ self.assertEqual(g(t), "sized")
+ g.register(c.Mapping, lambda obj: "mapping")
+ self.assertEqual(g(d), "mutablemapping") # not specific enough
+ self.assertEqual(g(l), "mutablesequence")
+ self.assertEqual(g(s), "mutableset")
+ self.assertEqual(g(f), "sized")
+ self.assertEqual(g(t), "sized")
+ g.register(c.Sequence, lambda obj: "sequence")
+ self.assertEqual(g(d), "mutablemapping")
+ self.assertEqual(g(l), "mutablesequence")
+ self.assertEqual(g(s), "mutableset")
+ self.assertEqual(g(f), "sized")
+ self.assertEqual(g(t), "sequence")
+ g.register(c.Set, lambda obj: "set")
+ self.assertEqual(g(d), "mutablemapping")
+ self.assertEqual(g(l), "mutablesequence")
+ self.assertEqual(g(s), "mutableset")
+ self.assertEqual(g(f), "set")
+ self.assertEqual(g(t), "sequence")
+ g.register(dict, lambda obj: "dict")
+ self.assertEqual(g(d), "dict")
+ self.assertEqual(g(l), "mutablesequence")
+ self.assertEqual(g(s), "mutableset")
+ self.assertEqual(g(f), "set")
+ self.assertEqual(g(t), "sequence")
+ g.register(list, lambda obj: "list")
+ self.assertEqual(g(d), "dict")
+ self.assertEqual(g(l), "list")
+ self.assertEqual(g(s), "mutableset")
+ self.assertEqual(g(f), "set")
+ self.assertEqual(g(t), "sequence")
+ g.register(set, lambda obj: "concrete-set")
+ self.assertEqual(g(d), "dict")
+ self.assertEqual(g(l), "list")
+ self.assertEqual(g(s), "concrete-set")
+ self.assertEqual(g(f), "set")
+ self.assertEqual(g(t), "sequence")
+ g.register(frozenset, lambda obj: "frozen-set")
+ self.assertEqual(g(d), "dict")
+ self.assertEqual(g(l), "list")
+ self.assertEqual(g(s), "concrete-set")
+ self.assertEqual(g(f), "frozen-set")
+ self.assertEqual(g(t), "sequence")
+ g.register(tuple, lambda obj: "tuple")
+ self.assertEqual(g(d), "dict")
+ self.assertEqual(g(l), "list")
+ self.assertEqual(g(s), "concrete-set")
+ self.assertEqual(g(f), "frozen-set")
+ self.assertEqual(g(t), "tuple")
+
+ def test_mro_conflicts(self):
+ c = collections
+
+ @functools.singledispatch
+ def g(arg):
+ return "base"
+
+ class O(c.Sized):
+ def __len__(self):
+ return 0
+
+ o = O()
+ self.assertEqual(g(o), "base")
+ g.register(c.Iterable, lambda arg: "iterable")
+ g.register(c.Container, lambda arg: "container")
+ g.register(c.Sized, lambda arg: "sized")
+ g.register(c.Set, lambda arg: "set")
+ self.assertEqual(g(o), "sized")
+ c.Iterable.register(O)
+ self.assertEqual(g(o), "sized") # because it's explicitly in __mro__
+ c.Container.register(O)
+ self.assertEqual(g(o), "sized") # see above: Sized is in __mro__
+
+ class P:
+ pass
+
+ p = P()
+ self.assertEqual(g(p), "base")
+ c.Iterable.register(P)
+ self.assertEqual(g(p), "iterable")
+ c.Container.register(P)
+ with self.assertRaises(RuntimeError) as re:
+ g(p)
+ self.assertEqual(
+ str(re),
+ ("Ambiguous dispatch: <class 'collections.abc.Container'> "
+ "or <class 'collections.abc.Iterable'>"),
+ )
+
+ class Q(c.Sized):
+ def __len__(self):
+ return 0
+
+ q = Q()
+ self.assertEqual(g(q), "sized")
+ c.Iterable.register(Q)
+ self.assertEqual(g(q), "sized") # because it's explicitly in __mro__
+ c.Set.register(Q)
+ self.assertEqual(g(q), "set") # because c.Set is a subclass of
+ # c.Sized which is explicitly in
+ # __mro__
+
+ def test_cache_invalidation(self):
+ from collections import UserDict
+ class TracingDict(UserDict):
+ def __init__(self, *args, **kwargs):
+ super(TracingDict, self).__init__(*args, **kwargs)
+ self.set_ops = []
+ self.get_ops = []
+ def __getitem__(self, key):
+ result = self.data[key]
+ self.get_ops.append(key)
+ return result
+ def __setitem__(self, key, value):
+ self.set_ops.append(key)
+ self.data[key] = value
+ def clear(self):
+ self.data.clear()
+ _orig_wkd = functools.WeakKeyDictionary
+ td = TracingDict()
+ functools.WeakKeyDictionary = lambda: td
+ c = collections
+ @functools.singledispatch
+ def g(arg):
+ return "base"
+ d = {}
+ l = []
+ self.assertEqual(len(td), 0)
+ self.assertEqual(g(d), "base")
+ self.assertEqual(len(td), 1)
+ self.assertEqual(td.get_ops, [])
+ self.assertEqual(td.set_ops, [dict])
+ self.assertEqual(td.data[dict], g.registry[object])
+ self.assertEqual(g(l), "base")
+ self.assertEqual(len(td), 2)
+ self.assertEqual(td.get_ops, [])
+ self.assertEqual(td.set_ops, [dict, list])
+ self.assertEqual(td.data[dict], g.registry[object])
+ self.assertEqual(td.data[list], g.registry[object])
+ self.assertEqual(td.data[dict], td.data[list])
+ self.assertEqual(g(l), "base")
+ self.assertEqual(g(d), "base")
+ self.assertEqual(td.get_ops, [list, dict])
+ self.assertEqual(td.set_ops, [dict, list])
+ g.register(list, lambda arg: "list")
+ self.assertEqual(td.get_ops, [list, dict])
+ self.assertEqual(len(td), 0)
+ self.assertEqual(g(d), "base")
+ self.assertEqual(len(td), 1)
+ self.assertEqual(td.get_ops, [list, dict])
+ self.assertEqual(td.set_ops, [dict, list, dict])
+ self.assertEqual(td.data[dict], g.dispatch(dict))
+ self.assertEqual(g(l), "list")
+ self.assertEqual(len(td), 2)
+ self.assertEqual(td.get_ops, [list, dict])
+ self.assertEqual(td.set_ops, [dict, list, dict, list])
+ self.assertEqual(td.data[list], g.dispatch(list))
+ class X:
+ pass
+ c.MutableMapping.register(X) # Will not invalidate the cache,
+ # not using ABCs yet.
+ self.assertEqual(g(d), "base")
+ self.assertEqual(g(l), "list")
+ self.assertEqual(td.get_ops, [list, dict, dict, list])
+ self.assertEqual(td.set_ops, [dict, list, dict, list])
+ g.register(c.Sized, lambda arg: "sized")
+ self.assertEqual(len(td), 0)
+ self.assertEqual(g(d), "sized")
+ self.assertEqual(len(td), 1)
+ self.assertEqual(td.get_ops, [list, dict, dict, list])
+ self.assertEqual(td.set_ops, [dict, list, dict, list, dict])
+ self.assertEqual(g(l), "list")
+ self.assertEqual(len(td), 2)
+ self.assertEqual(td.get_ops, [list, dict, dict, list])
+ self.assertEqual(td.set_ops, [dict, list, dict, list, dict, list])
+ self.assertEqual(g(l), "list")
+ self.assertEqual(g(d), "sized")
+ self.assertEqual(td.get_ops, [list, dict, dict, list, list, dict])
+ self.assertEqual(td.set_ops, [dict, list, dict, list, dict, list])
+ c.MutableSet.register(X) # Will invalidate the cache.
+ self.assertEqual(len(td), 2) # Stale cache.
+ self.assertEqual(g(l), "list")
+ self.assertEqual(len(td), 1)
+ g.register(c.MutableMapping, lambda arg: "mutablemapping")
+ self.assertEqual(len(td), 0)
+ self.assertEqual(g(d), "mutablemapping")
+ self.assertEqual(len(td), 1)
+ self.assertEqual(g(l), "list")
+ self.assertEqual(len(td), 2)
+ g.register(dict, lambda arg: "dict")
+ self.assertEqual(g(d), "dict")
+ self.assertEqual(g(l), "list")
+ functools.WeakKeyDictionary = _orig_wkd
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..c0fc179
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,12 @@
+[tox]
+envlist = py26,py27,py32,py33
+
+[testenv]
+changedir = test
+commands =
+ {envbindir}/python test_singledispatch.py
+
+[testenv:py26]
+basepython = python2.6
+deps =
+ unittest2