Coverage for src/dependenpy/node.py: 84.42%
184 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-09-04 11:35 +0200
« prev ^ index » next coverage.py v6.4.1, created at 2022-09-04 11:35 +0200
1"""dependenpy node module."""
3from __future__ import annotations
5import json
6import sys
7from typing import IO, TYPE_CHECKING, Any
9from dependenpy.structures import Graph, Matrix, TreeMap
11if TYPE_CHECKING: 11 ↛ 12line 11 didn't jump to line 12, because the condition on line 11 was never true
12 from dependenpy.dsm import Module, Package
15class NodeMixin(object):
16 """Shared code between DSM, Package and Module."""
18 @property
19 def ismodule(self) -> bool:
20 """
21 Property to check if object is instance of Module.
23 Returns:
24 Whether this object is a module.
25 """
26 return False
28 @property
29 def ispackage(self) -> bool:
30 """
31 Property to check if object is instance of Package.
33 Returns:
34 Whether this object is a package.
35 """
36 return False
38 @property
39 def isdsm(self) -> bool:
40 """
41 Property to check if object is instance of DSM.
43 Returns:
44 Whether this object is a DSM.
45 """
46 return False
49class RootNode(object):
50 """Shared code between DSM and Package."""
52 def __init__(self, build_tree=True):
53 """
54 Initialization method.
56 Args:
57 build_tree (bool): whether to immediately build the tree or not.
58 """
59 self._target_cache = {}
60 self._item_cache = {}
61 self._contains_cache = {}
62 self._matrix_cache = {}
63 self._graph_cache = {}
64 self._treemap_cache = None
65 self.modules = []
66 self.packages = []
68 if build_tree:
69 self.build_tree()
71 def __contains__(self, item: Package | Module) -> bool:
72 """
73 Get result of _contains, cache it and return it.
75 Args:
76 item: A package or module.
78 Returns:
79 True if self contains item, False otherwise.
80 """
81 if item not in self._contains_cache:
82 self._contains_cache[item] = self._contains(item)
83 return self._contains_cache[item]
85 def __getitem__(self, item: str) -> Package | Module: # noqa: WPS231
86 """
87 Return the corresponding Package or Module object.
89 Args:
90 item: Name of the package/module, dot-separated.
92 Raises:
93 KeyError: When the package or module cannot be found.
95 Returns:
96 The corresponding object.
97 """
98 depth = item.count(".") + 1
99 parts = item.split(".", 1)
100 for module in self.modules:
101 if parts[0] == module.name:
102 if depth == 1: 102 ↛ 100line 102 didn't jump to line 100, because the condition on line 102 was never false
103 return module
104 for package in self.packages: 104 ↛ 111line 104 didn't jump to line 111, because the loop on line 104 didn't complete
105 if parts[0] == package.name: 105 ↛ 104line 105 didn't jump to line 104, because the condition on line 105 was never false
106 if depth == 1:
107 return package
108 obj = package.get(parts[1])
109 if obj: 109 ↛ 104line 109 didn't jump to line 104, because the condition on line 109 was never false
110 return obj
111 raise KeyError(item)
113 def __bool__(self) -> bool:
114 """
115 Node as Boolean.
117 Returns:
118 Result of node.empty.
120 """
121 return bool(self.modules or self.packages)
123 @property
124 def empty(self) -> bool:
125 """
126 Whether the node has neither modules nor packages.
128 Returns:
129 True if empty, False otherwise.
130 """
131 return not bool(self)
133 @property
134 def submodules(self) -> list[Module]:
135 """
136 Property to return all sub-modules of the node, recursively.
138 Returns:
139 The sub-modules.
140 """
141 submodules = []
142 submodules.extend(self.modules)
143 for package in self.packages:
144 submodules.extend(package.submodules)
145 return submodules
147 def build_tree(self):
148 """To be overridden.""" # noqa: DAR401
149 raise NotImplementedError
151 def _contains(self, item):
152 """
153 Whether given item is contained inside the node modules/packages.
155 Args:
156 item (Package/Module): a package or module.
158 Returns:
159 bool: True if self is item or item in self's packages/modules.
160 """
161 if self is item:
162 return True
163 for module in self.modules:
164 if item in module:
165 return True
166 for package in self.packages:
167 if item in package:
168 return True
169 return False
171 def get(self, item: str) -> Package | Module:
172 """
173 Get item through `__getitem__` and cache the result.
175 Args:
176 item: Name of package or module.
178 Returns:
179 The corresponding object.
180 """
181 if item not in self._item_cache: 181 ↛ 187line 181 didn't jump to line 187, because the condition on line 181 was never false
182 try:
183 obj = self.__getitem__(item)
184 except KeyError:
185 obj = None
186 self._item_cache[item] = obj
187 return self._item_cache[item]
189 def get_target(self, target: str) -> Package | Module:
190 """
191 Get the result of _get_target, cache it and return it.
193 Args:
194 target: Target to find.
196 Returns:
197 Package containing target or corresponding module.
198 """
199 if target not in self._target_cache:
200 self._target_cache[target] = self._get_target(target)
201 return self._target_cache[target]
203 def _get_target(self, target): # noqa: WPS231
204 """
205 Get the Package or Module related to given target.
207 Args:
208 target (str): target to find.
210 Returns:
211 Package/Module: package containing target or corresponding module.
212 """
213 depth = target.count(".") + 1
214 parts = target.split(".", 1)
215 for module in self.modules:
216 if parts[0] == module.name:
217 if depth < 3: 217 ↛ 215line 217 didn't jump to line 215, because the condition on line 217 was never false
218 return module
219 for package in self.packages:
220 if parts[0] == package.name:
221 if depth == 1:
222 return package
223 target = package._get_target(parts[1]) # noqa: WPS437
224 if target:
225 return target
226 # FIXME: can lead to internal dep instead of external
227 # see example with django.contrib.auth.forms
228 # importing forms from django
229 # Idea: when parsing files with ast, record what objects
230 # are defined in the module. Then check here if the given
231 # part is one of these objects.
232 if depth < 3: 232 ↛ 219line 232 didn't jump to line 219, because the condition on line 232 was never false
233 return package
234 return None
236 def build_dependencies(self):
237 """
238 Recursively build the dependencies for sub-modules and sub-packages.
240 Iterate on node's modules then packages and call their
241 build_dependencies methods.
242 """
243 for module in self.modules:
244 module.build_dependencies()
245 for package in self.packages:
246 package.build_dependencies()
248 def print_graph(
249 self, format: str | None = None, output: IO = sys.stdout, depth: int = 0, **kwargs: Any # noqa: A002
250 ):
251 """
252 Print the graph for self's nodes.
254 Args:
255 format: Output format (csv, json or text).
256 output: File descriptor on which to write.
257 depth: Depth of the graph.
258 **kwargs: Additional keyword arguments passed to `graph.print`.
259 """
260 graph = self.as_graph(depth=depth)
261 graph.print(format=format, output=output, **kwargs)
263 def print_matrix(
264 self, format: str | None = None, output: IO = sys.stdout, depth: int = 0, **kwargs: Any # noqa: A002
265 ):
266 """
267 Print the matrix for self's nodes.
269 Args:
270 format: Output format (csv, json or text).
271 output: File descriptor on which to write.
272 depth: Depth of the matrix.
273 **kwargs: Additional keyword arguments passed to `matrix.print`.
274 """
275 matrix = self.as_matrix(depth=depth)
276 matrix.print(format=format, output=output, **kwargs)
278 def print_treemap(self, format: str | None = None, output: IO = sys.stdout, **kwargs: Any): # noqa: A002
279 """
280 Print the matrix for self's nodes.
282 Args:
283 format: Output format (csv, json or text).
284 output: File descriptor on which to write.
285 **kwargs: Additional keyword arguments passed to `treemap.print`.
286 """
287 treemap = self.as_treemap()
288 treemap.print(format=format, output=output, **kwargs)
290 def _to_text(self, **kwargs):
291 indent = kwargs.pop("indent", 2)
292 base_indent = kwargs.pop("base_indent", None)
293 if base_indent is None:
294 base_indent = indent
295 indent = 0
296 text = [" " * indent + str(self) + "\n"]
297 new_indent = indent + base_indent
298 for module in self.modules:
299 text.append(module._to_text(indent=new_indent, base_indent=base_indent)) # noqa: WPS437
300 for package in self.packages:
301 text.append(package._to_text(indent=new_indent, base_indent=base_indent)) # noqa: WPS437
302 return "".join(text)
304 def _to_csv(self, **kwargs):
305 header = kwargs.pop("header", True)
306 modules = sorted(self.submodules, key=lambda mod: mod.absolute_name())
307 text = ["module,path,target,lineno,what,external\n" if header else ""]
308 for module in modules:
309 text.append(module._to_csv(header=False)) # noqa: WPS437
310 return "".join(text)
312 def _to_json(self, **kwargs):
313 return json.dumps(self.as_dict(), **kwargs)
315 def as_dict(self) -> dict:
316 """
317 Return the dependencies as a dictionary.
319 Returns:
320 Dictionary of dependencies.
321 """
322 return {
323 "name": str(self),
324 "modules": [module.as_dict() for module in self.modules],
325 "packages": [package.as_dict() for package in self.packages],
326 }
328 def as_graph(self, depth: int = 0) -> Graph:
329 """
330 Create a graph with self as node, cache it, return it.
332 Args:
333 depth: Depth of the graph.
335 Returns:
336 An instance of Graph.
337 """
338 if depth not in self._graph_cache:
339 self._graph_cache[depth] = Graph(self, depth=depth)
340 return self._graph_cache[depth]
342 def as_matrix(self, depth: int = 0) -> Matrix:
343 """
344 Create a matrix with self as node, cache it, return it.
346 Args:
347 depth: Depth of the matrix.
349 Returns:
350 An instance of Matrix.
351 """
352 if depth not in self._matrix_cache: 352 ↛ 354line 352 didn't jump to line 354, because the condition on line 352 was never false
353 self._matrix_cache[depth] = Matrix(self, depth=depth) # type: ignore[arg-type]
354 return self._matrix_cache[depth]
356 def as_treemap(self) -> TreeMap:
357 """
358 Return the dependencies as a TreeMap.
360 Returns:
361 An instance of TreeMap.
362 """
363 if not self._treemap_cache: 363 ↛ 365line 363 didn't jump to line 365, because the condition on line 363 was never false
364 self._treemap_cache = TreeMap(self)
365 return self._treemap_cache
368class LeafNode(object):
369 """Shared code between Package and Module."""
371 def __init__(self):
372 """Initialization method."""
373 self._depth_cache = None
375 def __str__(self):
376 return self.absolute_name()
378 @property
379 def root(self) -> Package:
380 """
381 Property to return the root of this node.
383 Returns:
384 Package: this node's root package.
385 """
386 node: Package = self # type: ignore[assignment]
387 while node.package is not None:
388 node = node.package
389 return node
391 @property
392 def depth(self) -> int:
393 """
394 Property to tell the depth of the node in the tree.
396 Returns:
397 The node's depth in the tree.
398 """
399 if self._depth_cache is not None:
400 return self._depth_cache
401 node: Package
402 depth, node = 1, self # type: ignore[assignment]
403 while node.package is not None:
404 depth += 1
405 node = node.package
406 self._depth_cache = depth
407 return depth
409 def absolute_name(self, depth: int = 0) -> str:
410 """
411 Return the absolute name of the node.
413 Concatenate names from root to self within depth.
415 Args:
416 depth: Maximum depth to go to.
418 Returns:
419 Absolute name of the node (until given depth is reached).
420 """
421 node: Package
422 node, node_depth = self, self.depth # type: ignore[assignment]
423 if depth < 1:
424 depth = node_depth
425 while node_depth > depth and node.package is not None:
426 node = node.package
427 node_depth -= 1
428 names = []
429 while node is not None:
430 names.append(node.name)
431 node = node.package # type: ignore[assignment]
432 return ".".join(reversed(names))