1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
|
from collections import deque
from collections.abc import Iterator
from os.path import abspath
from types import FunctionType, ModuleType
from typing import Any, Dict, Optional, Protocol, Tuple, Type, Union, cast
from module import origin # type: ignore
FunctionContainerType = Union[
type, property, classmethod, staticmethod, Tuple, ModuleType
]
ContainerKey = Union[str, int, Type[staticmethod], Type[classmethod]]
CONTAINER_TYPES = (type, property, classmethod, staticmethod)
def set_cell_contents(cell, contents): # type: ignore[misc]
cell.cell_contents = contents
class FullyNamed(Protocol):
"""A fully named object."""
__name__ = None # type: Optional[str]
__fullname__ = None # type: Optional[str]
class FullyNamedFunction(FullyNamed):
"""A fully named function object."""
def __call__(self, *args, **kwargs):
pass
class ContainerIterator(Iterator, FullyNamedFunction):
"""Wrapper around different types of function containers.
A container comes with an origin, i.e. a parent container and a position
within it in the form of a key.
"""
def __init__(
self,
container, # type: FunctionContainerType
origin=None, # type: Optional[Union[Tuple[ContainerIterator, ContainerKey], Tuple[FullyNamedFunction, str]]]
):
# type: (...) -> None
if isinstance(container, (type, ModuleType)):
self._iter = iter(container.__dict__.items())
self.__name__ = container.__name__
elif isinstance(container, tuple):
self._iter = iter(enumerate(_.cell_contents for _ in container)) # type: ignore[arg-type]
self.__name__ = "<locals>"
elif isinstance(container, property):
self._iter = iter(
(m, getattr(container, a))
for m, a in {
("getter", "fget"),
("setter", "fset"),
("deleter", "fdel"),
}
)
assert container.fget is not None
self.__name__ = container.fget.__name__
elif isinstance(container, (classmethod, staticmethod)):
self._iter = iter([(type(container), container.__func__)]) # type: ignore[list-item]
self.__name__ = None
else:
raise TypeError("Unsupported container type: %s", type(container))
self._container = container
if origin is not None and origin[0].__fullname__ is not None:
origin_fullname = origin[0].__fullname__
self.__fullname__ = (
".".join((origin_fullname, self.__name__))
if self.__name__
else origin_fullname
)
else:
self.__fullname__ = self.__name__
def __iter__(self):
# type: () -> Iterator[Tuple[ContainerKey, Any]]
return self._iter
def __next__(self):
# type: () -> Tuple[ContainerKey, Any]
return next(self._iter)
next = __next__
def _collect_functions(module):
# type: (ModuleType) -> Dict[str, FullyNamedFunction]
"""Collect functions from a given module."""
assert isinstance(module, ModuleType)
path = origin(module)
containers = deque([ContainerIterator(module)])
functions = {}
seen_containers = set()
seen_functions = set()
while containers:
c = containers.pop()
if id(c._container) in seen_containers:
continue
seen_containers.add(id(c._container))
for k, o in c:
code = getattr(o, "__code__", None) if isinstance(o, FunctionType) else None
if code is not None and abspath(code.co_filename) == path:
if o not in seen_functions:
seen_functions.add(o)
o = cast(FullyNamedFunction, o)
o.__fullname__ = (
".".join((c.__fullname__, o.__name__))
if c.__fullname__
else o.__name__
)
for name in (k, o.__name__) if isinstance(k, str) else (o.__name__,):
fullname = (
".".join((c.__fullname__, name)) if c.__fullname__ else name
)
functions[fullname] = o
try:
if o.__closure__:
containers.append(
ContainerIterator(o.__closure__, origin=(o, "<locals>"))
)
except AttributeError:
pass
elif isinstance(o, CONTAINER_TYPES):
if isinstance(o, property) and not isinstance(o.fget, FunctionType):
continue
containers.append(ContainerIterator(o, origin=(c, k)))
return functions
class FunctionDiscovery(dict):
"""Discover all function objects in a module."""
def __init__(self, module):
# type: (ModuleType) -> None
super(FunctionDiscovery, self).__init__()
self._module = module
functions = _collect_functions(module)
seen_functions = set()
for fname, function in functions.items():
self[fname] = function
seen_functions.add(function)
|