Repository: seequent/methoddispatch

Issue:
Allow passing method object to register
You can't navigate from a string to the method you are dispatching.

permit:

```
class SubClass(BaseClass):
   @register(BaseClass.foo, int)
   def foo_int(self, bar):
      ...
```

Comments:

Patch:
diff --git a/README.md b/README.md
index ba94edb..68fd599 100644
--- a/README.md
+++ b/README.md
@@ -2,12 +2,10 @@
 
 [![Build Status](https://travis-ci.com/seequent/methoddispatch.svg?branch=master)](https://travis-ci.com/seequent/methoddispatch)
 
+Python 3.4 added the ``singledispatch`` decorator to the ``functools`` standard library module.
+This library extends this functionality to instance methods (and works for functions too).
 
-[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 extends this functionality to instance methods (and works for functions too)
-
-To define a generic method , decorate it with the ``@singledispatch`` decorator. Note that the dispatch happens on the type of the first argument, create your function accordingly::
+To define a generic method , decorate it with the ``@singledispatch`` decorator. Note that the dispatch happens on the type of the first argument, create your function accordingly.
 
     >>> from methoddispatch import singledispatch, register, SingleDispatch
 
@@ -110,16 +108,20 @@ Decorating class methods requires the class to inherit from ``SingleDispatch``
     'int'
 
 Subclasses can extend the type registry of the function on the base class with their own overrides.
-Because the ``foo`` function is not in scope, the ``methoddispatch.register`` decorator must be used instead
+Because we do not want to modify the base class ``foo`` registry the ``methoddispatch.register`` decorator must be used instead of ``foo.register``.  The module level ``register`` function takes either the method name or the method itself as the first parameter and the dispatch type as the second.
 
     >>> class SubClass(BaseClass):
     ...     @register('foo', float)
     ...     def foo_float(self, bar):
     ...         return 'float'
     ...
+    ...     @register(BaseClass.foo, str)
+    ...     def foo_str(self, bar):
+    ...         return 'str'
+    ...
     >>> s = SubClass()
-    >>> s.foo(1)
-    'int'
+    >>> s.foo('')
+    'str'
     >>> s.foo(1.0)
     'float'
 
@@ -144,7 +146,7 @@ Decorating a method override with a different type (not a good idea) will regist
 
 In Python 3.7 and later, for functions annotated with types, the decorator will infer the type of the first argument automatically as shown below
 
-    >>> class BaseClass(SingleDispatch):
+    >>> class BaseClassAnno(SingleDispatch):
     ...     @singledispatch
     ...     def foo(self, bar):
     ...         return 'default'
@@ -153,13 +155,13 @@ In Python 3.7 and later, for functions annotated with types, the decorator will
     ...     def foo_int(self, bar: int):
     ...         return 'int'
     ...
-    >>> class SubClass(BaseClass):
+    >>> class SubClassAnno(BaseClassAnno):
     ...     @register('foo')
     ...     def foo_float(self, bar: float):
     ...         return 'float'
 
 In Python 3.6 and earlier, the ``SingleDispatch`` class uses a meta-class ``SingleDispatchMeta`` to manage the dispatch registries.  However in Python 3.7 and later the ``__init_subclass__`` method is used instead.
-If your class also inhertits from an ABC interface you can use the ``SingleDispatchABCMeta`` metaclass in Python 3.6 and earlier.
+If your class also inherits from an ABC interface you can use the ``SingleDispatchABCMeta`` metaclass in Python 3.6 and earlier.
 
 Finally, accessing the method ``foo`` via a class will use the dispatch registry for that class
 
diff --git a/methoddispatch/__init__.py b/methoddispatch/__init__.py
index 242cd91..ea0cd33 100644
--- a/methoddispatch/__init__.py
+++ b/methoddispatch/__init__.py
@@ -2,11 +2,12 @@
 """
 # methoddispatch
 
- [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.
+[![Build Status](https://travis-ci.com/seequent/methoddispatch.svg?branch=master)](https://travis-ci.com/seequent/methoddispatch)
 
-This library extends this functionality to instance methods (and works for functions too)
+Python 3.4 added the ``singledispatch`` decorator to the ``functools`` standard library module.
+This library extends this functionality to instance methods (and works for functions too).
 
-To define a generic method , decorate it with the ``@singledispatch`` decorator. Note that the dispatch happens on the type of the first argument, create your function accordingly::
+To define a generic method , decorate it with the ``@singledispatch`` decorator. Note that the dispatch happens on the type of the first argument, create your function accordingly.
 
     >>> from methoddispatch import singledispatch, register, SingleDispatch
 
@@ -109,16 +110,20 @@ Decorating class methods requires the class to inherit from ``SingleDispatch``
     'int'
 
 Subclasses can extend the type registry of the function on the base class with their own overrides.
-Because the ``foo`` function is not in scope, the ``methoddispatch.register`` decorator must be used instead
+Because we do not want to modify the base class ``foo`` registry the ``methoddispatch.register`` decorator must be used instead of ``foo.register``.  The module level ``register`` function takes either the method name or the method itself as the first parameter and the dispatch type as the second.
 
     >>> class SubClass(BaseClass):
     ...     @register('foo', float)
     ...     def foo_float(self, bar):
     ...         return 'float'
     ...
+    ...     @register(BaseClass.foo, str)
+    ...     def foo_str(self, bar):
+    ...         return 'str'
+    ...
     >>> s = SubClass()
-    >>> s.foo(1)
-    'int'
+    >>> s.foo('')
+    'str'
     >>> s.foo(1.0)
     'float'
 
@@ -143,7 +148,7 @@ Decorating a method override with a different type (not a good idea) will regist
 
 In Python 3.7 and later, for functions annotated with types, the decorator will infer the type of the first argument automatically as shown below
 
-    >>> class BaseClass2(SingleDispatch):
+    >>> class BaseClassAnno(SingleDispatch):
     ...     @singledispatch
     ...     def foo(self, bar):
     ...         return 'default'
@@ -152,13 +157,13 @@ In Python 3.7 and later, for functions annotated with types, the decorator will
     ...     def foo_int(self, bar: int):
     ...         return 'int'
     ...
-    >>> class SubClass2(BaseClass2):
+    >>> class SubClassAnno(BaseClassAnno):
     ...     @register('foo')
     ...     def foo_float(self, bar: float):
     ...         return 'float'
 
 In Python 3.6 and earlier, the ``SingleDispatch`` class uses a meta-class ``SingleDispatchMeta`` to manage the dispatch registries.  However in Python 3.7 and later the ``__init_subclass__`` method is used instead.
-If your class also inhertits from an ABC interface you can use the ``SingleDispatchABCMeta`` metaclass in Python 3.6 and earlier.
+If your class also inherits from an ABC interface you can use the ``SingleDispatchABCMeta`` metaclass in Python 3.6 and earlier.
 
 Finally, accessing the method ``foo`` via a class will use the dispatch registry for that class
 
diff --git a/methoddispatch/methoddispatch2.py b/methoddispatch/methoddispatch2.py
index df42610..d136ad4 100644
--- a/methoddispatch/methoddispatch2.py
+++ b/methoddispatch/methoddispatch2.py
@@ -265,17 +265,20 @@ class sd_method(object):
             return self.dispatch(args[0].__class__)(self._instance, *args, **kwargs)
 
 
-def _fixup_class_attributes(attributes, bases):
+def _fixup_class_attributes(cls):
     generics = []
-    for base in bases:
+    attributes = cls.__dict__
+    patched = set()
+    for base in cls.mro()[1:]:
         if isinstance(base, SingleDispatchMeta):
             for name, value in base.__dict__.items():
-                if isinstance(value, singledispatch):
+                if isinstance(value, singledispatch) and name not in patched:
                     if name in attributes:
                         raise RuntimeError('Cannot override generic function.  '
-                                           'Try @register({}, object) instead.'.format(name))
+                                           'Try @register("{}", object) instead.'.format(name))
                     generic = value.copy()
-                    attributes[name] = generic
+                    setattr(cls, name, generic)
+                    patched.add(name)
                     generics.append(generic)
     for name, value in attributes.items():
         if not callable(value) or isinstance(value, singledispatch):
@@ -298,8 +301,8 @@ class SingleDispatchMeta(type):
     so that registered types on sub-classes do not modify the base class.
     """
     def __new__(mcs, clsname, bases, attributes):
-        _fixup_class_attributes(attributes, bases)
         cls = super(SingleDispatchMeta, mcs).__new__(mcs, clsname, bases, attributes)
+        _fixup_class_attributes(cls)
         return cls
 
 
@@ -317,9 +320,10 @@ class SingleDispatchABC(object):
 
 def register(name, cls):
     """ Decorator for methods on a sub-class to register an overload on a base-class generic method
-    name is the name of the generic method on the base class
-    cls is the type to register
+    :param name: is the name of the generic method on the base class, or the unbound method itself
+    :param cls: is the type to register
     """
+    name = getattr(name, '__name__', name)  # __name__ exists on sd_method courtesy of update_wrapper
     def wrapper(func):
         overloads = getattr(func, 'overloads', [])
         overloads.append((name, cls))
diff --git a/methoddispatch/methoddispatch3.py b/methoddispatch/methoddispatch3.py
index 7efe791..bb82d51 100644
--- a/methoddispatch/methoddispatch3.py
+++ b/methoddispatch/methoddispatch3.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 from abc import get_cache_token, ABCMeta
-from functools import update_wrapper, _find_impl, _c3_mro
+from functools import update_wrapper, _find_impl
 from types import MappingProxyType
 from weakref import WeakKeyDictionary
 
@@ -111,15 +111,17 @@ class sd_method(object):
 def _fixup_class_attributes(cls):
     generics = []
     attributes = cls.__dict__
+    patched = set()
     for base in cls.mro()[1:]:
         if isinstance(base, SingleDispatchMeta):
             for name, value in base.__dict__.items():
-                if isinstance(value, singledispatch):
+                if isinstance(value, singledispatch) and name not in patched:
                     if name in attributes:
                         raise RuntimeError('Cannot override generic function.  '
-                                           'Try @register({}, object) instead.'.format(name))
+                                           'Try @register("{}", object) instead.'.format(name))
                     generic = value.copy()
                     setattr(cls, name, generic)
+                    patched.add(name)
                     generics.append(generic)
     for name, value in attributes.items():
         if not callable(value) or isinstance(value, singledispatch):
@@ -158,12 +160,12 @@ class SingleDispatch(metaclass=SingleDispatchMeta):
 class SingleDispatchABC(metaclass=SingleDispatchABCMeta):
     pass
 
-
 def register(name, cls):
-    """ Decorator for methods on a sub-class to register an overload on a base-class generic method
-    name is the name of the generic method on the base class
-    cls is the type to register
+    """ Decorator for methods on a sub-class to register an overload on a base-class generic method.
+    :param name: is the name of the generic method on the base class, or the unbound method itself
+    :param cls: is the type to register
     """
+    name = getattr(name, '__name__', name)  # __name__ exists on sd_method courtesy of update_wrapper
     def wrapper(func):
         overloads = getattr(func, 'overloads', [])
         overloads.append((name, cls))
diff --git a/methoddispatch/methoddispatch37.py b/methoddispatch/methoddispatch37.py
index 6c5e80c..06be769 100644
--- a/methoddispatch/methoddispatch37.py
+++ b/methoddispatch/methoddispatch37.py
@@ -84,7 +84,7 @@ class singledispatch(object):
 
     def __get__(self, instance, cls=None):
         if cls is not None and not issubclass(cls, SingleDispatch):
-            raise ValueError('singledispatch can only be used on methods of SingleDispatchMeta types')
+            raise ValueError('singledispatch can only be used on methods of SingleDispatch subclasses')
         wrapper = sd_method(self, instance)
         update_wrapper(wrapper, self.func)
         return wrapper
@@ -124,40 +124,46 @@ class sd_method(object):
             return self.dispatch(args[0].__class__)(self._instance, *args, **kwargs)
 
 
+def _fixup_class_attributes(cls):
+    generics = []
+    attributes = cls.__dict__
+    patched = set()
+    for base in cls.mro()[1:]:
+        if issubclass(base, SingleDispatch):
+            for name, value in base.__dict__.items():
+                if isinstance(value, singledispatch) and name not in patched:
+                    if name in attributes:
+                        raise RuntimeError('Cannot override generic function.  '
+                                           'Try @register("{}", object) instead.'.format(name))
+                    generic = value.copy()
+                    setattr(cls, name, generic)
+                    patched.add(name)
+                    generics.append(generic)
+    for name, value in attributes.items():
+        if not callable(value) or isinstance(value, singledispatch):
+            continue
+        if hasattr(value, 'overloads'):
+            for generic_name, cls in value.overloads:
+                generic = attributes[generic_name]
+                if cls is None:
+                    generic.register(value)
+                else:
+                    generic.register(cls, value)
+        else:  # register over-ridden methods
+            for generic in generics:
+                for cls, f in generic.registry.items():
+                    if name == f.__name__:
+                        generic.register(cls, value)
+                        break
+
+
 class SingleDispatch(object):
     """
     Base or mixin class to enable single dispatch on methods.
     """
     def __init_subclass__(cls, **kwargs):
         super().__init_subclass__(**kwargs)
-        generics = []
-        attributes = cls.__dict__
-        for base in cls.mro()[1:]:
-            if issubclass(base, SingleDispatch):
-                for name, value in base.__dict__.items():
-                    if isinstance(value, singledispatch):
-                        if name in attributes:
-                            raise RuntimeError('Cannot override generic function.  '
-                                               'Try @register("{}", object) instead.'.format(name))
-                        generic = value.copy()
-                        setattr(cls, name, generic)
-                        generics.append(generic)
-        for name, value in attributes.items():
-            if not callable(value) or isinstance(value, singledispatch):
-                continue
-            if hasattr(value, 'overloads'):
-                for generic_name, cls in value.overloads:
-                    generic = attributes[generic_name]
-                    if cls is None:
-                        generic.register(value)
-                    else:
-                        generic.register(cls, value)
-            else:  # register over-ridden methods
-                for generic in generics:
-                    for cls, f in generic.registry.items():
-                        if name == f.__name__:
-                            generic.register(cls, value)
-                            break
+        _fixup_class_attributes(cls)
 
 
 SingleDispatchABC = SingleDispatch  # for backwards compatibility
@@ -165,9 +171,10 @@ SingleDispatchABC = SingleDispatch  # for backwards compatibility
 
 def register(name, cls=None):
     """ Decorator for methods on a sub-class to register an overload on a base-class generic method.
-    :param name: is the name of the generic method on the base class
+    :param name: is the name of the generic method on the base class, or the unbound method itself
     :param cls: is the type to register or may be omitted or None to use the annotated parameter type.
     """
+    name = getattr(name, '__name__', name)  # __name__ exists on sd_method courtesy of update_wrapper
     def wrapper(func):
         overloads = getattr(func, 'overloads', [])
         overloads.append((name, cls))


Test patch:
diff --git a/test.py b/test.py
index 4de209f..66710aa 100644
--- a/test.py
+++ b/test.py
@@ -27,6 +27,15 @@ class SubClass(BaseClass):
     def foo_int(self, bar):
         return 'sub int'
 
+    @register(BaseClass.foo, str)
+    def foo_str(self, bar):
+        return 'str'
+
+class SubSubClass(SubClass):
+    @register('foo', list)
+    def foo_list(self, bar):
+        return 'list'
+
 
 @six.add_metaclass(abc.ABCMeta)
 class IFoo(object):
@@ -64,9 +73,18 @@ class TestMethodDispatch(unittest.TestCase):
 
     def test_sub_class(self):
         s = SubClass()
-        self.assertEqual(s.foo('text'), 'default')
+        self.assertEqual(s.foo([]), 'default')
+        self.assertEqual(s.foo(1), 'sub int')
+        self.assertEqual(s.foo(1.0), 'float')
+        self.assertEqual(s.foo(''), 'str')
+
+    def test_sub_sub_class(self):
+        # this checks that we are using MRO and not just bases.
+        s = SubSubClass()
+        self.assertEqual(s.foo([]), 'list')
         self.assertEqual(s.foo(1), 'sub int')
         self.assertEqual(s.foo(1.0), 'float')
+        self.assertEqual(s.foo(''), 'str')
 
     def test_independence(self):
         b = BaseClass()
@@ -100,7 +118,7 @@ class TestMethodDispatch(unittest.TestCase):
         self.assertTrue(hasattr(SubClass.foo, 'dispatch'))
         self.assertTrue(hasattr(SubClass.foo, 'registry'))
         self.assertIs(SubClass.foo.dispatch(float), SubClass.__dict__['foo_float'])
-        self.assertEqual(set(SubClass.foo.registry.keys()), set([float, object, int]))
+        self.assertEqual(set(SubClass.foo.registry.keys()), set([float, object, int, str]))
 
     def test_instance_extra_attributes(self):
         """ Check that dispatch and registry attributes are accessible """
@@ -108,7 +126,7 @@ class TestMethodDispatch(unittest.TestCase):
         self.assertTrue(hasattr(s.foo, 'dispatch'))
         self.assertTrue(hasattr(s.foo, 'registry'))
         self.assertIs(s.foo.dispatch(float), SubClass.__dict__['foo_float'])
-        self.assertEqual(set(s.foo.registry.keys()), set([float, object, int]))
+        self.assertEqual(set(s.foo.registry.keys()), set([float, object, int, str]))
 
     @unittest.skipIf(six.PY2, 'docs are in python3 syntax')
     def test_docs(self):
@@ -119,10 +137,10 @@ class TestMethodDispatch(unittest.TestCase):
 
     @unittest.skipIf(sys.version_info < (3, 7), 'python < 3.7')
     def test_annotations(self):
-        exec(py_37_tests)
+        exec(annotation_tests)
 
 
-py_37_tests = """
+annotation_tests = """
 def test_annotations(self):
     class AnnClass(BaseClass):
         @register('foo')