Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 34k
gh-89263: Add typing.get_overloads#31716
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
Uh oh!
There was an error while loading. Please reload this page.
Conversation
JelleZijlstra commented Mar 7, 2022 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
AlexWaygood left a comment • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I have a few thoughts.
Overall, I guess I'm a little concerned about "dumping" so many new functions in the global functools namespace. functools is already kind of a hodgepodge of unrelated things, and I worry that this will make the problem worse.
Normally I'd say that having a class with 0 instance methods would be a code smell, but what about something like this?
classVariantRegistry: _registry=defaultdict(list) @staticmethoddef_get_key_for_callable(func): func=getattr(func, "__func__", func) try: returnf"{func.__module__}.{func.__qualname__}"exceptAttributeError: returnNone@classmethoddefregister_variant(func, variant): key=cls._get_key_for_callable(func) ifkeyisnotNone: cls._variant_registry[key].append(variant) @classmethoddefget_variants(cls, func): returncls._registry[cls._get_key_for_callable(func)] @classmethoddefclear_variants(cls, func=None): iffuncisNone: cls._variant_registry.clear() else: cls._variant_registry.pop(cls._get_key_for_callable(func), None)Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
JelleZijlstra commented Mar 8, 2022
Thanks for the review! I see the point about dumping, but I don't think using a class just as a bag of methods is a good solution. This group of functions serves a similar purpose to |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
AlexWaygood left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if a user does something weird (and probably misguided?) like this?
@overloaddefcomplex_func(arg: str) ->int: ... @overloaddefcomplex_func(arg: int) ->str: ... @singledispatchdefcomplex_func(arg: object): raiseNotImplementedError@complex_func.registerdef_(arg: str) ->int: returnint(arg) @complex_func.registerdef_(arg: int) ->str: returnstr(arg)What's the runtime behaviour in this case? Do we care? Do we warn users not to do this in the docs? Do we add a test case to make sure the behaviour is "correct"?
I think we need to account for this possible (mis)usage somehow, even if type checkers would probably balk at it.
JelleZijlstra commented Mar 27, 2022
We simply get variants both from overload and singledispatch. I added a test case based on this example. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
JelleZijlstra commented Apr 9, 2022
Implemented this approach. I now see a 5x performance ratio: |
| impl, overloads=self.set_up_overloads() | ||
| self.assertNotEqual(typing._overload_registry,{}) | ||
| self.assertEqual(list(get_overloads(impl)), overloads) | ||
| clear_overloads() | ||
| self.assertEqual(typing._overload_registry,{}) | ||
| self.assertEqual(get_overloads(impl), []) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment explaining why this sequence is repeated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Also added a test case to make sure that clearing one function's overloads doesn't clear another.
Thinking out loud, clear_overloads() is becoming the most complicated part of this. Perhaps we don't need to support clearing overloads per function, which would simplify it greatly. Use cases I can think of are:
- You want to save some memory after importing all your code -> you call clear_overloads() and get rid of them all.
- You are constantly reloading a module with overloads in it and changing the line numbers. Then maybe you want to clear_overloads() for just that function. But that seems really exotic.
So maybe we should only support clearing the whole overload registry at once. Can you think of other realistic reasons people may want to use clear_overloads()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. Given the reload(module) use case, maybe the only options should be clear all or clear a specific module? The new data structure makes that easy. The question would then be whether the argument should be the (full) module name or the module object? (Or either?) The sequence of events would be something like
typing.clear_overload(mod) importlib.reload(mod)I can't really think of a use case for clearing a specific function. Did you have one in mind originally? Or was it just convenient for testing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for now we should allow only clearing the whole registry. I see the use case for wanting to repeatedly reload modules while wanting to introspect overloads, but it seems unlikely to be common. We can always add an argument later to clear overloads by module if someone asks for it, but once we add it we're stuck with it.
JelleZijlstra commented Apr 14, 2022
@gvanrossum@AlexWaygood any further feedback? The last thing to resolve is whether |
AlexWaygood left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty good now! Just a few thoughts:
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| *func*. *func* is the function object for the implementation of the | ||
| overloaded function. For example, given the definition of ``process`` in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sentence sounds quite strange to me:
*func* is the function object for the implementation of the overloaded function. But I'm not sure I have a better suggestion off the top of my head ://
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, unfortunately the overload docs don't have a clear term for the implementation function either.
gvanrossum commented Apr 14, 2022
I'll wait until @AlexWaygood approves, then I'll do one more pass if you want me to. |
| the documentation for :func:`@overload <overload>`, | ||
| ``get_overloads(process)`` will return a sequence of three function objects | ||
| for the three defined overloads. If called on a function with no overloads, | ||
| ``get_overloads`` returns an empty sequence. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ``get_overloads`` returns an empty sequence. | |
| return an empty sequence. |
Tiny nit -- this paragraph starts off using the imperative mood
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the previous sentence is in the indicative, so I think this is fine?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, it's not really a big deal either way :)
AlexWaygood left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hooray!
JelleZijlstra commented Apr 16, 2022
@gvanrossum would you like to take another look or should I merge this? |
gvanrossum commented Apr 16, 2022
Go for it! |
Credit to Spencer Brown for the idea to use overloads' firstlineno to decide whether to clear the registry.
#89263
https://bugs.python.org/issue45100