|
5 | 5 | import contextlib
|
6 | 6 | from typing import TYPE_CHECKING, Any
|
7 | 7 |
|
8 |
| -from griffe import AliasResolutionError, Extension |
| 8 | +from griffe import AliasResolutionError, Docstring, Extension |
9 | 9 |
|
10 | 10 | if TYPE_CHECKING:
|
11 |
| - from griffe import Docstring, Module, Object |
| 11 | + from griffe import Module, Object |
12 | 12 |
|
13 | 13 |
|
14 |
| -def _inherited_docstring(obj: Object) -> Docstring | None: |
15 |
| - for parent_class in obj.parent.mro(): # type: ignore[union-attr] |
16 |
| - try: |
17 |
| - if docstring := parent_class.members[obj.name].docstring: |
18 |
| - return docstring |
19 |
| - except KeyError: |
20 |
| - pass |
| 14 | +def _docstring_above(obj: Object) -> Docstring | None: |
| 15 | + with contextlib.suppress(IndexError, KeyError): |
| 16 | + parent = obj.parent.mro()[0] # type: ignore[union-attr] |
| 17 | + return parent.members[obj.name].docstring |
21 | 18 | return None
|
22 | 19 |
|
23 | 20 |
|
24 |
| -def _inherit_docstrings(obj: Object) -> None: |
| 21 | +def _inherit_docstrings(obj: Object, *, merge: bool = False, seen: set[str] | None = None) -> None: |
| 22 | + if seen is None: |
| 23 | + seen = set() |
| 24 | + |
| 25 | + if obj.path in seen: |
| 26 | + return |
| 27 | + |
| 28 | + seen.add(obj.path) |
| 29 | + |
25 | 30 | if obj.is_module:
|
26 | 31 | for member in obj.members.values():
|
27 | 32 | if not member.is_alias:
|
28 | 33 | with contextlib.suppress(AliasResolutionError):
|
29 |
| - _inherit_docstrings(member) # type: ignore[arg-type] |
| 34 | + _inherit_docstrings(member, merge=merge, seen=seen) # type: ignore[arg-type] |
| 35 | + |
30 | 36 | elif obj.is_class:
|
| 37 | + # Recursively handle top-most parents first. |
| 38 | + # It means that we can just check the first parent |
| 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 | + |
31 | 44 | for member in obj.members.values():
|
32 | 45 | if not member.is_alias:
|
33 |
| - if member.docstring is None and (inherited := _inherited_docstring(member)): # type: ignore[arg-type] |
34 |
| - member.docstring = inherited |
| 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: |
| 60 | + member.docstring = docstring_above |
35 | 61 | if member.is_class:
|
36 |
| - _inherit_docstrings(member) # type: ignore[arg-type] |
| 62 | + _inherit_docstrings(member, merge=merge, seen=seen) # type: ignore[arg-type] |
37 | 63 |
|
38 | 64 |
|
39 | 65 | class InheritDocstringsExtension(Extension):
|
40 | 66 | """Griffe extension for inheriting docstrings."""
|
41 | 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 | + |
42 | 76 | def on_package_loaded(self, *, pkg: Module, **kwargs: Any) -> None: # noqa: ARG002
|
43 | 77 | """Inherit docstrings from parent classes once the whole package is loaded."""
|
44 |
| - _inherit_docstrings(pkg) |
| 78 | + _inherit_docstrings(pkg, merge=self.merge, seen=set()) |
0 commit comments