diff --git a/.gitignore b/.gitignore index 0d20b64..c3cd320 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ *.pyc +*.swp +*.so +*.html diff --git a/.travis.yml b/.travis.yml index a0b7108..9085384 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,11 +6,20 @@ python: - "3.3" - "pypy" +env: + - USE_CYTHON=True + - USE_CYTHON=False + install: - pip install coverage --use-mirrors - + - if [[ $USE_CYTHON == 'True' ]]; then + - pip install cython --use-mirrors + - python setup.py build_ext --inplace --with-cython + - fi script: - nosetests --with-doctest + - if [[ $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then USE_CYTHON=False ; fi + - python -c 'import multipledispatch ; assert multipledispatch.dispatcher.USE_CYTHON == '$USE_CYTHON after_success: - if [[ $TRAVIS_PYTHON_VERSION != 'pypy' ]]; then pip install coveralls --use-mirrors ; coveralls ; fi diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..565c9e4 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +PYTHON ?= python + +inplace: + $(PYTHON) setup.py build_ext --inplace --with-cython + +test: inplace + nosetests -s --with-doctest multipledispatch/ diff --git a/multipledispatch/_dispatcher.pxd b/multipledispatch/_dispatcher.pxd new file mode 100644 index 0000000..532ed50 --- /dev/null +++ b/multipledispatch/_dispatcher.pxd @@ -0,0 +1,10 @@ +cdef class DispatcherBase: + cdef public object name + cdef public object funcs + cdef public object ordering + cdef public object _cache + + +cdef class MethodDispatcherBase(DispatcherBase): + cdef public object obj + cdef public object cls diff --git a/multipledispatch/_dispatcher.pyx b/multipledispatch/_dispatcher.pyx new file mode 100644 index 0000000..7cf287f --- /dev/null +++ b/multipledispatch/_dispatcher.pyx @@ -0,0 +1,108 @@ +from .conflict import ordering +from .dispatcher import str_signature + +from cpython.dict cimport PyDict_GetItem, PyDict_SetItem +from cpython.object cimport PyObject_Call +from cpython.ref cimport PyObject, Py_INCREF +from cpython.tuple cimport (PyTuple_GET_ITEM, PyTuple_GET_SIZE, PyTuple_New, + PyTuple_SET_ITEM) + + +cdef class DispatcherBase: + def __call__(self, *args, **kwargs): + cdef PyObject *obj + cdef object val + cdef Py_ssize_t i, N = PyTuple_GET_SIZE(args) + if N == 1: + types = type(PyTuple_GET_ITEM(args, 0)) + else: + types = PyTuple_New(N) + for i in range(N): + val = type(PyTuple_GET_ITEM(args, i)) + Py_INCREF(val) + PyTuple_SET_ITEM(types, i, val) + + obj = PyDict_GetItem(self._cache, types) + if obj is not NULL: + return PyObject_Call(obj, args, kwargs) + elif N == 1: + # *Always* pass a tuple to `self.resolve` + val = self.resolve((types,)) + else: + val = self.resolve(types) + PyDict_SetItem(self._cache, types, val) + return PyObject_Call(val, args, kwargs) + + def resolve(self, types): + """ Deterimine appropriate implementation for this type signature + + This method is internal. Users should call this object as a function. + Implementation resolution occurs within the ``__call__`` method. + + >>> from multipledispatch import dispatch + >>> @dispatch(int) + ... def inc(x): + ... return x + 1 + + >>> implementation = inc.resolve((int,)) + >>> implementation(3) + 4 + + >>> inc.resolve((float,)) + Traceback (most recent call last): + ... + NotImplementedError: Could not find signature for inc: + + See Also: + ``multipledispatch.conflict`` - module to determine resolution order + """ + cdef PyObject *obj = PyDict_GetItem(self.funcs, types) + if obj is not NULL: + return obj + + n = len(types) + for signature in self.ordering: + if len(signature) == n and all(map(issubclass, types, signature)): + result = self.funcs[signature] + return result + raise NotImplementedError('Could not find signature for %s: <%s>' % + (self.name, str_signature(types))) + + def __reduce__(self): + return (type(self), (self.name,), self.funcs) + + def __setstate__(self, state): + self.funcs = state + self._cache = {} + self.ordering = ordering(self.funcs) + + +cdef class MethodDispatcherBase(DispatcherBase): + def __get__(self, instance, owner): + self.obj = instance + self.cls = owner + return self + + def __call__(self, *args, **kwargs): + cdef PyObject *obj + cdef object val + cdef Py_ssize_t i, N = PyTuple_GET_SIZE(args) + types = PyTuple_New(N) # = tuple([type(arg) for arg in args]) + selfargs = PyTuple_New(N + 1) # = (self.obj,) + args + Py_INCREF(self.obj) + PyTuple_SET_ITEM(selfargs, 0, self.obj) + for i in range(N): + val = PyTuple_GET_ITEM(args, i) + Py_INCREF(val) + PyTuple_SET_ITEM(selfargs, i + 1, val) + val = type(val) + Py_INCREF(val) + PyTuple_SET_ITEM(types, i, val) + + obj = PyDict_GetItem(self._cache, types) + if obj is not NULL: + return PyObject_Call(obj, selfargs, kwargs) + else: + val = self.resolve(types) + PyDict_SetItem(self._cache, types, val) + return PyObject_Call(val, selfargs, kwargs) diff --git a/multipledispatch/dispatcher.py b/multipledispatch/dispatcher.py index 6fd3728..877751b 100644 --- a/multipledispatch/dispatcher.py +++ b/multipledispatch/dispatcher.py @@ -3,6 +3,101 @@ from .utils import expand_tuples +def str_signature(sig): + """ String representation of type signature + + >>> str_signature((int, float)) + 'int, float' + """ + return ', '.join(cls.__name__ for cls in sig) + + +try: + import platform + USE_CYTHON = platform.python_implementation() == 'CPython' + if USE_CYTHON: + from ._dispatcher import DispatcherBase, MethodDispatcherBase +except ImportError: + USE_CYTHON = False + + +if not USE_CYTHON: + class DispatcherBase(object): + __slots__ = 'name', 'funcs', 'ordering', '_cache' + + def __call__(self, *args, **kwargs): + types = tuple([type(arg) for arg in args]) + try: + func = self._cache[types] + except KeyError: + func = self.resolve(types) + self._cache[types] = func + return func(*args, **kwargs) + + def resolve(self, types): + """ Deterimine appropriate implementation for this type signature + + This method is internal. Users should call this object as a function. + Implementation resolution occurs within the ``__call__`` method. + + >>> from multipledispatch import dispatch + >>> @dispatch(int) + ... def inc(x): + ... return x + 1 + + >>> implementation = inc.resolve((int,)) + >>> implementation(3) + 4 + + >>> inc.resolve((float,)) + Traceback (most recent call last): + ... + NotImplementedError: Could not find signature for inc: + + See Also: + ``multipledispatch.conflict`` - module to determine resolution order + """ + + if types in self.funcs: + return self.funcs[types] + + n = len(types) + for signature in self.ordering: + if len(signature) == n and all(map(issubclass, types, signature)): + result = self.funcs[signature] + return result + raise NotImplementedError('Could not find signature for %s: <%s>' % + (self.name, str_signature(types))) + + def __getstate__(self): + return {'name': self.name, + 'funcs': self.funcs} + + def __setstate__(self, d): + self.name = d['name'] + self.funcs = d['funcs'] + self.ordering = ordering(self.funcs) + self._cache = dict() + + + class MethodDispatcherBase(DispatcherBase): + __slots__ = 'obj', 'cls' + + 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]) + try: + func = self._cache[types] + except KeyError: + func = self.resolve(types) + self._cache[types] = func + return func(self.obj, *args, **kwargs) + + def ambiguity_warn(dispatcher, ambiguities): """ Raise warning when ambiguity is detected @@ -20,7 +115,7 @@ def ambiguity_warn(dispatcher, ambiguities): warn(warning_text(dispatcher.name, ambiguities), AmbiguityWarning) -class Dispatcher(object): +class Dispatcher(DispatcherBase): """ Dispatch methods based on type signature Use ``dispatch`` to add implementations @@ -42,7 +137,7 @@ class Dispatcher(object): >>> f(3.0) 2.0 """ - __slots__ = 'name', 'funcs', 'ordering', '_cache' + __slots__ = () def __init__(self, name): self.name = name @@ -112,65 +207,10 @@ def add(self, signature, func, on_ambiguity=ambiguity_warn): on_ambiguity(self, amb) self._cache.clear() - def __call__(self, *args, **kwargs): - types = tuple([type(arg) for arg in args]) - try: - func = self._cache[types] - except KeyError: - func = self.resolve(types) - self._cache[types] = func - return func(*args, **kwargs) - def __str__(self): return "" % self.name __repr__ = __str__ - def resolve(self, types): - """ Deterimine appropriate implementation for this type signature - - This method is internal. Users should call this object as a function. - Implementation resolution occurs within the ``__call__`` method. - - >>> from multipledispatch import dispatch - >>> @dispatch(int) - ... def inc(x): - ... return x + 1 - - >>> implementation = inc.resolve((int,)) - >>> implementation(3) - 4 - - >>> inc.resolve((float,)) - Traceback (most recent call last): - ... - NotImplementedError: Could not find signature for inc: - - See Also: - ``multipledispatch.conflict`` - module to determine resolution order - """ - - if types in self.funcs: - return self.funcs[types] - - n = len(types) - for signature in self.ordering: - if len(signature) == n and all(map(issubclass, types, signature)): - result = self.funcs[signature] - return result - raise NotImplementedError('Could not find signature for %s: <%s>' % - (self.name, str_signature(types))) - - def __getstate__(self): - return {'name': self.name, - 'funcs': self.funcs} - - def __setstate__(self, d): - self.name = d['name'] - self.funcs = d['funcs'] - self.ordering = ordering(self.funcs) - self._cache = dict() - - @property def __doc__(self): doc = " Multiply dispatched method: %s\n\n" % self.name @@ -196,30 +236,14 @@ def __doc__(self): return doc -class MethodDispatcher(Dispatcher): +class MethodDispatcher(MethodDispatcherBase, Dispatcher): """ Dispatch methods based on type signature See Also: Dispatcher """ - 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.resolve(types) - return func(self.obj, *args, **kwargs) - - -def str_signature(sig): - """ String representation of type signature + __slots__ = () - >>> str_signature((int, float)) - 'int, float' - """ - return ', '.join(cls.__name__ for cls in sig) def warning_text(name, amb): """ The text for ambiguity warnings """ diff --git a/setup.py b/setup.py index cdf8e02..8e37fc5 100755 --- a/setup.py +++ b/setup.py @@ -1,18 +1,121 @@ #!/usr/bin/env python +""" Build and install ``multipledispatch`` with or without Cython or C compiler +Building with Cython must be specified with the "--with-cython" option such as: + + $ python setup.py build_ext --inplace --with-cython + +Not using Cython by default makes contributing to ``multipledispatch`` easy, +because Cython and a C compiler are not required for development or usage. + +During installation, C extension modules will be automatically built with a +C compiler if possible, but will fail gracefully if there is an error during +compilation. Use of C extensions significantly improves the performance of +``multipledispatch``, but a pure Python implementation will be used if the +extension modules are unavailable. + +""" +import sys +import warnings from os.path import exists -from setuptools import setup +from setuptools import setup, Extension +from distutils.command.build_ext import build_ext +from distutils.errors import (CCompilerError, DistutilsExecError, + DistutilsPlatformError) import multipledispatch +try: + from Cython.Build import cythonize + has_cython = True +except ImportError: + has_cython = False + +use_cython = False +if '--without-cython' in sys.argv: + sys.argv.remove('--without-cython') + +if '--with-cython' in sys.argv: + use_cython = True + sys.argv.remove('--with-cython') + if use_cython and not has_cython: + raise RuntimeError('ERROR: Cython not found. Exiting.\n ' + 'Install Cython or don\'t use "--with-cython"') + +suffix = '.pyx' if use_cython else '.c' +ext_modules = [] +for modname in ['_dispatcher']: + ext_modules.append(Extension('multipledispatch.' + modname, + ['multipledispatch/' + modname + suffix])) +if use_cython: + ext_modules = cythonize(ext_modules) + + +build_exceptions = (CCompilerError, DistutilsExecError, DistutilsPlatformError, + IOError, SystemError) + + +class build_ext_may_fail(build_ext): + """ Allow compilation of extensions modules to fail, but warn if they do""" + + warning_message = """ +********************************************************************* +WARNING: %s + could not be compiled. See the output above for details. + +Compiled C extension modules are not required for `multipledispatch` +to run, but they do result in significant speed improvements. +Proceeding to build `multipledispatch` as a pure Python package. + +If you are using Linux, you probably need to install GCC or the +Python development package. + +Debian and Ubuntu users should issue the following command: + + $ sudo apt-get install build-essential python-dev + +RedHat, CentOS, and Fedora users should issue the following command: + + $ sudo yum install gcc python-devel + +********************************************************************* +""" + + def run(self): + try: + build_ext.run(self) + except build_exceptions: + self.warn_failed() + + def build_extension(self, ext): + try: + build_ext.build_extension(self, ext) + except build_exceptions: + self.warn_failed(name=ext.name) + + def warn_failed(self, name=None): + if name is None: + name = 'Extension modules' + else: + name = 'The "%s" extension module' % name + exc = sys.exc_info()[1] + sys.stdout.write('%s\n' % str(exc)) + warnings.warn(self.warning_message % name) + + +cmdclass = {} if use_cython else {'build_ext': build_ext_may_fail} + setup(name='multipledispatch', version=multipledispatch.__version__, description='Multiple dispatch', + cmdclass=cmdclass, + ext_modules=ext_modules, url='http://github.com/mrocklin/multipledispatch/', author='Matthew Rocklin', author_email='mrocklin@gmail.com', license='BSD', keywords='dispatch', packages=['multipledispatch'], + package_data={'multipledispatch': ['*.pxd', '*.pyx']}, long_description=(open('README.md').read() if exists('README.md') else ''), zip_safe=False)