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

1from __future__ import annotations 

2 

3import json 

4import sys 

5from typing import IO, TYPE_CHECKING, Any 

6 

7from dependenpy._internal.structures import Graph, Matrix, TreeMap 

8 

9if TYPE_CHECKING: 

10 from dependenpy._internal.dsm import Module, Package 

11 

12 

13class NodeMixin: 

14 """Shared code between DSM, Package and Module.""" 

15 

16 @property 

17 def ismodule(self) -> bool: 

18 """Property to check if object is instance of Module. 

19 

20 Returns: 

21 Whether this object is a module. 

22 """ 

23 return False 

24 

25 @property 

26 def ispackage(self) -> bool: 

27 """Property to check if object is instance of Package. 

28 

29 Returns: 

30 Whether this object is a package. 

31 """ 

32 return False 

33 

34 @property 

35 def isdsm(self) -> bool: 

36 """Property to check if object is instance of DSM. 

37 

38 Returns: 

39 Whether this object is a DSM. 

40 """ 

41 return False 

42 

43 

44class RootNode: 

45 """Shared code between DSM and Package.""" 

46 

47 def __init__(self, build_tree: bool = True): # noqa: FBT001,FBT002 

48 """Initialization method. 

49 

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.""" 

63 

64 if build_tree: 

65 self.build_tree() 

66 

67 def __contains__(self, item: Package | Module) -> bool: 

68 """Get result of _contains, cache it and return it. 

69 

70 Args: 

71 item: A package or module. 

72 

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] 

79 

80 def __getitem__(self, item: str) -> Package | Module: 

81 """Return the corresponding Package or Module object. 

82 

83 Args: 

84 item: Name of the package/module, dot-separated. 

85 

86 Raises: 

87 KeyError: When the package or module cannot be found. 

88 

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) 

105 

106 def __bool__(self) -> bool: 

107 """Node as Boolean. 

108 

109 Returns: 

110 Result of node.empty. 

111 """ 

112 return bool(self.modules or self.packages) 

113 

114 @property 

115 def empty(self) -> bool: 

116 """Whether the node has neither modules nor packages. 

117 

118 Returns: 

119 True if empty, False otherwise. 

120 """ 

121 return not bool(self) 

122 

123 @property 

124 def submodules(self) -> list[Module]: 

125 """Property to return all sub-modules of the node, recursively. 

126 

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 

135 

136 def build_tree(self) -> None: 

137 """To be overridden.""" 

138 raise NotImplementedError 

139 

140 def _contains(self, item: Package | Module) -> bool: 

141 """Whether given item is contained inside the node modules/packages. 

142 

143 Args: 

144 item (Package/Module): a package or module. 

145 

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) 

155 

156 def get(self, item: str) -> Package | Module: 

157 """Get item through `__getitem__` and cache the result. 

158 

159 Args: 

160 item: Name of package or module. 

161 

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] 

172 

173 def get_target(self, target: str) -> Package | Module: 

174 """Get the result of _get_target, cache it and return it. 

175 

176 Args: 

177 target: Target to find. 

178 

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] 

185 

186 def _get_target(self, target: str) -> Package | Module | None: 

187 """Get the Package or Module related to given target. 

188 

189 Args: 

190 target (str): target to find. 

191 

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 

216 

217 def build_dependencies(self) -> None: 

218 """Recursively build the dependencies for sub-modules and sub-packages. 

219 

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() 

227 

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. 

236 

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) 

245 

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. 

254 

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) 

263 

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. 

266 

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) 

274 

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) 

288 

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) 

296 

297 def _to_json(self, **kwargs: Any) -> str: 

298 return json.dumps(self.as_dict(), **kwargs) 

299 

300 def as_dict(self) -> dict: 

301 """Return the dependencies as a dictionary. 

302 

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 } 

311 

312 def as_graph(self, depth: int = 0) -> Graph: 

313 """Create a graph with self as node, cache it, return it. 

314 

315 Args: 

316 depth: Depth of the graph. 

317 

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] 

324 

325 def as_matrix(self, depth: int = 0) -> Matrix: 

326 """Create a matrix with self as node, cache it, return it. 

327 

328 Args: 

329 depth: Depth of the matrix. 

330 

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] 

337 

338 def as_treemap(self) -> TreeMap: 

339 """Return the dependencies as a TreeMap. 

340 

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 

347 

348 

349class LeafNode: 

350 """Shared code between Package and Module.""" 

351 

352 def __init__(self): 

353 """Initialization method.""" 

354 self._depth_cache = None 

355 

356 def __str__(self): 

357 return self.absolute_name() 

358 

359 @property 

360 def root(self) -> Package: 

361 """Property to return the root of this node. 

362 

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 

370 

371 @property 

372 def depth(self) -> int: 

373 """Property to tell the depth of the node in the tree. 

374 

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 

387 

388 def absolute_name(self, depth: int = 0) -> str: 

389 """Return the absolute name of the node. 

390 

391 Concatenate names from root to self within depth. 

392 

393 Args: 

394 depth: Maximum depth to go to. 

395 

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))