Coverage for src/pytkdocs/objects.py: 98.15%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

164 statements  

1""" 

2This module defines the documented objects classes. 

3 

4- the generic [`Object`][pytkdocs.objects.Object] class 

5- the [`Module`][pytkdocs.objects.Module] class 

6- the [`Class`][pytkdocs.objects.Class] class 

7- the [`Method`][pytkdocs.objects.Method] class 

8- the [`Function`][pytkdocs.objects.Function] class 

9- the [`Attribute`][pytkdocs.objects.Attribute] class 

10 

11Note that properties are considered attributes, because they are used like such. 

12 

13It also defines a convenient [`Source`][pytkdocs.objects.Source] class to represent source code. 

14""" 

15 

16import importlib 

17import inspect 

18import os 

19import sys 

20from abc import ABCMeta 

21from functools import lru_cache 

22from pathlib import Path 

23from typing import List, Optional, Union 

24 

25from pytkdocs.parsers.docstrings.base import Parser, Section 

26from pytkdocs.properties import NAME_CLASS_PRIVATE, NAME_PRIVATE, NAME_SPECIAL, ApplicableNameProperty 

27 

28 

29class Source: 

30 """ 

31 Helper class to represent source code. 

32 

33 It is simply used to wrap the result of 

34 [`inspect.getsourceslines`](https://docs.python.org/3/library/inspect.html#inspect.getsourcelines). 

35 """ 

36 

37 def __init__(self, lines: Union[str, List[str]], line_start: int) -> None: 

38 """ 

39 Initialize the object. 

40 

41 Arguments: 

42 lines: A list of strings. The strings should have trailing newlines. 

43 line_start: The line number of where the code starts in the file. 

44 """ 

45 if isinstance(lines, list): 

46 code = "".join(lines) 

47 else: 

48 code = lines 

49 self.code = code 

50 """The code, as a single string.""" 

51 self.line_start = line_start 

52 """The first line number.""" 

53 

54 

55class Object(metaclass=ABCMeta): 

56 """ 

57 A base class to store information about a Python object. 

58 

59 Each instance additionally stores references to its children, grouped by category. 

60 """ 

61 

62 possible_name_properties: List[ApplicableNameProperty] = [] 

63 """ 

64 The properties that we can apply to the object based on its name. 

65 

66 The applicable properties vary from one subclass of `Object` to another. 

67 """ 

68 

69 def __init__( 

70 self, 

71 name: str, 

72 path: str, 

73 file_path: str, 

74 docstring: Optional[str] = "", 

75 properties: Optional[List[str]] = None, 

76 source: Optional[Source] = None, 

77 ) -> None: 

78 """ 

79 Initialize the object. 

80 

81 Arguments: 

82 name: The object's name. 

83 path: The object's dotted-path. 

84 file_path: The file path of the object's direct parent module. 

85 docstring: The object's docstring. 

86 properties: The object's properties. 

87 source: The object's source code. 

88 """ 

89 self.name = name 

90 """The object's name.""" 

91 self.path = path 

92 """The object's dotted-path.""" 

93 self.file_path = file_path 

94 """The file path of the object's direct parent module.""" 

95 self.docstring = docstring 

96 """The object's docstring.""" 

97 self.docstring_sections: List[Section] = [] 

98 """The object's docstring parsed into sections.""" 

99 self.docstring_errors: List[str] = [] 

100 """The errors detected while parsing the docstring.""" 

101 self.properties = properties or [] 

102 """The object's properties.""" 

103 self.parent: Optional[Object] = None 

104 """The object's parent (another instance of a subclass of `Object`).""" 

105 self.source = source 

106 """The object's source code.""" 

107 

108 self._path_map = {self.path: self} 

109 self._parsed = False 

110 

111 self.attributes: List[Attribute] = [] 

112 """The list of all the object's attributes.""" 

113 self.methods: List[Method] = [] 

114 """The list of all the object's methods.""" 

115 self.functions: List[Function] = [] 

116 """The list of all the object's functions.""" 

117 self.modules: List[Module] = [] 

118 """The list of all the object's submodules.""" 

119 self.classes: List[Class] = [] 

120 """The list of all the object's classes.""" 

121 self.children: List[Object] = [] 

122 """The list of all the object's children.""" 

123 

124 def __str__(self) -> str: 

125 return self.path 

126 

127 @property 

128 def category(self) -> str: 

129 """ 

130 Return the object's category. 

131 

132 Returns: 

133 The object's category (module, class, function, method or attribute). 

134 """ 

135 return self.__class__.__name__.lower() 

136 

137 @property 

138 def root(self) -> "Object": 

139 """ 

140 Return the object's root. 

141 

142 Returns: 

143 The object's root (top-most parent). 

144 """ 

145 obj = self 

146 while obj.parent: 

147 obj = obj.parent 

148 return obj 

149 

150 @property 

151 def relative_file_path(self) -> str: 

152 """ 

153 Return the relative file path of the object. 

154 

155 It is the relative path to the object's module, 

156 starting at the path of the top-most package it is contained in. 

157 

158 For example: 

159 

160 - package is `a` 

161 - package absolute path is `/abs/path/to/a` 

162 - module is `a.b.c` 

163 - object is `c` or anything defined in `c` 

164 - relative file path is `a/b/c.py` 

165 

166 If the relative file path cannot be determined, the value returned is `""` (empty string). 

167 

168 Returns: 

169 The path relative to the object's package. 

170 """ 

171 parts = self.path.split(".") 

172 namespaces = [".".join(parts[:length]) for length in range(1, len(parts) + 1)] # noqa: WPS221 (not complex) 

173 # Iterate through all sub namespaces including the last in case it is a module 

174 for namespace in namespaces: 174 ↛ 195line 174 didn't jump to line 195, because the loop on line 174 didn't complete

175 try: # noqa: WPS229 (more compact) 

176 importlib.import_module(namespace) 

177 top_package = sys.modules[namespace] 

178 except (ModuleNotFoundError, ImportError, KeyError): 

179 # ImportError: Triggered if the namespace is not importable 

180 # ModuleNotFoundError: Triggered if the namespace is not a module 

181 # KeyError: Triggered if the imported package isn't referenced under the same fully qualified name 

182 # Namespace packages are importable, so this should work for them 

183 return "" 

184 

185 try: # noqa: WPS229 (more compact) 

186 top_package_path = Path(inspect.getabsfile(top_package)).parent 

187 return str(Path(self.file_path).relative_to(top_package_path.parent)) 

188 except TypeError: 

189 # Triggered if getabsfile() can't be found in the case of a Namespace package 

190 pass # noqa: WPS420 (passing is the only way) 

191 except ValueError: 

192 # Triggered if Path().relative_to can't find an appropriate path 

193 return "" 

194 

195 return "" 

196 

197 @property 

198 def name_to_check(self) -> str: 

199 """ 

200 Return the attribute to check against name-properties regular expressions (private, class-private, special). 

201 

202 Returns: 

203 The attribute to check (its name). 

204 """ 

205 return self.name 

206 

207 @property 

208 def name_properties(self) -> List[str]: 

209 """ 

210 Return the object's name properties. 

211 

212 Returns: 

213 The object's name properties (private, class-private, special). 

214 """ 

215 properties = [] 

216 for prop, predicate in self.possible_name_properties: 

217 if predicate(self.name_to_check): 

218 properties.append(prop) 

219 return properties 

220 

221 @property 

222 def parent_path(self) -> str: 

223 """ 

224 Return the parent's path, computed from the current path. 

225 

226 The parent object path is not used: this property is used to see if an object is really related to another one, 

227 to add it as a child to the other. When we do that, the child doesn't even have a parent. 

228 

229 Returns: 

230 The dotted path of the parent object. 

231 """ 

232 return self.path.rsplit(".", 1)[0] 

233 

234 def add_child(self, obj: "Object") -> None: # noqa: WPS231 (not complex) 

235 """ 

236 Add an object as a child of this object. 

237 

238 If the child computed `parent_path` is not equal to this object's path, abort. 

239 

240 Append the child to the `children` list, and to the right category list. 

241 

242 Arguments: 

243 obj: An instance of documented object. 

244 """ 

245 if obj.parent_path != self.path: 

246 return 

247 

248 self.children.append(obj) 

249 if isinstance(obj, Module): 

250 self.modules.append(obj) 

251 elif isinstance(obj, Class): 

252 self.classes.append(obj) 

253 elif isinstance(obj, Function): 

254 self.functions.append(obj) 

255 elif isinstance(obj, Method): 

256 self.methods.append(obj) 

257 elif isinstance(obj, Attribute): 257 ↛ 267line 257 didn't jump to line 267, because the condition on line 257 was never false

258 # Dataclass attributes with default values will already be present in `self.attributes` as they are 

259 # resolved differently by the python interpreter. As they have a concrete value, they are already present 

260 # in the "original" class. They should be overridden with the new "dataclass" attribute coming in here 

261 # (having the "dataclass_field" property set) 

262 new_attribute_name = obj.name 

263 for attribute in self.attributes: 

264 if attribute.name == new_attribute_name: 

265 self.attributes.remove(attribute) 

266 self.attributes.append(obj) 

267 obj.parent = self 

268 

269 self._path_map[obj.path] = obj 

270 

271 def add_children(self, children: List["Object"]) -> None: 

272 """ 

273 Add a list of objects as children of this object. 

274 

275 Arguments: 

276 children: The list of children to add. 

277 """ 

278 for child in children: 

279 self.add_child(child) 

280 

281 def parse_docstring(self, parser: Parser, **context) -> None: 

282 """ 

283 Parse the docstring of this object. 

284 

285 Arguments: 

286 parser: A parser to parse the docstrings. 

287 **context: Additional context to use when parsing. 

288 """ 

289 if self.docstring and not self._parsed: 

290 sections, errors = parser.parse(self.docstring, {"obj": self, **context}) 

291 self.docstring_sections = sections 

292 self.docstring_errors = errors 

293 self._parsed = True 

294 

295 def parse_all_docstrings(self, parser: Parser) -> None: 

296 """ 

297 Recursively parse the docstring of this object and its children. 

298 

299 Arguments: 

300 parser: A parser to parse the docstrings. 

301 """ 

302 self.parse_docstring(parser) 

303 for child in self.children: 

304 child.parse_all_docstrings(parser) 

305 

306 @lru_cache() 

307 def has_contents(self) -> bool: 

308 """ 

309 Tells if the object has "contents". 

310 

311 An object has contents when: 

312 

313 - it is the root of the object tree 

314 - it has a docstring 

315 - at least one of its children (whatever the depth) has contents 

316 

317 The value is cached, so this method should be called last, when the tree doesn't change anymore. 

318 

319 Returns: 

320 Whether this object has contents or not. 

321 """ 

322 has_docstring = bool(self.docstring) 

323 is_root = not self.parent 

324 children_have_contents = any(child.has_contents() for child in self.children) 

325 return has_docstring or is_root or children_have_contents 

326 

327 

328class Module(Object): 

329 """A class to store information about a module.""" 

330 

331 possible_name_properties: List[ApplicableNameProperty] = [NAME_SPECIAL, NAME_PRIVATE] 

332 

333 @property 

334 def file_name(self) -> str: 

335 """ 

336 Return the base name of the module file, without the extension. 

337 

338 Returns: 

339 The module file's base name. 

340 """ 

341 return os.path.splitext(os.path.basename(self.file_path))[0] 

342 

343 @property 

344 def name_to_check(self) -> str: # noqa: D102 

345 return self.file_name 

346 

347 

348class Class(Object): 

349 """A class to store information about a class.""" 

350 

351 possible_name_properties: List[ApplicableNameProperty] = [NAME_PRIVATE] 

352 

353 def __init__(self, *args, bases: List[str] = None, **kwargs): 

354 """ 

355 Initialize the object. 

356 

357 Arguments: 

358 *args: Arguments passed to the parent class Initialize the object. 

359 bases: The base classes (dotted paths). 

360 **kwargs: Arguments passed to the parent class Initialize the object. 

361 """ 

362 super().__init__(*args, **kwargs) 

363 self.bases = bases or ["object"] 

364 

365 

366class Function(Object): 

367 """ 

368 A class to store information about a function. 

369 

370 It accepts an additional `signature` argument at instantiation. 

371 """ 

372 

373 possible_name_properties: List[ApplicableNameProperty] = [NAME_PRIVATE] 

374 

375 def __init__(self, *args, signature=None, **kwargs): 

376 """ 

377 Initialize the object. 

378 

379 Arguments: 

380 *args: Arguments passed to the parent class Initialize the object. 

381 signature: The function signature. 

382 **kwargs: Arguments passed to the parent class Initialize the object. 

383 """ 

384 super().__init__(*args, **kwargs) 

385 self.signature = signature 

386 

387 

388class Method(Object): 

389 """ 

390 A class to store information about a method. 

391 

392 It accepts an additional `signature` argument at instantiation. 

393 """ 

394 

395 possible_name_properties: List[ApplicableNameProperty] = [NAME_SPECIAL, NAME_PRIVATE] 

396 

397 def __init__(self, *args, signature=None, **kwargs): 

398 """ 

399 Initialize the object. 

400 

401 Arguments: 

402 *args: Arguments passed to the parent class Initialize the object. 

403 signature: The function signature. 

404 **kwargs: Arguments passed to the parent class Initialize the object. 

405 """ 

406 super().__init__(*args, **kwargs) 

407 self.signature = signature 

408 

409 

410class Attribute(Object): 

411 """ 

412 A class to store information about an attribute. 

413 

414 It accepts an additional `attr_type` argument at instantiation. 

415 """ 

416 

417 possible_name_properties: List[ApplicableNameProperty] = [NAME_SPECIAL, NAME_CLASS_PRIVATE, NAME_PRIVATE] 

418 

419 def __init__(self, *args, attr_type=None, **kwargs): 

420 """ 

421 Initialize the object. 

422 

423 Arguments: 

424 *args: Arguments passed to the parent class Initialize the object. 

425 attr_type: The attribute type. 

426 **kwargs: Arguments passed to the parent class Initialize the object. 

427 """ 

428 super().__init__(*args, **kwargs) 

429 self.type = attr_type