Coverage for src/dependenpy/_internal/node.py: 85.66%
179 statements
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-24 18:36 +0200
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-24 18:36 +0200
1from __future__ import annotations
3import json
4import sys
5from typing import IO, TYPE_CHECKING, Any
7from dependenpy._internal.structures import Graph, Matrix, TreeMap
9if TYPE_CHECKING:
10 from dependenpy._internal.dsm import Module, Package
13class NodeMixin:
14 """Shared code between DSM, Package and Module."""
16 @property
17 def ismodule(self) -> bool:
18 """Property to check if object is instance of Module.
20 Returns:
21 Whether this object is a module.
22 """
23 return False
25 @property
26 def ispackage(self) -> bool:
27 """Property to check if object is instance of Package.
29 Returns:
30 Whether this object is a package.
31 """
32 return False
34 @property
35 def isdsm(self) -> bool:
36 """Property to check if object is instance of DSM.
38 Returns:
39 Whether this object is a DSM.
40 """
41 return False
44class RootNode:
45 """Shared code between DSM and Package."""
47 def __init__(self, build_tree: bool = True): # noqa: FBT001,FBT002
48 """Initialization method.
50 Args:
51 build_tree (bool): whether to immediately build the tree or not.
52 """
53 self._target_cache: dict[str, Any] = {}
54 self._item_cache: dict[str, Any] = {}
55 self._contains_cache: dict[Package | Module, bool] = {}
56 self._matrix_cache: dict[int, Matrix] = {}
57 self._graph_cache: dict[int, Graph] = {}
58 self._treemap_cache = TreeMap()
59 self.modules: list[Module] = []
60 """List of modules contained in the node."""
61 self.packages: list[Package] = []
62 """List of packages contained in the node."""
64 if build_tree:
65 self.build_tree()
67 def __contains__(self, item: Package | Module) -> bool:
68 """Get result of _contains, cache it and return it.
70 Args:
71 item: A package or module.
73 Returns:
74 True if self contains item, False otherwise.
75 """
76 if item not in self._contains_cache:
77 self._contains_cache[item] = self._contains(item)
78 return self._contains_cache[item]
80 def __getitem__(self, item: str) -> Package | Module:
81 """Return the corresponding Package or Module object.
83 Args:
84 item: Name of the package/module, dot-separated.
86 Raises:
87 KeyError: When the package or module cannot be found.
89 Returns:
90 The corresponding object.
91 """
92 depth = item.count(".") + 1
93 parts = item.split(".", 1)
94 for module in self.modules:
95 if parts[0] == module.name and depth == 1:
96 return module
97 for package in self.packages: 97 ↛ 104line 97 didn't jump to line 104 because the loop on line 97 didn't complete
98 if parts[0] == package.name: 98 ↛ 97line 98 didn't jump to line 97 because the condition on line 98 was always true
99 if depth == 1:
100 return package
101 obj = package.get(parts[1])
102 if obj: 102 ↛ 97line 102 didn't jump to line 97 because the condition on line 102 was always true
103 return obj
104 raise KeyError(item)
106 def __bool__(self) -> bool:
107 """Node as Boolean.
109 Returns:
110 Result of node.empty.
111 """
112 return bool(self.modules or self.packages)
114 @property
115 def empty(self) -> bool:
116 """Whether the node has neither modules nor packages.
118 Returns:
119 True if empty, False otherwise.
120 """
121 return not bool(self)
123 @property
124 def submodules(self) -> list[Module]:
125 """Property to return all sub-modules of the node, recursively.
127 Returns:
128 The sub-modules.
129 """
130 submodules = []
131 submodules.extend(self.modules)
132 for package in self.packages:
133 submodules.extend(package.submodules)
134 return submodules
136 def build_tree(self) -> None:
137 """To be overridden."""
138 raise NotImplementedError
140 def _contains(self, item: Package | Module) -> bool:
141 """Whether given item is contained inside the node modules/packages.
143 Args:
144 item (Package/Module): a package or module.
146 Returns:
147 bool: True if self is item or item in self's packages/modules.
148 """
149 if self is item:
150 return True
151 for module in self.modules:
152 if item in module:
153 return True
154 return any(item in package for package in self.packages)
156 def get(self, item: str) -> Package | Module:
157 """Get item through `__getitem__` and cache the result.
159 Args:
160 item: Name of package or module.
162 Returns:
163 The corresponding object.
164 """
165 if item not in self._item_cache: 165 ↛ 171line 165 didn't jump to line 171 because the condition on line 165 was always true
166 try:
167 obj = self.__getitem__(item)
168 except KeyError:
169 obj = None
170 self._item_cache[item] = obj
171 return self._item_cache[item]
173 def get_target(self, target: str) -> Package | Module:
174 """Get the result of _get_target, cache it and return it.
176 Args:
177 target: Target to find.
179 Returns:
180 Package containing target or corresponding module.
181 """
182 if target not in self._target_cache:
183 self._target_cache[target] = self._get_target(target)
184 return self._target_cache[target]
186 def _get_target(self, target: str) -> Package | Module | None:
187 """Get the Package or Module related to given target.
189 Args:
190 target (str): target to find.
192 Returns:
193 Package/Module: package containing target or corresponding module.
194 """
195 depth = target.count(".") + 1
196 parts = target.split(".", 1)
197 for module in self.modules:
198 if parts[0] == module.name and depth < 3: # noqa: PLR2004
199 return module
200 for package in self.packages:
201 if parts[0] == package.name:
202 if depth == 1:
203 return package
204 targ = package._get_target(parts[1])
205 if targ:
206 return targ
207 # FIXME: can lead to internal dep instead of external
208 # see example with django.contrib.auth.forms
209 # importing forms from django
210 # Idea: when parsing files with ast, record what objects
211 # are defined in the module. Then check here if the given
212 # part is one of these objects.
213 if depth < 3: # noqa: PLR2004 213 ↛ 200line 213 didn't jump to line 200 because the condition on line 213 was always true
214 return package
215 return None
217 def build_dependencies(self) -> None:
218 """Recursively build the dependencies for sub-modules and sub-packages.
220 Iterate on node's modules then packages and call their
221 build_dependencies methods.
222 """
223 for module in self.modules:
224 module.build_dependencies()
225 for package in self.packages:
226 package.build_dependencies()
228 def print_graph(
229 self,
230 format: str | None = None, # noqa: A002
231 output: IO = sys.stdout,
232 depth: int = 0,
233 **kwargs: Any,
234 ) -> None:
235 """Print the graph for self's nodes.
237 Args:
238 format: Output format (csv, json or text).
239 output: File descriptor on which to write.
240 depth: Depth of the graph.
241 **kwargs: Additional keyword arguments passed to `graph.print`.
242 """
243 graph = self.as_graph(depth=depth)
244 graph.print(format=format, output=output, **kwargs)
246 def print_matrix(
247 self,
248 format: str | None = None, # noqa: A002
249 output: IO = sys.stdout,
250 depth: int = 0,
251 **kwargs: Any,
252 ) -> None:
253 """Print the matrix for self's nodes.
255 Args:
256 format: Output format (csv, json or text).
257 output: File descriptor on which to write.
258 depth: Depth of the matrix.
259 **kwargs: Additional keyword arguments passed to `matrix.print`.
260 """
261 matrix = self.as_matrix(depth=depth)
262 matrix.print(format=format, output=output, **kwargs)
264 def print_treemap(self, format: str | None = None, output: IO = sys.stdout, **kwargs: Any) -> None: # noqa: A002
265 """Print the matrix for self's nodes.
267 Args:
268 format: Output format (csv, json or text).
269 output: File descriptor on which to write.
270 **kwargs: Additional keyword arguments passed to `treemap.print`.
271 """
272 treemap = self.as_treemap()
273 treemap.print(format=format, output=output, **kwargs)
275 def _to_text(self, **kwargs: Any) -> str:
276 indent = kwargs.pop("indent", 2)
277 base_indent = kwargs.pop("base_indent", None)
278 if base_indent is None:
279 base_indent = indent
280 indent = 0
281 text = [" " * indent + str(self) + "\n"]
282 new_indent = indent + base_indent
283 for module in self.modules:
284 text.append(module._to_text(indent=new_indent, base_indent=base_indent))
285 for package in self.packages:
286 text.append(package._to_text(indent=new_indent, base_indent=base_indent))
287 return "".join(text)
289 def _to_csv(self, **kwargs: Any) -> str:
290 header = kwargs.pop("header", True)
291 modules = sorted(self.submodules, key=lambda mod: mod.absolute_name())
292 text = ["module,path,target,lineno,what,external\n" if header else ""]
293 for module in modules:
294 text.append(module._to_csv(header=False))
295 return "".join(text)
297 def _to_json(self, **kwargs: Any) -> str:
298 return json.dumps(self.as_dict(), **kwargs)
300 def as_dict(self) -> dict:
301 """Return the dependencies as a dictionary.
303 Returns:
304 Dictionary of dependencies.
305 """
306 return {
307 "name": str(self),
308 "modules": [module.as_dict() for module in self.modules],
309 "packages": [package.as_dict() for package in self.packages],
310 }
312 def as_graph(self, depth: int = 0) -> Graph:
313 """Create a graph with self as node, cache it, return it.
315 Args:
316 depth: Depth of the graph.
318 Returns:
319 An instance of Graph.
320 """
321 if depth not in self._graph_cache:
322 self._graph_cache[depth] = Graph(self, depth=depth) # type: ignore[arg-type]
323 return self._graph_cache[depth]
325 def as_matrix(self, depth: int = 0) -> Matrix:
326 """Create a matrix with self as node, cache it, return it.
328 Args:
329 depth: Depth of the matrix.
331 Returns:
332 An instance of Matrix.
333 """
334 if depth not in self._matrix_cache: 334 ↛ 336line 334 didn't jump to line 336 because the condition on line 334 was always true
335 self._matrix_cache[depth] = Matrix(self, depth=depth) # type: ignore[arg-type]
336 return self._matrix_cache[depth]
338 def as_treemap(self) -> TreeMap:
339 """Return the dependencies as a TreeMap.
341 Returns:
342 An instance of TreeMap.
343 """
344 if not self._treemap_cache: 344 ↛ 345line 344 didn't jump to line 345 because the condition on line 344 was never true
345 self._treemap_cache = TreeMap(self)
346 return self._treemap_cache
349class LeafNode:
350 """Shared code between Package and Module."""
352 def __init__(self):
353 """Initialization method."""
354 self._depth_cache = None
356 def __str__(self):
357 return self.absolute_name()
359 @property
360 def root(self) -> Package:
361 """Property to return the root of this node.
363 Returns:
364 Package: this node's root package.
365 """
366 node: Package = self # type: ignore[assignment]
367 while node.package is not None:
368 node = node.package
369 return node
371 @property
372 def depth(self) -> int:
373 """Property to tell the depth of the node in the tree.
375 Returns:
376 The node's depth in the tree.
377 """
378 if self._depth_cache is not None:
379 return self._depth_cache
380 node: Package
381 depth, node = 1, self # type: ignore[assignment]
382 while node.package is not None:
383 depth += 1
384 node = node.package
385 self._depth_cache = depth
386 return depth
388 def absolute_name(self, depth: int = 0) -> str:
389 """Return the absolute name of the node.
391 Concatenate names from root to self within depth.
393 Args:
394 depth: Maximum depth to go to.
396 Returns:
397 Absolute name of the node (until given depth is reached).
398 """
399 node: Package
400 node, node_depth = self, self.depth # type: ignore[assignment]
401 if depth < 1:
402 depth = node_depth
403 while node_depth > depth and node.package is not None:
404 node = node.package
405 node_depth -= 1
406 names = []
407 while node is not None:
408 names.append(node.name)
409 node = node.package # type: ignore[assignment]
410 return ".".join(reversed(names))