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
|
import ast
import inspect
import textwrap
from typing import AbstractSet, Callable, Collection, Dict, Set
Dependencies = AbstractSet[str]
class DependencyFinder(ast.NodeVisitor):
def __init__(self, param: str):
self.param = param
self.dependencies: Set[str] = set()
def visit_Attribute(self, node):
self.generic_visit(node)
if isinstance(node.value, ast.Name) and node.value.id == self.param:
self.dependencies.add(node.attr)
# TODO Add warning in case of function call with self in parameter
# or better, follow the call, but it would be too hard (local import, etc.)
def first_parameter(func: Callable) -> str:
try:
return next(iter(inspect.signature(func).parameters))
except StopIteration:
raise TypeError("Cannot compute dependencies if no parameter")
def find_dependencies(func: Callable) -> Dependencies:
try:
finder = DependencyFinder(first_parameter(func))
finder.visit(ast.parse(textwrap.dedent(inspect.getsource(func))))
except ValueError:
return set()
return finder.dependencies
cache: Dict[Callable, Dependencies] = {}
def find_all_dependencies(
cls: type, func: Callable, rec_guard: Collection[str] = ()
) -> Dependencies:
"""Dependencies contains class variables (because they can be "fake" ones as in
dataclasses)"""
if func not in cache:
dependencies = set(find_dependencies(func))
for attr in list(dependencies):
if not hasattr(cls, attr):
continue
member = getattr(cls, attr)
if isinstance(member, property):
member = member.fget
if callable(member):
dependencies.remove(attr)
if member in rec_guard:
continue
rec_deps = find_all_dependencies(cls, member, {*rec_guard, member})
dependencies.update(rec_deps)
cache[func] = dependencies
return cache[func]
|