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

1"""dependenpy node module.""" 

2 

3from __future__ import annotations 

4 

5import json 

6import sys 

7from typing import IO, TYPE_CHECKING, Any 

8 

9from dependenpy.structures import Graph, Matrix, TreeMap 

10 

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 

13 

14 

15class NodeMixin(object): 

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

17 

18 @property 

19 def ismodule(self) -> bool: 

20 """ 

21 Property to check if object is instance of Module. 

22 

23 Returns: 

24 Whether this object is a module. 

25 """ 

26 return False 

27 

28 @property 

29 def ispackage(self) -> bool: 

30 """ 

31 Property to check if object is instance of Package. 

32 

33 Returns: 

34 Whether this object is a package. 

35 """ 

36 return False 

37 

38 @property 

39 def isdsm(self) -> bool: 

40 """ 

41 Property to check if object is instance of DSM. 

42 

43 Returns: 

44 Whether this object is a DSM. 

45 """ 

46 return False 

47 

48 

49class RootNode(object): 

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

51 

52 def __init__(self, build_tree=True): 

53 """ 

54 Initialization method. 

55 

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 = [] 

67 

68 if build_tree: 

69 self.build_tree() 

70 

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

72 """ 

73 Get result of _contains, cache it and return it. 

74 

75 Args: 

76 item: A package or module. 

77 

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] 

84 

85 def __getitem__(self, item: str) -> Package | Module: # noqa: WPS231 

86 """ 

87 Return the corresponding Package or Module object. 

88 

89 Args: 

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

91 

92 Raises: 

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

94 

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) 

112 

113 def __bool__(self) -> bool: 

114 """ 

115 Node as Boolean. 

116 

117 Returns: 

118 Result of node.empty. 

119 

120 """ 

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

122 

123 @property 

124 def empty(self) -> bool: 

125 """ 

126 Whether the node has neither modules nor packages. 

127 

128 Returns: 

129 True if empty, False otherwise. 

130 """ 

131 return not bool(self) 

132 

133 @property 

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

135 """ 

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

137 

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 

146 

147 def build_tree(self): 

148 """To be overridden.""" # noqa: DAR401 

149 raise NotImplementedError 

150 

151 def _contains(self, item): 

152 """ 

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

154 

155 Args: 

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

157 

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 

170 

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

172 """ 

173 Get item through `__getitem__` and cache the result. 

174 

175 Args: 

176 item: Name of package or module. 

177 

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] 

188 

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

190 """ 

191 Get the result of _get_target, cache it and return it. 

192 

193 Args: 

194 target: Target to find. 

195 

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] 

202 

203 def _get_target(self, target): # noqa: WPS231 

204 """ 

205 Get the Package or Module related to given target. 

206 

207 Args: 

208 target (str): target to find. 

209 

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 

235 

236 def build_dependencies(self): 

237 """ 

238 Recursively build the dependencies for sub-modules and sub-packages. 

239 

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

247 

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. 

253 

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) 

262 

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. 

268 

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) 

277 

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. 

281 

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) 

289 

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) 

303 

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) 

311 

312 def _to_json(self, **kwargs): 

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

314 

315 def as_dict(self) -> dict: 

316 """ 

317 Return the dependencies as a dictionary. 

318 

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 } 

327 

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

329 """ 

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

331 

332 Args: 

333 depth: Depth of the graph. 

334 

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] 

341 

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

343 """ 

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

345 

346 Args: 

347 depth: Depth of the matrix. 

348 

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] 

355 

356 def as_treemap(self) -> TreeMap: 

357 """ 

358 Return the dependencies as a TreeMap. 

359 

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 

366 

367 

368class LeafNode(object): 

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

370 

371 def __init__(self): 

372 """Initialization method.""" 

373 self._depth_cache = None 

374 

375 def __str__(self): 

376 return self.absolute_name() 

377 

378 @property 

379 def root(self) -> Package: 

380 """ 

381 Property to return the root of this node. 

382 

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 

390 

391 @property 

392 def depth(self) -> int: 

393 """ 

394 Property to tell the depth of the node in the tree. 

395 

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 

408 

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

410 """ 

411 Return the absolute name of the node. 

412 

413 Concatenate names from root to self within depth. 

414 

415 Args: 

416 depth: Maximum depth to go to. 

417 

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