Coverage for src/griffe_inherited_docstrings/_internal/extension.py: 89.61%

43 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-05 17:16 +0200

1from __future__ import annotations 

2 

3import contextlib 

4from typing import TYPE_CHECKING, Any 

5 

6from griffe import AliasResolutionError, Docstring, Extension 

7 

8if TYPE_CHECKING: 

9 from griffe import Module, Object 

10 

11 

12def _docstring_above(obj: Object) -> Docstring | None: 

13 with contextlib.suppress(IndexError, KeyError): 

14 for parent in obj.parent.mro(): # type: ignore[union-attr] 

15 # Fetch docstring from first parent that has the member. 

16 if obj.name in parent.members: 

17 return parent.members[obj.name].docstring 

18 return None 

19 

20 

21def _inherit_docstrings(obj: Object, *, merge: bool = False, seen: set[str] | None = None) -> None: 

22 if seen is None: 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true

23 seen = set() 

24 

25 if obj.path in seen: 

26 return 

27 

28 seen.add(obj.path) 

29 

30 if obj.is_module: 

31 for member in obj.members.values(): 

32 if not member.is_alias: 32 ↛ 31line 32 didn't jump to line 31 because the condition on line 32 was always true

33 with contextlib.suppress(AliasResolutionError): 

34 _inherit_docstrings(member, merge=merge, seen=seen) # type: ignore[arg-type] 

35 

36 elif obj.is_class: 36 ↛ exitline 36 didn't return from function '_inherit_docstrings' because the condition on line 36 was always true

37 # Recursively handle top-most parents first. 

38 # It means that we can just check the first parent with the member 

39 # when actually inheriting (and optionally merging) a docstring, 

40 # since the docstrings of the other parents have already been inherited. 

41 for parent in reversed(obj.mro()): # type: ignore[attr-defined] 

42 _inherit_docstrings(parent, merge=merge, seen=seen) 

43 

44 for member in obj.members.values(): 

45 if not member.is_alias: 45 ↛ 44line 45 didn't jump to line 44 because the condition on line 45 was always true

46 if docstring_above := _docstring_above(member): # type: ignore[arg-type] 

47 if merge: 

48 if member.docstring is None: 

49 member.docstring = Docstring( 

50 docstring_above.value, 

51 parent=member, # type: ignore[arg-type] 

52 parser=docstring_above.parser, 

53 parser_options=docstring_above.parser_options, 

54 ) 

55 elif member.docstring.value: 

56 member.docstring.value = docstring_above.value + "\n\n" + member.docstring.value 

57 else: 

58 member.docstring.value = docstring_above.value 

59 elif member.docstring is None: 59 ↛ 61line 59 didn't jump to line 61 because the condition on line 59 was always true

60 member.docstring = docstring_above 

61 if member.is_class: 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true

62 _inherit_docstrings(member, merge=merge, seen=seen) # type: ignore[arg-type] 

63 

64 

65class InheritDocstringsExtension(Extension): 

66 """Griffe extension for inheriting docstrings.""" 

67 

68 def __init__(self, *, merge: bool = False) -> None: 

69 """Initialize the extension by setting the merge flag. 

70 

71 Parameters: 

72 merge: Whether to merge the docstrings from the parent classes into the docstring of the member. 

73 """ 

74 self.merge = merge 

75 """Whether to merge the docstrings from the parent classes into the docstring of the member.""" 

76 

77 def on_package(self, *, pkg: Module, **kwargs: Any) -> None: # noqa: ARG002 

78 """Inherit docstrings from parent classes once the whole package is loaded.""" 

79 _inherit_docstrings(pkg, merge=self.merge, seen=set())