objgraph.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. """Find all objects reachable from a root object."""
  2. from __future__ import annotations
  3. import types
  4. import weakref
  5. from collections.abc import Iterable
  6. from typing import Iterator, Mapping
  7. from typing_extensions import Final
  8. method_descriptor_type: Final = type(object.__dir__)
  9. method_wrapper_type: Final = type(object().__ne__)
  10. wrapper_descriptor_type: Final = type(object.__ne__)
  11. FUNCTION_TYPES: Final = (
  12. types.BuiltinFunctionType,
  13. types.FunctionType,
  14. types.MethodType,
  15. method_descriptor_type,
  16. wrapper_descriptor_type,
  17. method_wrapper_type,
  18. )
  19. ATTR_BLACKLIST: Final = {"__doc__", "__name__", "__class__", "__dict__"}
  20. # Instances of these types can't have references to other objects
  21. ATOMIC_TYPE_BLACKLIST: Final = {bool, int, float, str, type(None), object}
  22. # Don't look at most attributes of these types
  23. COLLECTION_TYPE_BLACKLIST: Final = {list, set, dict, tuple}
  24. # Don't return these objects
  25. TYPE_BLACKLIST: Final = {weakref.ReferenceType}
  26. def isproperty(o: object, attr: str) -> bool:
  27. return isinstance(getattr(type(o), attr, None), property)
  28. def get_edge_candidates(o: object) -> Iterator[tuple[object, object]]:
  29. # use getattr because mypyc expects dict, not mappingproxy
  30. if "__getattribute__" in getattr(type(o), "__dict__"): # noqa: B009
  31. return
  32. if type(o) not in COLLECTION_TYPE_BLACKLIST:
  33. for attr in dir(o):
  34. try:
  35. if attr not in ATTR_BLACKLIST and hasattr(o, attr) and not isproperty(o, attr):
  36. e = getattr(o, attr)
  37. if not type(e) in ATOMIC_TYPE_BLACKLIST:
  38. yield attr, e
  39. except AssertionError:
  40. pass
  41. if isinstance(o, Mapping):
  42. yield from o.items()
  43. elif isinstance(o, Iterable) and not isinstance(o, str):
  44. for i, e in enumerate(o):
  45. yield i, e
  46. def get_edges(o: object) -> Iterator[tuple[object, object]]:
  47. for s, e in get_edge_candidates(o):
  48. if isinstance(e, FUNCTION_TYPES):
  49. # We don't want to collect methods, but do want to collect values
  50. # in closures and self pointers to other objects
  51. if hasattr(e, "__closure__"):
  52. yield (s, "__closure__"), e.__closure__
  53. if hasattr(e, "__self__"):
  54. se = e.__self__
  55. if se is not o and se is not type(o) and hasattr(s, "__self__"):
  56. yield s.__self__, se
  57. else:
  58. if not type(e) in TYPE_BLACKLIST:
  59. yield s, e
  60. def get_reachable_graph(root: object) -> tuple[dict[int, object], dict[int, tuple[int, object]]]:
  61. parents = {}
  62. seen = {id(root): root}
  63. worklist = [root]
  64. while worklist:
  65. o = worklist.pop()
  66. for s, e in get_edges(o):
  67. if id(e) in seen:
  68. continue
  69. parents[id(e)] = (id(o), s)
  70. seen[id(e)] = e
  71. worklist.append(e)
  72. return seen, parents
  73. def get_path(
  74. o: object, seen: dict[int, object], parents: dict[int, tuple[int, object]]
  75. ) -> list[tuple[object, object]]:
  76. path = []
  77. while id(o) in parents:
  78. pid, attr = parents[id(o)]
  79. o = seen[pid]
  80. path.append((attr, o))
  81. path.reverse()
  82. return path