Coverage for src/pytkdocs/loader.py: 93.48%

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

407 statements  

1""" 

2This module is responsible for loading the documentation from Python objects. 

3 

4It uses [`inspect`](https://docs.python.org/3/library/inspect.html) for introspecting objects, 

5iterating over their members, etc. 

6""" 

7 

8import importlib 

9import inspect 

10import pkgutil 

11import re 

12from functools import lru_cache 

13from itertools import chain 

14from operator import attrgetter 

15from pathlib import Path 

16from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Union 

17 

18from pytkdocs.objects import Attribute, Class, Function, Method, Module, Object, Source 

19from pytkdocs.parsers.attributes import get_class_attributes, get_instance_attributes, get_module_attributes, merge 

20from pytkdocs.parsers.docstrings import PARSERS 

21from pytkdocs.properties import RE_SPECIAL 

22 

23try: 

24 from functools import cached_property # type: ignore 

25except ImportError: 

26 from cached_property import cached_property # type: ignore 

27 

28 

29class ObjectNode: 

30 """ 

31 Helper class to represent an object tree. 

32 

33 It's not really a tree but more a backward-linked list: 

34 each node has a reference to its parent, but not to its child (for simplicity purposes and to avoid bugs). 

35 

36 Each node stores an object, its name, and a reference to its parent node. 

37 """ 

38 

39 def __init__(self, obj: Any, name: str, parent: Optional["ObjectNode"] = None) -> None: 

40 """ 

41 Initialize the object. 

42 

43 Arguments: 

44 obj: A Python object. 

45 name: The object's name. 

46 parent: The object's parent node. 

47 """ 

48 try: 

49 obj = inspect.unwrap(obj) 

50 except Exception: # noqa: S110,W0703 (we purposely catch every possible exception) 

51 # inspect.unwrap at some point runs hasattr(obj, "__wrapped__"), 

52 # which triggers the __getattr__ method of the object, which in 

53 # turn can raise various exceptions. Probably not just __getattr__. 

54 # See https://github.com/pawamoy/pytkdocs/issues/45 

55 pass # noqa: WPS420 (no other way than passing) 

56 

57 self.obj: Any = obj 

58 """The actual Python object.""" 

59 

60 self.name: str = name 

61 """The Python object's name.""" 

62 

63 self.parent: Optional[ObjectNode] = parent 

64 """The parent node.""" 

65 

66 @property 

67 def dotted_path(self) -> str: 

68 """ 

69 Return the Python dotted path to the object. 

70 

71 Returns: 

72 The Python dotted path to the object. 

73 """ 

74 parts = [self.name] 

75 current = self.parent 

76 while current: 

77 parts.append(current.name) 

78 current = current.parent 

79 return ".".join(reversed(parts)) 

80 

81 @property 

82 def file_path(self) -> str: 

83 """ 

84 Return the object's module file path. 

85 

86 Returns: 

87 The object's module file path. 

88 """ 

89 return inspect.getabsfile(self.root.obj) 

90 

91 @property 

92 def root(self) -> "ObjectNode": 

93 """ 

94 Return the root of the tree. 

95 

96 Returns: 

97 The root of the tree. 

98 """ 

99 if self.parent is not None: 

100 return self.parent.root 

101 return self 

102 

103 def is_module(self) -> bool: 

104 """ 

105 Tell if this node's object is a module. 

106 

107 Returns: 

108 The root of the tree. 

109 """ 

110 return inspect.ismodule(self.obj) 

111 

112 def is_class(self) -> bool: 

113 """ 

114 Tell if this node's object is a class. 

115 

116 Returns: 

117 If this node's object is a class. 

118 """ 

119 return inspect.isclass(self.obj) 

120 

121 def is_function(self) -> bool: 

122 """ 

123 Tell if this node's object is a function. 

124 

125 Returns: 

126 If this node's object is a function. 

127 """ 

128 return inspect.isfunction(self.obj) 

129 

130 def is_coroutine_function(self) -> bool: 

131 """ 

132 Tell if this node's object is a coroutine. 

133 

134 Returns: 

135 If this node's object is a coroutine. 

136 """ 

137 return inspect.iscoroutinefunction(self.obj) 

138 

139 def is_property(self) -> bool: 

140 """ 

141 Tell if this node's object is a property. 

142 

143 Returns: 

144 If this node's object is a property. 

145 """ 

146 return isinstance(self.obj, property) or self.is_cached_property() 

147 

148 def is_cached_property(self) -> bool: 

149 """ 

150 Tell if this node's object is a cached property. 

151 

152 Returns: 

153 If this node's object is a cached property. 

154 """ 

155 return isinstance(self.obj, cached_property) 

156 

157 def parent_is_class(self) -> bool: 

158 """ 

159 Tell if the object of this node's parent is a class. 

160 

161 Returns: 

162 If the object of this node's parent is a class. 

163 """ 

164 return bool(self.parent and self.parent.is_class()) 

165 

166 def is_method(self) -> bool: 

167 """ 

168 Tell if this node's object is a method. 

169 

170 Returns: 

171 If this node's object is a method. 

172 """ 

173 function_type = type(lambda: None) 173 ↛ exitline 173 didn't run the lambda on line 173

174 return self.parent_is_class() and isinstance(self.obj, function_type) 

175 

176 def is_method_descriptor(self) -> bool: 

177 """ 

178 Tell if this node's object is a method descriptor. 

179 

180 Built-in methods (e.g. those implemented in C/Rust) are often 

181 method descriptors, rather than normal methods. 

182 

183 Returns: 

184 If this node's object is a method descriptor. 

185 """ 

186 return inspect.ismethoddescriptor(self.obj) 

187 

188 def is_staticmethod(self) -> bool: 

189 """ 

190 Tell if this node's object is a staticmethod. 

191 

192 Returns: 

193 If this node's object is a staticmethod. 

194 """ 

195 if not self.parent: 195 ↛ 196line 195 didn't jump to line 196, because the condition on line 195 was never true

196 return False 

197 self_from_parent = self.parent.obj.__dict__.get(self.name, None) 

198 return self.parent_is_class() and isinstance(self_from_parent, staticmethod) 

199 

200 def is_classmethod(self) -> bool: 

201 """ 

202 Tell if this node's object is a classmethod. 

203 

204 Returns: 

205 If this node's object is a classmethod. 

206 """ 

207 if not self.parent: 207 ↛ 208line 207 didn't jump to line 208, because the condition on line 207 was never true

208 return False 

209 self_from_parent = self.parent.obj.__dict__.get(self.name, None) 

210 return self.parent_is_class() and isinstance(self_from_parent, classmethod) 

211 

212 

213# New path syntax: the new path syntax uses a colon to separate the 

214# modules (to import) from the objects (to get with getattr). 

215# It's easier to deal with, and it naturally improves error handling. 

216# At first, we default to the old syntax, then at some point we will 

217# default to the new syntax, and later again we will drop the old syntax. 

218def get_object_tree(path: str, new_path_syntax: bool = False) -> ObjectNode: 

219 """ 

220 Transform a path into an actual Python object. 

221 

222 The path can be arbitrary long. You can pass the path to a package, 

223 a module, a class, a function or a global variable, as deep as you 

224 want, as long as the deepest module is importable through 

225 `importlib.import_module` and each object is obtainable through 

226 the `getattr` method. It is not possible to load local objects. 

227 

228 Args: 

229 path: The dot/colon-separated path of the object. 

230 new_path_syntax: Whether to use the "colon" syntax for the path. 

231 

232 Raises: 

233 ValueError: When the path is not valid (evaluates to `False`). 

234 ImportError: When the object or its parent module could not be imported. 

235 

236 Returns: 

237 The leaf node representing the object and its parents. 

238 """ 

239 if not path: 

240 raise ValueError(f"path must be a valid Python path, not {path}") 

241 

242 objects: List[str] = [] 

243 

244 if ":" in path or new_path_syntax: 

245 try: 

246 module_path, object_path = path.split(":") 

247 except ValueError: # no colon 

248 module_path, objects = path, [] 

249 else: 

250 objects = object_path.split(".") 

251 

252 # let the ImportError bubble up 

253 parent_module = importlib.import_module(module_path) 

254 

255 else: 

256 # We will try to import the longest dotted-path first. 

257 # If it fails, we remove the right-most part and put it in a list of "objects", used later. 

258 # We loop until we find the deepest importable submodule. 

259 obj_parent_modules = path.split(".") 

260 

261 while True: 

262 parent_module_path = ".".join(obj_parent_modules) 

263 try: 

264 parent_module = importlib.import_module(parent_module_path) 

265 except ImportError as error: 

266 if len(obj_parent_modules) == 1: 

267 raise ImportError( 

268 f"Importing '{path}' failed, possible causes are:\n" 

269 f"- an exception happened while importing\n" 

270 f"- an element in the path does not exist", 

271 ) from error 

272 objects.insert(0, obj_parent_modules.pop(-1)) 

273 else: 

274 break 

275 

276 # We now have the module containing the desired object. 

277 # We will build the object tree by iterating over the previously stored objects names 

278 # and trying to get them as attributes. 

279 current_node = ObjectNode(parent_module, parent_module.__name__) 

280 for obj_name in objects: 

281 obj = getattr(current_node.obj, obj_name) 

282 child = ObjectNode(obj, obj_name, parent=current_node) 

283 current_node = child 

284 

285 leaf = current_node 

286 

287 # We now try to get the "real" parent module, not the one the object was imported into. 

288 # This is important if we want to be able to retrieve the docstring of an attribute for example. 

289 # Once we find an object for which we could get the module, we stop trying to get the module. 

290 # Once we reach the node before the root, we apply the module if found, and break. 

291 real_module = None 

292 while current_node.parent is not None: 

293 if real_module is None: 

294 real_module = inspect.getmodule(current_node.obj) 

295 if inspect.ismodule(current_node.parent.obj): 

296 if real_module is not None and real_module is not current_node.parent.obj: 

297 current_node.parent = ObjectNode(real_module, real_module.__name__) 

298 break 

299 current_node = current_node.parent 

300 

301 return leaf 

302 

303 

304class Loader: 

305 """ 

306 This class contains the object documentation loading mechanisms. 

307 

308 Any error that occurred during collection of the objects and their documentation is stored in the `errors` list. 

309 """ 

310 

311 def __init__( 

312 self, 

313 filters: Optional[List[str]] = None, 

314 docstring_style: str = "google", 

315 docstring_options: Optional[dict] = None, 

316 inherited_members: bool = False, 

317 new_path_syntax: bool = False, 

318 ) -> None: 

319 """ 

320 Initialize the object. 

321 

322 Arguments: 

323 filters: A list of regular expressions to fine-grain select members. It is applied recursively. 

324 docstring_style: The style to use when parsing docstrings. 

325 docstring_options: The options to pass to the docstrings parser. 

326 inherited_members: Whether to select inherited members for classes. 

327 new_path_syntax: Whether to use the "colon" syntax for the path. 

328 """ 

329 if not filters: 

330 filters = [] 

331 

332 self.filters = [(filtr, re.compile(filtr.lstrip("!"))) for filtr in filters] 

333 self.docstring_parser = PARSERS[docstring_style](**(docstring_options or {})) # type: ignore 

334 self.errors: List[str] = [] 

335 self.select_inherited_members = inherited_members 

336 self.new_path_syntax = new_path_syntax 

337 

338 def get_object_documentation(self, dotted_path: str, members: Optional[Union[Set[str], bool]] = None) -> Object: 

339 """ 

340 Get the documentation for an object and its children. 

341 

342 Arguments: 

343 dotted_path: The Python dotted path to the desired object. 

344 members: `True` to select members and filter them, `False` to select no members, 

345 or a list of names to explicitly select the members with these names. 

346 It is applied only on the root object. 

347 

348 Returns: 

349 The documented object. 

350 """ 

351 if members is True: 351 ↛ 352line 351 didn't jump to line 352, because the condition on line 351 was never true

352 members = set() 

353 

354 root_object: Object 

355 leaf = get_object_tree(dotted_path, self.new_path_syntax) 

356 

357 if leaf.is_module(): 

358 root_object = self.get_module_documentation(leaf, members) 

359 elif leaf.is_class(): 

360 root_object = self.get_class_documentation(leaf, members) 

361 elif leaf.is_staticmethod(): 

362 root_object = self.get_staticmethod_documentation(leaf) 

363 elif leaf.is_classmethod(): 

364 root_object = self.get_classmethod_documentation(leaf) 

365 elif leaf.is_method_descriptor(): 

366 root_object = self.get_regular_method_documentation(leaf) 

367 elif leaf.is_method(): 

368 root_object = self.get_regular_method_documentation(leaf) 

369 elif leaf.is_function(): 

370 root_object = self.get_function_documentation(leaf) 

371 elif leaf.is_property(): 

372 root_object = self.get_property_documentation(leaf) 

373 else: 

374 root_object = self.get_attribute_documentation(leaf) 

375 

376 root_object.parse_all_docstrings(self.docstring_parser) 

377 

378 return root_object 

379 

380 def get_module_documentation(self, node: ObjectNode, select_members=None) -> Module: 

381 """ 

382 Get the documentation for a module and its children. 

383 

384 Arguments: 

385 node: The node representing the module and its parents. 

386 select_members: Explicit members to select. 

387 

388 Returns: 

389 The documented module object. 

390 """ 

391 module = node.obj 

392 path = node.dotted_path 

393 name = path.split(".")[-1] 

394 source: Optional[Source] 

395 

396 try: 

397 source = Source(inspect.getsource(module), 1) 

398 except OSError as error: 

399 try: 

400 code = Path(node.file_path).read_text() 

401 except (OSError, UnicodeDecodeError): 

402 source = None 

403 else: 

404 source = Source(code, 1) if code else None 

405 

406 root_object = Module( 

407 name=name, 

408 path=path, 

409 file_path=node.file_path, 

410 docstring=inspect.getdoc(module), 

411 source=source, 

412 ) 

413 

414 if select_members is False: 

415 return root_object 

416 

417 select_members = select_members or set() 

418 

419 attributes_data = get_module_attributes(module) 

420 root_object.parse_docstring(self.docstring_parser, attributes=attributes_data) 

421 

422 for member_name, member in inspect.getmembers(module): 

423 if self.select(member_name, select_members): 

424 child_node = ObjectNode(member, member_name, parent=node) 

425 if child_node.is_class() and node.root.obj is inspect.getmodule(child_node.obj): 

426 root_object.add_child(self.get_class_documentation(child_node)) 

427 elif child_node.is_function() and node.root.obj is inspect.getmodule(child_node.obj): 

428 root_object.add_child(self.get_function_documentation(child_node)) 

429 elif member_name in attributes_data: 

430 root_object.add_child(self.get_attribute_documentation(child_node, attributes_data[member_name])) 

431 

432 if hasattr(module, "__path__"): # noqa: WPS421 (hasattr) 

433 for _, modname, _ in pkgutil.iter_modules(module.__path__): 

434 if self.select(modname, select_members): 434 ↛ 433line 434 didn't jump to line 433, because the condition on line 434 was never false

435 leaf = get_object_tree(f"{path}.{modname}") 

436 root_object.add_child(self.get_module_documentation(leaf)) 

437 

438 return root_object 

439 

440 @staticmethod 

441 def _class_path(cls): 

442 mod = cls.__module__ 

443 qname = cls.__qualname__ 

444 if mod == "builtins": 

445 return qname 

446 else: 

447 return f"{mod}.{qname}" 

448 

449 def get_class_documentation(self, node: ObjectNode, select_members=None) -> Class: 

450 """ 

451 Get the documentation for a class and its children. 

452 

453 Arguments: 

454 node: The node representing the class and its parents. 

455 select_members: Explicit members to select. 

456 

457 Returns: 

458 The documented class object. 

459 """ 

460 class_ = node.obj 

461 docstring = inspect.cleandoc(class_.__doc__ or "") 

462 bases = [self._class_path(b) for b in class_.__bases__] 

463 

464 source: Optional[Source] 

465 

466 try: 

467 source = Source(*inspect.getsourcelines(node.obj)) 

468 except (OSError, TypeError) as error: 

469 source = None 

470 

471 root_object = Class( 

472 name=node.name, 

473 path=node.dotted_path, 

474 file_path=node.file_path, 

475 docstring=docstring, 

476 bases=bases, 

477 source=source, 

478 ) 

479 

480 # Even if we don't select members, we want to correctly parse the docstring 

481 attributes_data: Dict[str, Dict[str, Any]] = {} 

482 for parent_class in reversed(class_.__mro__[:-1]): 

483 merge(attributes_data, get_class_attributes(parent_class)) 

484 context: Dict[str, Any] = {"attributes": attributes_data} 

485 if "__init__" in class_.__dict__: 

486 try: 

487 attributes_data.update(get_instance_attributes(class_.__init__)) 

488 context["signature"] = inspect.signature(class_.__init__) 

489 except (TypeError, ValueError): 

490 pass 

491 root_object.parse_docstring(self.docstring_parser, **context) 

492 

493 if select_members is False: 493 ↛ 494line 493 didn't jump to line 494, because the condition on line 493 was never true

494 return root_object 

495 

496 select_members = select_members or set() 

497 

498 # Build the list of members 

499 members = {} 

500 inherited = set() 

501 direct_members = class_.__dict__ 

502 all_members = dict(inspect.getmembers(class_)) 

503 for member_name, member in all_members.items(): 

504 if member is class_: 504 ↛ 505line 504 didn't jump to line 505, because the condition on line 504 was never true

505 continue 

506 if not (member is type or member is object) and self.select(member_name, select_members): 

507 if member_name not in direct_members: 

508 if self.select_inherited_members: 

509 members[member_name] = member 

510 inherited.add(member_name) 

511 else: 

512 members[member_name] = member 

513 

514 # Iterate on the selected members 

515 child: Object 

516 for member_name, member in members.items(): 

517 child_node = ObjectNode(member, member_name, parent=node) 

518 if child_node.is_class(): 

519 child = self.get_class_documentation(child_node) 

520 elif child_node.is_classmethod(): 

521 child = self.get_classmethod_documentation(child_node) 

522 elif child_node.is_staticmethod(): 

523 child = self.get_staticmethod_documentation(child_node) 

524 elif child_node.is_method(): 

525 child = self.get_regular_method_documentation(child_node) 

526 elif child_node.is_property(): 

527 child = self.get_property_documentation(child_node) 

528 elif member_name in attributes_data: 

529 child = self.get_attribute_documentation(child_node, attributes_data[member_name]) 

530 else: 

531 continue 

532 if member_name in inherited: 

533 child.properties.append("inherited") 

534 root_object.add_child(child) 

535 

536 for attr_name, properties, add_method in ( 

537 ("__fields__", ["pydantic-model"], self.get_pydantic_field_documentation), 

538 ("_declared_fields", ["marshmallow-model"], self.get_marshmallow_field_documentation), 

539 ("_meta.get_fields", ["django-model"], self.get_django_field_documentation), 

540 ("__dataclass_fields__", ["dataclass"], self.get_annotated_dataclass_field), 

541 ): 

542 if self.detect_field_model(attr_name, direct_members, all_members): 

543 root_object.properties.extend(properties) 

544 self.add_fields( 

545 node, 

546 root_object, 

547 attr_name, 

548 all_members, 

549 select_members, 

550 class_, 

551 add_method, 

552 ) 

553 break 

554 

555 return root_object 

556 

557 def detect_field_model(self, attr_name: str, direct_members, all_members) -> bool: 

558 """ 

559 Detect if an attribute is present in members. 

560 

561 Arguments: 

562 attr_name: The name of the attribute to detect, can contain dots. 

563 direct_members: The direct members of the class. 

564 all_members: All members of the class. 

565 

566 Returns: 

567 Whether the attribute is present. 

568 """ 

569 

570 first_order_attr_name, remainder = split_attr_name(attr_name) 

571 if not ( 

572 first_order_attr_name in direct_members 

573 or (self.select_inherited_members and first_order_attr_name in all_members) 

574 ): 

575 return False 

576 

577 if remainder and not attrgetter(remainder)(all_members[first_order_attr_name]): 577 ↛ 578line 577 didn't jump to line 578, because the condition on line 577 was never true

578 return False 

579 return True 

580 

581 def add_fields( 

582 self, 

583 node: ObjectNode, 

584 root_object: Object, 

585 attr_name: str, 

586 members, 

587 select_members, 

588 base_class, 

589 add_method, 

590 ) -> None: 

591 """ 

592 Add detected fields to the current object. 

593 

594 Arguments: 

595 node: The current object node. 

596 root_object: The current object. 

597 attr_name: The fields attribute name. 

598 members: The members to pick the fields attribute in. 

599 select_members: The members to select. 

600 base_class: The class declaring the fields. 

601 add_method: The method to add the children object. 

602 """ 

603 

604 fields = get_fields(attr_name, members=members) 

605 

606 for field_name, field in fields.items(): 

607 select_field = self.select(field_name, select_members) 

608 is_inherited = field_is_inherited(field_name, attr_name, base_class) 

609 

610 if select_field and (self.select_inherited_members or not is_inherited): 

611 child_node = ObjectNode(obj=field, name=field_name, parent=node) 

612 root_object.add_child(add_method(child_node)) 

613 

614 def get_function_documentation(self, node: ObjectNode) -> Function: 

615 """ 

616 Get the documentation for a function. 

617 

618 Arguments: 

619 node: The node representing the function and its parents. 

620 

621 Returns: 

622 The documented function object. 

623 """ 

624 function = node.obj 

625 path = node.dotted_path 

626 source: Optional[Source] 

627 signature: Optional[inspect.Signature] 

628 

629 try: 

630 signature = inspect.signature(function) 

631 except TypeError as error: 

632 signature = None 

633 

634 try: 

635 source = Source(*inspect.getsourcelines(function)) 

636 except OSError as error: 

637 source = None 

638 

639 properties: List[str] = [] 

640 if node.is_coroutine_function(): 

641 properties.append("async") 

642 

643 return Function( 

644 name=node.name, 

645 path=node.dotted_path, 

646 file_path=node.file_path, 

647 docstring=inspect.getdoc(function), 

648 signature=signature, 

649 source=source, 

650 properties=properties, 

651 ) 

652 

653 def get_property_documentation(self, node: ObjectNode) -> Attribute: 

654 """ 

655 Get the documentation for a property. 

656 

657 Arguments: 

658 node: The node representing the property and its parents. 

659 

660 Returns: 

661 The documented attribute object (properties are considered attributes for now). 

662 """ 

663 prop = node.obj 

664 path = node.dotted_path 

665 properties = ["property"] 

666 if node.is_cached_property(): 

667 # cached_property is always writable, see the docs 

668 properties.extend(["writable", "cached"]) 

669 sig_source_func = prop.func 

670 else: 

671 properties.append("readonly" if prop.fset is None else "writable") 

672 sig_source_func = prop.fget 

673 

674 source: Optional[Source] 

675 

676 try: 

677 signature = inspect.signature(sig_source_func) 

678 except (TypeError, ValueError) as error: 

679 attr_type = None 

680 else: 

681 attr_type = signature.return_annotation 

682 

683 try: 

684 source = Source(*inspect.getsourcelines(sig_source_func)) 

685 except (OSError, TypeError) as error: 

686 source = None 

687 

688 return Attribute( 

689 name=node.name, 

690 path=path, 

691 file_path=node.file_path, 

692 docstring=inspect.getdoc(prop), 

693 attr_type=attr_type, 

694 properties=properties, 

695 source=source, 

696 ) 

697 

698 @staticmethod 

699 def get_pydantic_field_documentation(node: ObjectNode) -> Attribute: 

700 """ 

701 Get the documentation for a Pydantic Field. 

702 

703 Arguments: 

704 node: The node representing the Field and its parents. 

705 

706 Returns: 

707 The documented attribute object. 

708 """ 

709 prop = node.obj 

710 path = node.dotted_path 

711 properties = ["pydantic-field"] 

712 if prop.required: 

713 properties.append("required") 

714 

715 return Attribute( 

716 name=node.name, 

717 path=path, 

718 file_path=node.file_path, 

719 docstring=prop.field_info.description, 

720 attr_type=prop.outer_type_, 

721 properties=properties, 

722 ) 

723 

724 @staticmethod 

725 def get_django_field_documentation(node: ObjectNode) -> Attribute: 

726 """ 

727 Get the documentation for a Django Field. 

728 

729 Arguments: 

730 node: The node representing the Field and its parents. 

731 

732 Returns: 

733 The documented attribute object. 

734 """ 

735 prop = node.obj 

736 path = node.dotted_path 

737 properties = ["django-field"] 

738 

739 if prop.null: 739 ↛ 740line 739 didn't jump to line 740, because the condition on line 739 was never true

740 properties.append("nullable") 

741 if prop.blank: 741 ↛ 742line 741 didn't jump to line 742, because the condition on line 741 was never true

742 properties.append("blank") 

743 

744 # set correct docstring based on verbose_name and help_text 

745 # both should be converted to str type in case lazy translation 

746 # is being used, which is common scenario in django 

747 if prop.help_text: 747 ↛ 748line 747 didn't jump to line 748, because the condition on line 747 was never true

748 docstring = f"{prop.verbose_name}: {prop.help_text}" 

749 else: 

750 docstring = str(prop.verbose_name) 

751 

752 return Attribute( 

753 name=node.name, 

754 path=path, 

755 file_path=node.file_path, 

756 docstring=docstring, 

757 attr_type=prop.__class__, 

758 properties=properties, 

759 ) 

760 

761 @staticmethod 

762 def get_marshmallow_field_documentation(node: ObjectNode) -> Attribute: 

763 """ 

764 Get the documentation for a Marshmallow Field. 

765 

766 Arguments: 

767 node: The node representing the Field and its parents. 

768 

769 Returns: 

770 The documented attribute object. 

771 """ 

772 prop = node.obj 

773 path = node.dotted_path 

774 properties = ["marshmallow-field"] 

775 if prop.required: 

776 properties.append("required") 

777 

778 return Attribute( 

779 name=node.name, 

780 path=path, 

781 file_path=node.file_path, 

782 docstring=prop.metadata.get("description"), 

783 attr_type=type(prop), 

784 properties=properties, 

785 ) 

786 

787 @staticmethod 

788 def get_annotated_dataclass_field(node: ObjectNode, attribute_data: Optional[dict] = None) -> Attribute: 

789 """ 

790 Get the documentation for a dataclass field. 

791 

792 Arguments: 

793 node: The node representing the annotation and its parents. 

794 attribute_data: Docstring and annotation for this attribute. 

795 

796 Returns: 

797 The documented attribute object. 

798 """ 

799 if attribute_data is None: 799 ↛ 805line 799 didn't jump to line 805, because the condition on line 799 was never false

800 if node.parent_is_class(): 800 ↛ 803line 800 didn't jump to line 803, because the condition on line 800 was never false

801 attribute_data = get_class_attributes(node.parent.obj).get(node.name, {}) # type: ignore 

802 else: 

803 attribute_data = get_module_attributes(node.root.obj).get(node.name, {}) 

804 

805 return Attribute( 

806 name=node.name, 

807 path=node.dotted_path, 

808 file_path=node.file_path, 

809 docstring=attribute_data["docstring"], 

810 attr_type=attribute_data["annotation"], 

811 properties=["dataclass-field"], 

812 ) 

813 

814 def get_classmethod_documentation(self, node: ObjectNode) -> Method: 

815 """ 

816 Get the documentation for a class-method. 

817 

818 Arguments: 

819 node: The node representing the class-method and its parents. 

820 

821 Returns: 

822 The documented method object. 

823 """ 

824 return self.get_method_documentation(node, ["classmethod"]) 

825 

826 def get_staticmethod_documentation(self, node: ObjectNode) -> Method: 

827 """ 

828 Get the documentation for a static-method. 

829 

830 Arguments: 

831 node: The node representing the static-method and its parents. 

832 

833 Returns: 

834 The documented method object. 

835 """ 

836 return self.get_method_documentation(node, ["staticmethod"]) 

837 

838 def get_regular_method_documentation(self, node: ObjectNode) -> Method: 

839 """ 

840 Get the documentation for a regular method (not class- nor static-method). 

841 

842 We do extra processing in this method to discard docstrings of `__init__` methods 

843 that were inherited from parent classes. 

844 

845 Arguments: 

846 node: The node representing the method and its parents. 

847 

848 Returns: 

849 The documented method object. 

850 """ 

851 method = self.get_method_documentation(node) 

852 if node.parent: 852 ↛ 866line 852 didn't jump to line 866, because the condition on line 852 was never false

853 class_ = node.parent.obj 

854 if RE_SPECIAL.match(node.name): 

855 docstring = method.docstring 

856 parent_classes = class_.__mro__[1:] 

857 for parent_class in parent_classes: 

858 try: 

859 parent_method = getattr(parent_class, node.name) 

860 except AttributeError: 

861 continue 

862 else: 

863 if docstring == inspect.getdoc(parent_method): 

864 method.docstring = "" 

865 break 

866 return method 

867 

868 def get_method_documentation(self, node: ObjectNode, properties: Optional[List[str]] = None) -> Method: 

869 """ 

870 Get the documentation for a method or method descriptor. 

871 

872 Arguments: 

873 node: The node representing the method and its parents. 

874 properties: A list of properties to apply to the method. 

875 

876 Returns: 

877 The documented method object. 

878 """ 

879 method = node.obj 

880 path = node.dotted_path 

881 signature: Optional[inspect.Signature] 

882 source: Optional[Source] 

883 

884 try: 

885 source = Source(*inspect.getsourcelines(method)) 

886 except OSError as error: 

887 source = None 

888 except TypeError: 

889 source = None 

890 

891 if node.is_coroutine_function(): 

892 if properties is None: 892 ↛ 895line 892 didn't jump to line 895, because the condition on line 892 was never false

893 properties = ["async"] 

894 else: 

895 properties.append("async") 

896 

897 try: 

898 # for "built-in" functions, e.g. those implemented in C, 

899 # inspect.signature() uses the __text_signature__ attribute, which 

900 # provides a limited but still useful amount of signature information. 

901 # "built-in" functions with no __text_signature__ will 

902 # raise a ValueError(). 

903 signature = inspect.signature(method) 

904 except ValueError as error: 

905 signature = None 

906 

907 return Method( 

908 name=node.name, 

909 path=path, 

910 file_path=node.file_path, 

911 docstring=inspect.getdoc(method), 

912 signature=signature, 

913 properties=properties or [], 

914 source=source, 

915 ) 

916 

917 @staticmethod 

918 def get_attribute_documentation(node: ObjectNode, attribute_data: Optional[dict] = None) -> Attribute: 

919 """ 

920 Get the documentation for an attribute. 

921 

922 Arguments: 

923 node: The node representing the method and its parents. 

924 attribute_data: Docstring and annotation for this attribute. 

925 

926 Returns: 

927 The documented attribute object. 

928 """ 

929 if attribute_data is None: 

930 if node.parent_is_class(): 

931 attribute_data = get_class_attributes(node.parent.obj).get(node.name, {}) # type: ignore 

932 else: 

933 attribute_data = get_module_attributes(node.root.obj).get(node.name, {}) 

934 return Attribute( 

935 name=node.name, 

936 path=node.dotted_path, 

937 file_path=node.file_path, 

938 docstring=attribute_data.get("docstring", ""), 

939 attr_type=attribute_data.get("annotation", None), 

940 ) 

941 

942 def select(self, name: str, names: Set[str]) -> bool: 

943 """ 

944 Tells whether we should select an object or not, given its name. 

945 

946 If the set of names is not empty, we check against it, otherwise we check against filters. 

947 

948 Arguments: 

949 name: The name of the object to select or not. 

950 names: An explicit list of names to select. 

951 

952 Returns: 

953 Yes or no. 

954 """ 

955 if names: 

956 return name in names 

957 return not self.filter_name_out(name) 

958 

959 @lru_cache(maxsize=None) 

960 def filter_name_out(self, name: str) -> bool: 

961 """ 

962 Filter a name based on the loader's filters. 

963 

964 Arguments: 

965 name: The name to filter. 

966 

967 Returns: 

968 True if the name was filtered out, False otherwise. 

969 """ 

970 if not self.filters: 

971 return False 

972 keep = True 

973 for fltr, regex in self.filters: 

974 is_matching = bool(regex.search(name)) 

975 if is_matching: 

976 if str(fltr).startswith("!"): 

977 is_matching = not is_matching 

978 keep = is_matching 

979 return not keep 

980 

981 

982def field_is_inherited(field_name: str, fields_name: str, base_class: type) -> bool: 

983 """ 

984 Check if a field with a certain name was inherited from parent classes. 

985 

986 Arguments: 

987 field_name: The name of the field to check. 

988 fields_name: The name of the attribute in which the fields are stored. 

989 base_class: The base class in which the field appears. 

990 

991 Returns: 

992 Whether the field was inherited. 

993 """ 

994 # To tell if a field was inherited, we check if it exists in parent classes __fields__ attributes. 

995 # We don't check the current class, nor the top one (object), hence __mro__[1:-1] 

996 return field_name in set( 

997 chain( 

998 *(getattr(parent_class, fields_name, {}).keys() for parent_class in base_class.__mro__[1:-1]), 

999 ), 

1000 ) 

1001 

1002 

1003def split_attr_name(attr_name: str) -> Tuple[str, Optional[str]]: 

1004 """ 

1005 Split an attribute name into a first-order attribute name and remainder. 

1006 

1007 Args: 

1008 attr_name: Attribute name (a) 

1009 

1010 Returns: 

1011 Tuple containing: 

1012 first_order_attr_name: Name of the first order attribute (a) 

1013 remainder: The remainder (b.c) 

1014 

1015 """ 

1016 first_order_attr_name, *remaining = attr_name.split(".", maxsplit=1) 

1017 remainder = remaining[0] if remaining else None 

1018 return first_order_attr_name, remainder 

1019 

1020 

1021def get_fields(attr_name: str, *, members: Mapping[str, Any] = None, class_obj=None) -> Dict[str, Any]: 

1022 if not (bool(members) ^ bool(class_obj)): 1022 ↛ 1023line 1022 didn't jump to line 1023, because the condition on line 1022 was never true

1023 raise ValueError("Either members or class_obj is required.") 

1024 first_order_attr_name, remainder = split_attr_name(attr_name) 

1025 fields = members[first_order_attr_name] if members else dict(vars(class_obj)).get(first_order_attr_name, {}) 

1026 if remainder: 

1027 fields = attrgetter(remainder)(fields) 

1028 

1029 if callable(fields): 

1030 fields = fields() 

1031 

1032 if not isinstance(fields, dict): 

1033 # Support Django models 

1034 fields = {getattr(f, "name", str(f)): f for f in fields if not getattr(f, "auto_created", False)} 

1035 

1036 return fields