Skip to content

bpo-32380 Fix singledispatch interaction with methods #4987

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from

Conversation

emmatyping
Copy link
Member

@emmatyping emmatyping commented Dec 23, 2017

Singledispatch dispatches on the first argument, making it incompatible with classes. This change fixes that.

https://bugs.python.org/issue32380

Singledispatch used the first argument to dispatch on. This made it
incomatible with methods. This change adds support for methods by
dispatching on the second argument if the passed function is a method.
Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I think this should be implemented more carefully, see comments.

Lib/functools.py Outdated
return dispatch(args[0].__class__)(*args, **kw)
# If our decorated function is in the first arg's attributes,
# we are using a method.
if getattr(args[0], func.__name__, False):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a fragile hack. Some arguments may have the same attribute just by coincidence.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, yes, I wasn't sure if this was the best solution or not. The main issue is that the decorated function is not an instance of types.MethodType when the decorator reaches it. I also looked at frame stack hacks but that would cause conflicts if it was decorating closures. At this point it seems to me a new methodsingledispatch is in order, with dispatching on args[1]. e.g.:

def methodsingledispatch(func):
     wrapped = singledispatch(func)
     def wrapper(*args, **kwargs):
        wrapped.dispatch(args[1].__class__)(*args, **kw)
     wrapper.register = wrapped.register
     update_wrapper(wrapper, func)
     return wrapper

Alternatively, I think allowing the user to specify which arg index for singledispatch to dispatch on would also be a possible solution (and a nice feature to add in general). Then it would be as simple as methodsingledispatch = partial(singledispatch, index=1) or something like that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an API change, so this probably should go through python-ideas/python-dev.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I will post to python-dev.

Lib/functools.py Outdated
@@ -816,8 +815,15 @@ def register(cls, func=None):
dispatch_cache.clear()
return func


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you change the whitespace formatting?

@@ -332,7 +332,7 @@ The :mod:`functools` module defines the following functions:
False

When called, the generic function dispatches on the type of the first
argument::
argument, unless it is used on class::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be more clear. For example, "Note: if it is used with instance or class methods, then self/cls is skipped, and the next argument is used." Also add an example illustrating this.

self.assertEqual(a.t(0), "int")
self.assertEqual(a.t(''), "str")
self.assertEqual(a.t(0.0), "base")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add more tests. At least cover these cases:

  • @classmethod and @staticmethod
  • method with zero args (it should fail gracefully)
  • calling via instance vs calling via class for all of the above

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@classmethod and @staticmethod return descriptors, and singledispatch requires a callable, thus these are not compatible with singledispatch (or would require more significant changes than I'm intending). Generally, you put these around the decorator, but for singledispatch that won't work, as that would break register().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be at least one way to do it. Allowing to use singledispatch only with instance methods looks like an incomplete solution.

@@ -0,0 +1,2 @@
``functools.singledispatch`` now supports decorating and registering with
class methods.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just "methods."

@bedevere-bot
Copy link

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

try:
impl = registry[cls]
impl = dispatch_cache[cls]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the diff being weird, I did not change this. The block and main part of singledispatch was indented.

@@ -0,0 +1,511 @@
:mod:`functools` --- Higher-order functions and operations on callable objects
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't have been included in the commit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I don't know why this isn't ignored by git. I will remove it.

Copy link
Contributor

@ncoghlan ncoghlan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, but unfortunately, I don't believe this particular class-definition-time-only approach is ever going to work right given the semantics of the descriptor protocol.

Instead, something more like the functools.partialmethod will be needed, which defers some key tasks to its __get__ method implementation.

@@ -357,6 +357,23 @@ The :mod:`functools` module defines the following functions:
for the base ``object`` type, which means it is used if no better
implementation is found.

You can also tell ``@singeldispatch`` to dispatch on another argument index
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/singeldispatch/singledispatch/ (typo fix)

@@ -753,76 +753,78 @@ def singledispatch(func):
implementations can be registered using the register() attribute of the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring needs to be updated for the new parameter

@@ -744,7 +744,7 @@ def _find_impl(cls, registry):
match = t
return registry.get(match)

def singledispatch(func):
def singledispatch(func=None, arg=0):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name arg doesn't clear convey that this is the arg index the will be used to dispatch on. arg_index would be clearer.

(Although this will become a moot point if you switch to a separate singledispatchmethod as proposed below)

return "int"
@t.register(str)
def _(self, arg):
return "str"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @ilevkivskyi noted, additional tests are needed to cover the interaction with other method decorators, especially classmethod, staticmethod, abstractmethod, and partialmethod.

I think you will find that adding a parameter to the existing singledispatch function isn't going to be sufficient to solve the problem in the general case (including supporting post-registration of new overloads after the class has been defined). Instead, you'll need something more along the lines of partialmethod which is aware of the descriptor protocol, and uses that to delegate to the underlying descriptor's __get__ method as necessary. While it's a bit long, singledispatchmethod would follow the existing convention for descriptor protocol aware variants of previously function-only APIs.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ncoghlan and @ethanhs, I'd been lurking the cpython PRs and had some time today, so I made a quick gist of a possible descriptor-aware implementation of singlemethoddispatch. It's far from complete, but it does work for instance, abstract and class methods. (I haven't tried static methods or partial methods yet). I thought I'd link to it here in case it can provide you with some direction / inspiration.

Nice to haves (for my implementation, at least), would be a mechanism which wraps up any methods registered by the decorated method in the same decorators in which that method is wrapped; e.g., if Foo.bar is a classmethod, then Foo.bar.register(int, baz) should make baz a class method too. On the other hand, that's complicated to implement and possibly not even the desired behavior, so I'm putting it on the back-burner for now.

Anyway, here's the gist. Heavily inspired by the partialmethod implementation, as you'll see: https://gist.github.com/ianhoffman/e25899e8fca3c1ef8772f331b14031d0

@emmatyping
Copy link
Member Author

I'm afraid I don't have the bandwidth for this at the moment. @ianhoffman if you want to take over be my guest. I'll come back to it another time if not.

@emmatyping emmatyping closed this Jan 31, 2018
@ilevkivskyi
Copy link
Member

It is sad to see this closed, I think it would be a useful addition, please keep me posted if you will resume your work.

@emmatyping
Copy link
Member Author

I want to focus on PEP 561 and a personal project, but I might get back to this after a month or two.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants