Skip to content
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

gh-89263: Add typing.get_overloads #31716

Merged
merged 37 commits into from
Apr 16, 2022
Merged

Conversation

JelleZijlstra
Copy link
Member

@JelleZijlstra JelleZijlstra commented Mar 7, 2022

Credit to Spencer Brown for the idea to use overloads' firstlineno to decide whether to clear the registry.

#89263

https://bugs.python.org/issue45100

Copy link
Member

@AlexWaygood AlexWaygood 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 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?

class VariantRegistry:
    _registry = defaultdict(list)

    @staticmethod
    def _get_key_for_callable(func):
        func = getattr(func, "__func__", func)
        try:
            return f"{func.__module__}.{func.__qualname__}"
        except AttributeError:
            return None

    @classmethod
    def register_variant(func, variant):
        key = cls._get_key_for_callable(func)
        if key is not None:
            cls._variant_registry[key].append(variant)

    @classmethod
    def get_variants(cls, func):
        return cls._registry[cls._get_key_for_callable(func)]

    @classmethod
    def clear_variants(cls, func=None):
        if func is None:
           cls._variant_registry.clear()
        else:
            cls._variant_registry.pop(cls._get_key_for_callable(func), None)

Lib/functools.py Outdated Show resolved Hide resolved
Lib/functools.py Outdated Show resolved Hide resolved
Lib/functools.py Outdated Show resolved Hide resolved
@JelleZijlstra
Copy link
Member Author

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 update_wrapper and wraps (enabling introspection for advanced usage of functions) and functools now feels like a good home for them.

Lib/test/test_typing.py Outdated Show resolved Hide resolved
Copy link
Member

@AlexWaygood AlexWaygood left a 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?

@overload
def complex_func(arg: str) -> int: ...

@overload
def complex_func(arg: int) -> str: ...

@singledispatch
def complex_func(arg: object):
    raise NotImplementedError

@complex_func.register
def _(arg: str) -> int: 
    return int(arg)

@complex_func.register
def _(arg: int) -> str:
    return str(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 JelleZijlstra marked this pull request as draft March 9, 2022 04:56
@JelleZijlstra
Copy link
Member Author

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.

We simply get variants both from overload and singledispatch. I added a test case based on this example.

Doc/library/typing.rst Outdated Show resolved Hide resolved
@JelleZijlstra
Copy link
Member Author

Implemented this approach. I now see a 5x performance ratio:

In [9]: %timeit overload(f)
223 ns ± 1.85 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

In [10]: %timeit old_overload(f)
44.7 ns ± 0.74 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

In [11]: 223 / 44.7
Out[11]: 4.988814317673378

@JelleZijlstra JelleZijlstra removed the request for review from rhettinger April 9, 2022 01:55
Comment on lines 3922 to 3928
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), [])
Copy link
Member

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?

Copy link
Member Author

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()?

Copy link
Member

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?

Copy link
Member Author

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.

@AlexWaygood AlexWaygood self-requested a review April 9, 2022 04:34
@AlexWaygood AlexWaygood added topic-typing type-feature A feature request or enhancement labels Apr 13, 2022
@JelleZijlstra JelleZijlstra changed the title bpo-45100: Add typing.get_overloads gh-89263: Add typing.get_overloads Apr 14, 2022
@JelleZijlstra
Copy link
Member Author

@gvanrossum @AlexWaygood any further feedback? The last thing to resolve is whether clear_overloads should take arguments (#31716 (comment)).

Copy link
Member

@AlexWaygood AlexWaygood left a 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:

Doc/library/typing.rst Show resolved Hide resolved
Lib/typing.py Outdated Show resolved Hide resolved
Lib/typing.py Show resolved Hide resolved
Lib/test/test_typing.py Show resolved Hide resolved
Doc/library/typing.rst Outdated Show resolved Hide resolved
Doc/library/typing.rst Show resolved Hide resolved
Comment on lines +2418 to +2419
*func*. *func* is the function object for the implementation of the
overloaded function. For example, given the definition of ``process`` in
Copy link
Member

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 ://

Copy link
Member Author

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
Copy link
Member

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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
``get_overloads`` returns an empty sequence.
return an empty sequence.

Tiny nit -- this paragraph starts off using the imperative mood

Copy link
Member Author

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?

Copy link
Member

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 :)

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Hooray!

@AlexWaygood AlexWaygood added the stdlib Python modules in the Lib dir label Apr 14, 2022
@JelleZijlstra
Copy link
Member Author

@gvanrossum would you like to take another look or should I merge this?

@gvanrossum
Copy link
Member

Go for it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stdlib Python modules in the Lib dir topic-typing type-feature A feature request or enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants