122 lines
3.3 KiB
Python
122 lines
3.3 KiB
Python
|
"""Find all objects reachable from a root object."""
|
||
|
|
||
|
from collections.abc import Iterable
|
||
|
import weakref
|
||
|
import types
|
||
|
|
||
|
from typing import List, Dict, Iterator, Tuple, Mapping
|
||
|
from typing_extensions import Final
|
||
|
|
||
|
method_descriptor_type: Final = type(object.__dir__)
|
||
|
method_wrapper_type: Final = type(object().__ne__)
|
||
|
wrapper_descriptor_type: Final = type(object.__ne__)
|
||
|
|
||
|
FUNCTION_TYPES: Final = (
|
||
|
types.BuiltinFunctionType,
|
||
|
types.FunctionType,
|
||
|
types.MethodType,
|
||
|
method_descriptor_type,
|
||
|
wrapper_descriptor_type,
|
||
|
method_wrapper_type,
|
||
|
)
|
||
|
|
||
|
ATTR_BLACKLIST: Final = {
|
||
|
'__doc__',
|
||
|
'__name__',
|
||
|
'__class__',
|
||
|
'__dict__',
|
||
|
}
|
||
|
|
||
|
# Instances of these types can't have references to other objects
|
||
|
ATOMIC_TYPE_BLACKLIST: Final = {
|
||
|
bool,
|
||
|
int,
|
||
|
float,
|
||
|
str,
|
||
|
type(None),
|
||
|
object,
|
||
|
}
|
||
|
|
||
|
# Don't look at most attributes of these types
|
||
|
COLLECTION_TYPE_BLACKLIST: Final = {
|
||
|
list,
|
||
|
set,
|
||
|
dict,
|
||
|
tuple,
|
||
|
}
|
||
|
|
||
|
# Don't return these objects
|
||
|
TYPE_BLACKLIST: Final = {
|
||
|
weakref.ReferenceType,
|
||
|
}
|
||
|
|
||
|
|
||
|
def isproperty(o: object, attr: str) -> bool:
|
||
|
return isinstance(getattr(type(o), attr, None), property)
|
||
|
|
||
|
|
||
|
def get_edge_candidates(o: object) -> Iterator[Tuple[object, object]]:
|
||
|
# use getattr because mypyc expects dict, not mappingproxy
|
||
|
if '__getattribute__' in getattr(type(o), '__dict__'): # noqa
|
||
|
return
|
||
|
if type(o) not in COLLECTION_TYPE_BLACKLIST:
|
||
|
for attr in dir(o):
|
||
|
try:
|
||
|
if attr not in ATTR_BLACKLIST and hasattr(o, attr) and not isproperty(o, attr):
|
||
|
e = getattr(o, attr)
|
||
|
if not type(e) in ATOMIC_TYPE_BLACKLIST:
|
||
|
yield attr, e
|
||
|
except AssertionError:
|
||
|
pass
|
||
|
if isinstance(o, Mapping):
|
||
|
yield from o.items()
|
||
|
elif isinstance(o, Iterable) and not isinstance(o, str):
|
||
|
for i, e in enumerate(o):
|
||
|
yield i, e
|
||
|
|
||
|
|
||
|
def get_edges(o: object) -> Iterator[Tuple[object, object]]:
|
||
|
for s, e in get_edge_candidates(o):
|
||
|
if (isinstance(e, FUNCTION_TYPES)):
|
||
|
# We don't want to collect methods, but do want to collect values
|
||
|
# in closures and self pointers to other objects
|
||
|
|
||
|
if hasattr(e, '__closure__'):
|
||
|
yield (s, '__closure__'), e.__closure__ # type: ignore
|
||
|
if hasattr(e, '__self__'):
|
||
|
se = e.__self__ # type: ignore
|
||
|
if se is not o and se is not type(o) and hasattr(s, '__self__'):
|
||
|
yield s.__self__, se # type: ignore
|
||
|
else:
|
||
|
if not type(e) in TYPE_BLACKLIST:
|
||
|
yield s, e
|
||
|
|
||
|
|
||
|
def get_reachable_graph(root: object) -> Tuple[Dict[int, object],
|
||
|
Dict[int, Tuple[int, object]]]:
|
||
|
parents = {}
|
||
|
seen = {id(root): root}
|
||
|
worklist = [root]
|
||
|
while worklist:
|
||
|
o = worklist.pop()
|
||
|
for s, e in get_edges(o):
|
||
|
if id(e) in seen:
|
||
|
continue
|
||
|
parents[id(e)] = (id(o), s)
|
||
|
seen[id(e)] = e
|
||
|
worklist.append(e)
|
||
|
|
||
|
return seen, parents
|
||
|
|
||
|
|
||
|
def get_path(o: object,
|
||
|
seen: Dict[int, object],
|
||
|
parents: Dict[int, Tuple[int, object]]) -> List[Tuple[object, object]]:
|
||
|
path = []
|
||
|
while id(o) in parents:
|
||
|
pid, attr = parents[id(o)]
|
||
|
o = seen[pid]
|
||
|
path.append((attr, o))
|
||
|
path.reverse()
|
||
|
return path
|