deduperreload.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. from __future__ import annotations
  2. import ast
  3. import builtins
  4. import contextlib
  5. import itertools
  6. import logging
  7. import os
  8. import pickle
  9. import platform
  10. import sys
  11. import textwrap
  12. from types import ModuleType
  13. from typing import TYPE_CHECKING, Any, NamedTuple, cast
  14. from collections.abc import Generator, Iterable
  15. from IPython.extensions.deduperreload.deduperreload_patching import (
  16. DeduperReloaderPatchingMixin,
  17. )
  18. if TYPE_CHECKING:
  19. TDefinitionAst = (
  20. ast.FunctionDef
  21. | ast.AsyncFunctionDef
  22. | ast.Import
  23. | ast.ImportFrom
  24. | ast.Assign
  25. | ast.AnnAssign
  26. )
  27. def get_module_file_name(module: ModuleType | str) -> str | None:
  28. """Returns the module's file path, or the empty string if it's inaccessible"""
  29. if (mod := sys.modules.get(module) if isinstance(module, str) else module) is None:
  30. return ""
  31. return getattr(mod, "__file__", "") or ""
  32. def compare_ast(node1: ast.AST | list[ast.AST], node2: ast.AST | list[ast.AST]) -> bool:
  33. """Checks if node1 and node2 have identical AST structure/values, apart from some attributes"""
  34. if type(node1) is not type(node2):
  35. return False
  36. if isinstance(node1, ast.AST):
  37. for k, v in node1.__dict__.items():
  38. if k in (
  39. "lineno",
  40. "end_lineno",
  41. "col_offset",
  42. "end_col_offset",
  43. "ctx",
  44. "parent",
  45. ):
  46. continue
  47. if not hasattr(node2, k) or not compare_ast(v, getattr(node2, k)):
  48. return False
  49. return True
  50. elif isinstance(node1, list) and isinstance( # type:ignore [redundant-expr]
  51. node2, list
  52. ):
  53. return len(node1) == len(node2) and all(
  54. compare_ast(n1, n2) for n1, n2 in zip(node1, node2)
  55. )
  56. else:
  57. return node1 == node2
  58. class DependencyNode(NamedTuple):
  59. """
  60. Each node represents a function.
  61. qualified_name: string which represents the namespace/name of the function
  62. abstract_syntax_tree: subtree of the overall module which corresponds to this function
  63. qualified_name is of the structure: (namespace1, namespace2, ..., name)
  64. For example, foo() in the following would be represented as (A, B, foo):
  65. class A:
  66. class B:
  67. def foo():
  68. pass
  69. """
  70. qualified_name: tuple[str, ...]
  71. abstract_syntax_tree: ast.AST
  72. class GatherResult(NamedTuple):
  73. import_defs: list[tuple[tuple[str, ...], ast.Import | ast.ImportFrom]] = []
  74. assign_defs: list[tuple[tuple[str, ...], ast.Assign | ast.AnnAssign]] = []
  75. function_defs: list[
  76. tuple[tuple[str, ...], ast.FunctionDef | ast.AsyncFunctionDef]
  77. ] = []
  78. classes: dict[str, ast.ClassDef] = {}
  79. unfixable: list[ast.AST] = []
  80. @classmethod
  81. def create(cls) -> GatherResult:
  82. return cls([], [], [], {}, [])
  83. def all_defs(self) -> Iterable[tuple[tuple[str, ...], TDefinitionAst]]:
  84. return itertools.chain(self.import_defs, self.assign_defs, self.function_defs)
  85. def inplace_merge(self, other: GatherResult) -> None:
  86. self.import_defs.extend(other.import_defs)
  87. self.assign_defs.extend(other.assign_defs)
  88. self.function_defs.extend(other.function_defs)
  89. self.classes.update(other.classes)
  90. self.unfixable.extend(other.unfixable)
  91. class ConstexprDetector(ast.NodeVisitor):
  92. def __init__(self) -> None:
  93. self.is_constexpr = True
  94. self._allow_builtins_exceptions = True
  95. @contextlib.contextmanager
  96. def disallow_builtins_exceptions(self) -> Generator[None, None, None]:
  97. prev_allow = self._allow_builtins_exceptions
  98. self._allow_builtins_exceptions = False
  99. try:
  100. yield
  101. finally:
  102. self._allow_builtins_exceptions = prev_allow
  103. def visit_Attribute(self, node: ast.Attribute) -> None:
  104. with self.disallow_builtins_exceptions():
  105. self.visit(node.value)
  106. def visit_Name(self, node: ast.Name) -> None:
  107. if self._allow_builtins_exceptions and hasattr(builtins, node.id):
  108. return
  109. self.is_constexpr = False
  110. def visit(self, node: ast.AST) -> None:
  111. if not self.is_constexpr:
  112. # can short-circuit if we've already detected that it's not a constexpr
  113. return
  114. super().visit(node)
  115. def __call__(self, node: ast.AST) -> bool:
  116. self.is_constexpr = True
  117. self.visit(node)
  118. return self.is_constexpr
  119. class AutoreloadTree:
  120. """
  121. Recursive data structure to keep track of reloadable functions/methods. Each object corresponds to a specific scope level.
  122. children: classes inside given scope, maps class name to autoreload tree for that class's scope
  123. funcs_to_autoreload: list of function names that can be autoreloaded in given scope.
  124. new_nested_classes: Classes getting added in new autoreload cycle
  125. """
  126. def __init__(self) -> None:
  127. self.children: dict[str, AutoreloadTree] = {}
  128. self.defs_to_reload: list[tuple[tuple[str, ...], ast.AST]] = []
  129. self.defs_to_delete: set[str] = set()
  130. self.new_nested_classes: dict[str, ast.AST] = {}
  131. def traverse_prefixes(self, prefixes: list[str]) -> AutoreloadTree:
  132. """
  133. Return ref to the AutoreloadTree at the namespace specified by prefixes
  134. """
  135. cur = self
  136. for prefix in prefixes:
  137. if prefix not in cur.children:
  138. cur.children[prefix] = AutoreloadTree()
  139. cur = cur.children[prefix]
  140. return cur
  141. class DeduperReloader(DeduperReloaderPatchingMixin):
  142. """
  143. This version of autoreload detects when we can leverage targeted recompilation of a subset of a module and patching
  144. existing function/method objects to reflect these changes.
  145. Detects what functions/methods can be reloaded by recursively comparing the old/new AST of module-level classes,
  146. module-level classes' methods, recursing through nested classes' methods. If other changes are made, original
  147. autoreload algorithm is called directly.
  148. """
  149. def __init__(self) -> None:
  150. self._to_autoreload: AutoreloadTree = AutoreloadTree()
  151. self.source_by_modname: dict[str, str] = {}
  152. self.dependency_graph: dict[tuple[str, ...], list[DependencyNode]] = {}
  153. self._enabled = True
  154. @property
  155. def enabled(self) -> bool:
  156. return self._enabled and platform.python_implementation() == "CPython"
  157. @enabled.setter
  158. def enabled(self, value: bool) -> None:
  159. self._enabled = value
  160. def update_sources(self) -> None:
  161. """
  162. Update dictionary source_by_modname with current modules' source codes.
  163. """
  164. if not self.enabled:
  165. return
  166. for new_modname in sys.modules.keys() - self.source_by_modname.keys():
  167. new_module = sys.modules[new_modname]
  168. if (
  169. (fname := get_module_file_name(new_module)) is None
  170. or "site-packages" in fname
  171. or "dist-packages" in fname
  172. or not os.access(fname, os.R_OK)
  173. ):
  174. self.source_by_modname[new_modname] = ""
  175. continue
  176. # Skip binary files (e.g., .so, .pyd, .pyo)
  177. if not fname.endswith(".py"):
  178. self.source_by_modname[new_modname] = ""
  179. continue
  180. try:
  181. with open(fname, "r", encoding="utf8") as f:
  182. self.source_by_modname[new_modname] = f.read()
  183. except Exception as e:
  184. logger = logging.getLogger("autoreload")
  185. logger.exception(
  186. f"Failed to read module file '{fname}' for module '{new_modname}': {type(e).__name__}"
  187. )
  188. self.source_by_modname[new_modname] = ""
  189. constexpr_detector = ConstexprDetector()
  190. @staticmethod
  191. def is_enum_subclass(node: ast.Module | ast.ClassDef) -> bool:
  192. if isinstance(node, ast.Module):
  193. return False
  194. for base in node.bases:
  195. if isinstance(base, ast.Name) and base.id == "Enum":
  196. return True
  197. elif (
  198. isinstance(base, ast.Attribute)
  199. and base.attr == "Enum"
  200. and isinstance(base.value, ast.Name)
  201. and base.value.id == "enum"
  202. ):
  203. return True
  204. return False
  205. @classmethod
  206. def is_constexpr_assign(
  207. cls, node: ast.AST, parent_node: ast.Module | ast.ClassDef
  208. ) -> bool:
  209. if not isinstance(node, (ast.Assign, ast.AnnAssign)) or node.value is None:
  210. return False
  211. if cls.is_enum_subclass(parent_node):
  212. return False
  213. for target in node.targets if isinstance(node, ast.Assign) else [node.target]:
  214. if not isinstance(target, ast.Name):
  215. return False
  216. return cls.constexpr_detector(node.value)
  217. @classmethod
  218. def _gather_children(
  219. cls, body: list[ast.stmt], parent_node: ast.Module | ast.ClassDef
  220. ) -> GatherResult:
  221. """
  222. Given list of ast elements, return:
  223. 1. dict mapping function names to their ASTs.
  224. 2. dict mapping class names to their ASTs.
  225. 3. list of any other ASTs.
  226. """
  227. result = GatherResult.create()
  228. for ast_node in body:
  229. ast_elt: ast.expr | ast.stmt = ast_node
  230. while isinstance(ast_elt, ast.Expr):
  231. ast_elt = ast_elt.value
  232. if isinstance(ast_elt, (ast.FunctionDef, ast.AsyncFunctionDef)):
  233. result.function_defs.append(((ast_elt.name,), ast_elt))
  234. elif isinstance(ast_elt, (ast.Import, ast.ImportFrom)):
  235. result.import_defs.append(
  236. (tuple(name.asname or name.name for name in ast_elt.names), ast_elt)
  237. )
  238. elif isinstance(ast_elt, ast.ClassDef):
  239. result.classes[ast_elt.name] = ast_elt
  240. elif isinstance(ast_elt, ast.If):
  241. result.unfixable.append(ast_elt.test)
  242. result.inplace_merge(cls._gather_children(ast_elt.body, parent_node))
  243. result.inplace_merge(cls._gather_children(ast_elt.orelse, parent_node))
  244. elif isinstance(ast_elt, (ast.AsyncWith, ast.With)):
  245. result.unfixable.extend(ast_elt.items)
  246. result.inplace_merge(cls._gather_children(ast_elt.body, parent_node))
  247. elif isinstance(ast_elt, ast.Try):
  248. result.inplace_merge(cls._gather_children(ast_elt.body, parent_node))
  249. result.inplace_merge(cls._gather_children(ast_elt.orelse, parent_node))
  250. result.inplace_merge(
  251. cls._gather_children(ast_elt.finalbody, parent_node)
  252. )
  253. for handler in ast_elt.handlers:
  254. if handler.type is not None:
  255. result.unfixable.append(handler.type)
  256. result.inplace_merge(
  257. cls._gather_children(handler.body, parent_node)
  258. )
  259. elif not isinstance(ast_elt, (ast.Constant, ast.Pass)):
  260. if cls.is_constexpr_assign(ast_elt, parent_node):
  261. assert isinstance(ast_elt, (ast.Assign, ast.AnnAssign))
  262. targets = (
  263. ast_elt.targets
  264. if isinstance(ast_elt, ast.Assign)
  265. else [ast_elt.target]
  266. )
  267. result.assign_defs.append(
  268. (
  269. tuple(cast(ast.Name, target).id for target in targets),
  270. ast_elt,
  271. )
  272. )
  273. else:
  274. result.unfixable.append(ast_elt)
  275. return result
  276. def detect_autoreload(
  277. self,
  278. old_node: ast.Module | ast.ClassDef,
  279. new_node: ast.Module | ast.ClassDef,
  280. prefixes: list[str] | None = None,
  281. ) -> bool:
  282. """
  283. Returns
  284. -------
  285. `True` if we can run our targeted autoreload algorithm safely.
  286. `False` if we should instead use IPython's original autoreload implementation.
  287. """
  288. if not self.enabled:
  289. return False
  290. prefixes = prefixes or []
  291. old_result = self._gather_children(old_node.body, old_node)
  292. new_result = self._gather_children(new_node.body, new_node)
  293. old_defs_by_name: dict[str, ast.AST] = {
  294. name: ast_def for names, ast_def in old_result.all_defs() for name in names
  295. }
  296. new_defs_by_name: dict[str, ast.AST] = {
  297. name: ast_def for names, ast_def in new_result.all_defs() for name in names
  298. }
  299. if not compare_ast(old_result.unfixable, new_result.unfixable):
  300. return False
  301. cur = self._to_autoreload.traverse_prefixes(prefixes)
  302. for names, new_ast_def in new_result.all_defs():
  303. names_to_reload = []
  304. for name in names:
  305. if new_defs_by_name[name] is not new_ast_def:
  306. continue
  307. if name not in old_defs_by_name or not compare_ast(
  308. new_ast_def, old_defs_by_name[name]
  309. ):
  310. names_to_reload.append(name)
  311. if names_to_reload:
  312. cur.defs_to_reload.append((tuple(names), new_ast_def))
  313. cur.defs_to_delete |= set(old_defs_by_name.keys()) - set(
  314. new_defs_by_name.keys()
  315. )
  316. for name, new_ast_def_class in new_result.classes.items():
  317. if name not in old_result.classes:
  318. cur.new_nested_classes[name] = new_ast_def_class
  319. elif not compare_ast(
  320. new_ast_def_class, old_result.classes[name]
  321. ) and not self.detect_autoreload(
  322. old_result.classes[name], new_ast_def_class, prefixes + [name]
  323. ):
  324. return False
  325. return True
  326. def _check_dependents(self) -> bool:
  327. """
  328. If a decorator function is modified, we should similarly reload the functions which are decorated by this
  329. decorator. Iterate through the Dependency Graph to find such cases in the given AutoreloadTree.
  330. """
  331. for node in self._check_dependents_inner():
  332. self._add_node_to_autoreload_tree(node)
  333. return True
  334. def _add_node_to_autoreload_tree(self, node: DependencyNode) -> None:
  335. """
  336. Given a node of the dependency graph, add decorator dependencies to the autoreload tree.
  337. """
  338. if len(node.qualified_name) == 0:
  339. return
  340. cur = self._to_autoreload.traverse_prefixes(list(node.qualified_name[:-1]))
  341. if node.abstract_syntax_tree is not None:
  342. cur.defs_to_reload.append(
  343. ((node.qualified_name[-1],), node.abstract_syntax_tree)
  344. )
  345. def _check_dependents_inner(
  346. self, prefixes: list[str] | None = None
  347. ) -> list[DependencyNode]:
  348. prefixes = prefixes or []
  349. cur = self._to_autoreload.traverse_prefixes(prefixes)
  350. ans = []
  351. for (func_name, *_), _ in cur.defs_to_reload:
  352. node = tuple(prefixes + [func_name])
  353. ans.extend(self._gen_dependents(node))
  354. for class_name in cur.new_nested_classes:
  355. ans.extend(self._check_dependents_inner(prefixes + [class_name]))
  356. return ans
  357. def _gen_dependents(self, qualname: tuple[str, ...]) -> list[DependencyNode]:
  358. ans = []
  359. if qualname not in self.dependency_graph:
  360. return []
  361. for elt in self.dependency_graph[qualname]:
  362. ans.extend(self._gen_dependents(elt.qualified_name))
  363. ans.append(elt)
  364. return ans
  365. def _patch_namespace_inner(
  366. self, ns: ModuleType | type, prefixes: list[str] | None = None
  367. ) -> bool:
  368. """
  369. This function patches module functions and methods. Specifically, only objects with their name in
  370. self.to_autoreload will be considered for patching. If an object has been marked to be autoreloaded,
  371. new_source_code gets executed in the old version's global environment. Then, replace the old function's
  372. attributes with the new function's attributes.
  373. """
  374. prefixes = prefixes or []
  375. cur = self._to_autoreload.traverse_prefixes(prefixes)
  376. namespace_to_check = ns
  377. for prefix in prefixes:
  378. namespace_to_check = namespace_to_check.__dict__[prefix]
  379. seen_names: set[str] = set()
  380. for names, new_ast_def in cur.defs_to_reload:
  381. if len(names) == 1 and names[0] in seen_names:
  382. continue
  383. seen_names.update(names)
  384. local_env: dict[str, Any] = {}
  385. if (
  386. isinstance(new_ast_def, (ast.FunctionDef, ast.AsyncFunctionDef))
  387. and (name := names[0]) in namespace_to_check.__dict__
  388. ):
  389. assert len(names) == 1
  390. to_patch_to = namespace_to_check.__dict__[name]
  391. if isinstance(to_patch_to, (staticmethod, classmethod)):
  392. to_patch_to = to_patch_to.__func__
  393. # exec new source code using old function's (obj) globals environment.
  394. func_code = textwrap.dedent(ast.unparse(new_ast_def))
  395. if is_method := (len(prefixes) > 0):
  396. func_code = "class __autoreload_class__:\n" + textwrap.indent(
  397. func_code, " "
  398. )
  399. global_env = ns.__dict__
  400. if not isinstance(global_env, dict):
  401. global_env = dict(global_env)
  402. # Compile with correct filename to preserve in traceback
  403. filename = (
  404. getattr(to_patch_to, "__code__", None)
  405. and to_patch_to.__code__.co_filename
  406. or "<string>"
  407. )
  408. func_asts = [ast.parse(func_code)]
  409. if len(cast(ast.FunctionDef, func_asts[0].body[0]).decorator_list) > 0:
  410. without_decorator_list = pickle.loads(pickle.dumps(func_asts[0]))
  411. cast(
  412. ast.FunctionDef, without_decorator_list.body[0]
  413. ).decorator_list = []
  414. func_asts.insert(0, without_decorator_list)
  415. for func_ast in func_asts:
  416. compiled_code = compile(
  417. func_ast, filename, mode="exec", dont_inherit=True
  418. )
  419. exec(compiled_code, global_env, local_env) # type: ignore[arg-type]
  420. # local_env contains the function exec'd from new version of function
  421. if is_method:
  422. to_patch_from = getattr(local_env["__autoreload_class__"], name)
  423. else:
  424. to_patch_from = local_env[name]
  425. if isinstance(to_patch_from, (staticmethod, classmethod)):
  426. to_patch_from = to_patch_from.__func__
  427. if isinstance(to_patch_to, property) and isinstance(
  428. to_patch_from, property
  429. ):
  430. for attr in ("fget", "fset", "fdel"):
  431. if (
  432. getattr(to_patch_to, attr) is None
  433. or getattr(to_patch_from, attr) is None
  434. ):
  435. self.try_patch_attr(to_patch_to, to_patch_from, attr)
  436. else:
  437. self.patch_function(
  438. getattr(to_patch_to, attr),
  439. getattr(to_patch_from, attr),
  440. is_method,
  441. )
  442. elif not isinstance(to_patch_to, property) and not isinstance(
  443. to_patch_from, property
  444. ):
  445. self.patch_function(to_patch_to, to_patch_from, is_method)
  446. else:
  447. raise ValueError(
  448. "adding or removing property decorations not supported"
  449. )
  450. else:
  451. exec(
  452. ast.unparse(new_ast_def),
  453. ns.__dict__ | namespace_to_check.__dict__,
  454. local_env,
  455. )
  456. for name in names:
  457. setattr(namespace_to_check, name, local_env[name])
  458. cur.defs_to_reload.clear()
  459. for name in cur.defs_to_delete:
  460. try:
  461. delattr(namespace_to_check, name)
  462. except (AttributeError, TypeError, ValueError):
  463. # give up on deleting the attribute, let the stale one dangle
  464. pass
  465. cur.defs_to_delete.clear()
  466. for class_name, class_ast_node in cur.new_nested_classes.items():
  467. local_env_class: dict[str, Any] = {}
  468. exec(
  469. ast.unparse(class_ast_node),
  470. ns.__dict__ | namespace_to_check.__dict__,
  471. local_env_class,
  472. )
  473. setattr(namespace_to_check, class_name, local_env_class[class_name])
  474. cur.new_nested_classes.clear()
  475. for class_name in cur.children.keys():
  476. if not self._patch_namespace(ns, prefixes + [class_name]):
  477. return False
  478. cur.children.clear()
  479. return True
  480. def _patch_namespace(
  481. self, ns: ModuleType | type, prefixes: list[str] | None = None
  482. ) -> bool:
  483. """
  484. Wrapper for patching all elements in a namespace as specified by the to_autoreload member variable.
  485. Returns `true` if patching was successful, and `false` if unsuccessful.
  486. """
  487. try:
  488. return self._patch_namespace_inner(ns, prefixes=prefixes)
  489. except Exception:
  490. return False
  491. def maybe_reload_module(self, module: ModuleType) -> bool:
  492. """
  493. Uses Deduperreload to try to update a module.
  494. Returns `true` on success and `false` on failure.
  495. """
  496. if not self.enabled:
  497. return False
  498. if not (modname := getattr(module, "__name__", None)):
  499. return False
  500. if (fname := get_module_file_name(module)) is None:
  501. return False
  502. try:
  503. with open(fname, "r", encoding="utf8") as f:
  504. new_source_code = f.read()
  505. except Exception as e:
  506. logger = logging.getLogger("autoreload")
  507. logger.exception(
  508. f"Failed to read module file '{fname}' for module '{modname}': {type(e).__name__}"
  509. )
  510. return False
  511. patched_flag = False
  512. if old_source_code := self.source_by_modname.get(modname):
  513. # get old/new module ast
  514. try:
  515. old_module_ast = ast.parse(old_source_code)
  516. new_module_ast = ast.parse(new_source_code)
  517. except Exception:
  518. return False
  519. # detect if we are able to use our autoreload algorithm
  520. ctx = contextlib.suppress()
  521. with ctx:
  522. self._build_dependency_graph(new_module_ast)
  523. if (
  524. self.detect_autoreload(old_module_ast, new_module_ast)
  525. and self._check_dependents()
  526. and self._patch_namespace(module)
  527. ):
  528. patched_flag = True
  529. self.source_by_modname[modname] = new_source_code
  530. self._to_autoreload = AutoreloadTree()
  531. return patched_flag
  532. def _separate_name(
  533. self,
  534. decorator: ast.Attribute | ast.Name | ast.Call | ast.expr,
  535. accept_calls: bool,
  536. ) -> list[str] | None:
  537. """
  538. Generates a qualified name for a given decorator by finding its relative namespace.
  539. """
  540. if isinstance(decorator, ast.Name):
  541. return [decorator.id]
  542. elif isinstance(decorator, ast.Call):
  543. if accept_calls:
  544. return self._separate_name(decorator.func, False)
  545. else:
  546. return None
  547. if not isinstance(decorator, ast.Attribute):
  548. return None
  549. if pref := self._separate_name(decorator.value, False):
  550. return pref + [decorator.attr]
  551. else:
  552. return None
  553. def _gather_dependents(
  554. self, body: list[ast.stmt], body_prefixes: list[str] | None = None
  555. ) -> bool:
  556. body_prefixes = body_prefixes or []
  557. for ast_node in body:
  558. ast_elt: ast.expr | ast.stmt = ast_node
  559. if isinstance(ast_elt, ast.ClassDef):
  560. self._gather_dependents(ast_elt.body, body_prefixes + [ast_elt.name])
  561. continue
  562. if not isinstance(ast_elt, (ast.FunctionDef, ast.AsyncFunctionDef)):
  563. continue
  564. qualified_name = tuple(body_prefixes + [ast_elt.name])
  565. cur_dependency_node = DependencyNode(qualified_name, ast_elt)
  566. for decorator in ast_elt.decorator_list:
  567. decorator_path = self._separate_name(decorator, True)
  568. if not decorator_path:
  569. continue
  570. decorator_path_tuple = tuple(decorator_path)
  571. self.dependency_graph.setdefault(decorator_path_tuple, []).append(
  572. cur_dependency_node
  573. )
  574. return True
  575. def _build_dependency_graph(self, new_ast: ast.Module | ast.ClassDef) -> bool:
  576. """
  577. Wrapper function for generating dependency graph given some AST.
  578. Returns `true` on success. Returns `false` on failure.
  579. Currently, only returns `true` as we do not block on failure to build this graph.
  580. """
  581. return self._gather_dependents(new_ast.body)