63 lines
2 KiB
Python
63 lines
2 KiB
Python
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]
|