Visitar URL original
gh-89263: Add typing.get_overloads by JelleZijlstra · Pull Request #31716 · python/cpython · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2ee377d
initial
JelleZijlstra Mar 6, 2022
831b565
Implementation, tests, and docs
JelleZijlstra Mar 7, 2022
f03f8a9
fix versionadded
JelleZijlstra Mar 7, 2022
404668a
Merge branch 'main' into funcregistry
JelleZijlstra Mar 8, 2022
7a5b0d1
make get_key_for_callable private
JelleZijlstra Mar 8, 2022
6998255
doc updates; remove unnecessary try-except
JelleZijlstra Mar 9, 2022
26bb908
Merge remote-tracking branch 'upstream/main' into funcregistry
JelleZijlstra Mar 27, 2022
f52b757
rename method
JelleZijlstra Mar 27, 2022
fc6a925
Don't store singledispatch in the registry
JelleZijlstra Mar 27, 2022
b524244
more tests
JelleZijlstra Mar 27, 2022
e95558e
and another
JelleZijlstra Mar 27, 2022
31fd72d
fix line length in new tests
JelleZijlstra Mar 27, 2022
7041ad3
Update Doc/library/functools.rst
JelleZijlstra Mar 27, 2022
e26b0db
Update Doc/library/typing.rst
JelleZijlstra Mar 27, 2022
1bf89fb
only for overload
JelleZijlstra Apr 2, 2022
83ac432
Merge remote-tracking branch 'upstream/main' into funcregistry
JelleZijlstra Apr 2, 2022
dfdbdc7
fix tests
JelleZijlstra Apr 2, 2022
e16c8d0
undo stray changes, fix NEWS entry
JelleZijlstra Apr 2, 2022
b3d2227
remove extra import
JelleZijlstra Apr 2, 2022
9727eee
Apply suggestions from code review
JelleZijlstra Apr 2, 2022
2e374b8
Apply suggestions from code review
JelleZijlstra Apr 3, 2022
ff03b12
Guido's feedback
JelleZijlstra Apr 3, 2022
17f0710
Optimizations suggested by Guido and Alex
JelleZijlstra Apr 3, 2022
2346970
inline _get_firstlineno, store outer objects for classmethod/staticme…
JelleZijlstra Apr 3, 2022
f2053a0
use defaultdict
JelleZijlstra Apr 3, 2022
b6131ad
another optimization
JelleZijlstra Apr 4, 2022
506bd66
Update Lib/typing.py
JelleZijlstra Apr 7, 2022
e9a2100
Merge remote-tracking branch 'upstream/main' into funcregistry
JelleZijlstra Apr 8, 2022
2b1a5cc
Merge remote-tracking branch 'upstream/main' into funcregistry
JelleZijlstra Apr 9, 2022
103bfd4
Simpler implementation (thanks Guido)
JelleZijlstra Apr 9, 2022
d453f7f
More comments and tests
JelleZijlstra Apr 9, 2022
450afeb
Merge remote-tracking branch 'upstream/main' into funcregistry
JelleZijlstra Apr 14, 2022
ea62287
simplify clear_overloads
JelleZijlstra Apr 14, 2022
905253c
use partial
JelleZijlstra Apr 14, 2022
debbf8a
add test
JelleZijlstra Apr 14, 2022
754c134
docs changes (thanks Alex)
JelleZijlstra Apr 14, 2022
1ad8224
Merge remote-tracking branch 'upstream/main' into funcregistry
JelleZijlstra Apr 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Simpler implementation (thanks Guido)
  • Loading branch information
JelleZijlstra committed Apr 9, 2022
commit 103bfd4f3eafdfac9b03960e3a425836f769443b
37 changes: 13 additions & 24 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import contextlib
import collections
from collections import defaultdict
from functools import lru_cache
import inspect
import pickle
import re
import sys
import warnings
from unittest import TestCase, main, skipUnless, skip
from unittest.mock import patch
from copy import copy, deepcopy

from typing import Any, NoReturn, Never, assert_never
Expand Down Expand Up @@ -3903,17 +3905,26 @@ def blah():

return blah, [overload1, overload2]

# Make sure we don't clear the global overload registry
@patch("typing._overload_registry",
defaultdict(lambda: defaultdict(dict)))
def test_overload_registry(self):
Comment thread
JelleZijlstra marked this conversation as resolved.
Resolved
impl, overloads = self.set_up_overloads()
self.assertEqual(len(typing._overload_registry), 0)

impl, overloads = self.set_up_overloads()
self.assertNotEqual(typing._overload_registry, {})
self.assertEqual(list(get_overloads(impl)), overloads)

clear_overloads(impl)
self.assertEqual(typing._overload_registry, {})
self.assertEqual(get_overloads(impl), [])

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), [])
Comment on lines +3922 to +3969

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


def test_overload_registry_repeated(self):
Expand All @@ -3922,28 +3933,6 @@ def test_overload_registry_repeated(self):

self.assertEqual(list(get_overloads(impl)), overloads)

def test_get_key_for_callable(self):
self.assertEqual(
typing._get_key_for_callable(len),
("builtins", "len"),
)
self.assertEqual(
typing._get_key_for_callable(cached_func),
(__name__, "cached_func"),
)
self.assertEqual(
typing._get_key_for_callable(MethodHolder.clsmethod),
(__name__, "MethodHolder.clsmethod"),
)
self.assertEqual(
typing._get_key_for_callable(MethodHolder.stmethod),
(__name__, "MethodHolder.stmethod"),
)
self.assertEqual(
typing._get_key_for_callable(MethodHolder.method),
(__name__, "MethodHolder.method"),
)


# Definitions needed for features introduced in Python 3.6

Expand Down
109 changes: 22 additions & 87 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from abc import abstractmethod, ABCMeta
import collections
from collections import defaultdict
import collections.abc
import contextlib
import functools
Expand Down Expand Up @@ -2431,6 +2432,10 @@ def _overload_dummy(*args, **kwds):
"by an implementation that is not @overload-ed.")


# {module: {qualname: {firstlineno: func}}}
_overload_registry = defaultdict(lambda: defaultdict(dict))
Comment thread
JelleZijlstra marked this conversation as resolved.
Resolved


def overload(func):
"""Decorator for overloaded functions/methods.

Expand Down Expand Up @@ -2460,110 +2465,40 @@ def utf8(value):
The overloads for a function can be retrieved at runtime using the
get_overloads() function.
"""
# Inline version of _get_key_for_callable
# classmethod and staticmethod
inner_func = getattr(func, "__func__", func)
f = getattr(func, "__func__", func)
try:
key = (inner_func.__module__, inner_func.__qualname__)
_overload_registry[f.__module__][f.__qualname__][f.__code__.co_firstlineno] = func
except AttributeError:
# Not a normal function; ignore.
pass
else:
# We're inlining get_overloads() here to avoid computing the key twice.
existing = _overload_registry[key]
if existing and len(existing) > 20:
# If we are registering an overload with a lineno below or equal to that of the
# most recent existing overload, we're probably re-creating overloads for a
# function that already exists. This can happen if the module is being reloaded
# or if the overloads are being created in a nested function.
# In that case, we clear the existing overloads
# to avoid leaking memory. But we do this only if there are already a lot of
# overloads, so we don't have to figure out the linenos in the common case
# where there are only a few overloads.
try:
firstlineno = inner_func.__code__.co_firstlineno
except AttributeError:
pass
else:
existing_func = existing[-1]
# classmethod and staticmethod
existing_func = getattr(existing_func, "__func__", existing_func)
try:
existing_lineno = existing_func.__code__.co_firstlineno
except AttributeError:
pass
else:
if firstlineno <= existing_lineno:
existing.clear()

existing.append(func)
return _overload_dummy


# {key: [overload]}
_overload_registry = collections.defaultdict(list)


def get_overloads(func):
"""Return all defined overloads for *func* as a sequence."""
key = _get_key_for_callable(func)
overloads = _overload_registry[key]

# We clear out overloads that have higher linenos than a later
# overload, because they're probably the result of a recreation of
# the function (see the comments in @overload). But we have to do it
# here too because @overload only clears out overloads when there are
# many.
final_overloads = []
last_lineno = -1
should_clear = False
for overloaded_func in overloads:
lineno = _get_firstlineno(overloaded_func)
if lineno is not None:
# If the same function is registered multiple times on the same line,
# we skip the duplicates.
if lineno <= last_lineno:
final_overloads.clear()
should_clear = True
last_lineno = lineno
final_overloads.append(overloaded_func)
if should_clear:
_overload_registry[key] = final_overloads
return final_overloads


def _get_firstlineno(func):
# staticmethod, classmethod
func = getattr(func, "__func__", func)
if not hasattr(func, '__code__'):
return None
return func.__code__.co_firstlineno

# classmethod and staticmethod
f = getattr(func, "__func__", func)
if f.__module__ not in _overload_registry:
return []
mod_dict = _overload_registry[f.__module__]
if f.__qualname__ not in mod_dict:
return []
return list(mod_dict[f.__qualname__].values())


def clear_overloads(func=None):
"""Clear all overloads for the given function (or all functions)."""
if func is None:
_overload_registry.clear()
else:
key = _get_key_for_callable(func)
_overload_registry.pop(key, None)


def _get_key_for_callable(func):
"""Return a key for the given callable.

This is used as a key in the overload registry.

If no key can be created (because the object is not of a supported type), raise
AttributeError.
"""
# classmethod and staticmethod
func = getattr(func, "__func__", func)
try:
return (func.__module__, func.__qualname__)
except AttributeError:
raise TypeError(f"Cannot create key for {func!r}") from None
f = getattr(func, "__func__", func)
if f.__module__ not in _overload_registry:
return
mod_dict = _overload_registry[f.__module__]
mod_dict.pop(f.__qualname__, None)
if not mod_dict:
del _overload_registry[f.__module__]


def final(f):
Expand Down