diff --git a/pyproject.toml b/pyproject.toml index 489992e..e6eade1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,9 @@ classifiers = [ "Topic :: Utilities", "Typing :: Typed", ] -dependencies = [] +dependencies = [ + "griffe>=0.38", +] [project.urls] Homepage = "https://mkdocstrings.github.io/griffe-inherited-docstrings" diff --git a/src/griffe_inherited_docstrings/__init__.py b/src/griffe_inherited_docstrings/__init__.py index d06bb4f..9d4b7ed 100644 --- a/src/griffe_inherited_docstrings/__init__.py +++ b/src/griffe_inherited_docstrings/__init__.py @@ -5,4 +5,6 @@ from __future__ import annotations -__all__: list[str] = [] +from griffe_inherited_docstrings.extension import InheritDocstringsExtension + +__all__: list[str] = ["InheritDocstringsExtension"] diff --git a/src/griffe_inherited_docstrings/extension.py b/src/griffe_inherited_docstrings/extension.py new file mode 100644 index 0000000..18c2c91 --- /dev/null +++ b/src/griffe_inherited_docstrings/extension.py @@ -0,0 +1,45 @@ +"""The Griffe extension.""" + +from __future__ import annotations + +import contextlib +from typing import TYPE_CHECKING + +from griffe import Extension +from griffe.exceptions import AliasResolutionError + +if TYPE_CHECKING: + from griffe import Docstring, Module, Object + + +def _inherited_docstring(obj: Object) -> Docstring | None: + for parent_class in obj.parent.mro(): # type: ignore[union-attr] + try: + if docstring := parent_class.members[obj.name].docstring: + return docstring + except KeyError: + pass + return None + + +def _inherit_docstrings(obj: Object) -> None: + if obj.is_module: + for member in obj.members.values(): + if not member.is_alias: + with contextlib.suppress(AliasResolutionError): + _inherit_docstrings(member) # type: ignore[arg-type] + elif obj.is_class: + for member in obj.members.values(): + if not member.is_alias: + if member.docstring is None and (inherited := _inherited_docstring(member)): # type: ignore[arg-type] + member.docstring = inherited + if member.is_class: + _inherit_docstrings(member) # type: ignore[arg-type] + + +class InheritDocstringsExtension(Extension): + """Griffe extension for inheriting docstrings.""" + + def on_package_loaded(self, *, pkg: Module) -> None: + """Inherit docstrings from parent classes once the whole package is loaded.""" + _inherit_docstrings(pkg) diff --git a/tests/test_extension.py b/tests/test_extension.py new file mode 100644 index 0000000..e848ba9 --- /dev/null +++ b/tests/test_extension.py @@ -0,0 +1,28 @@ +"""Tests for the extension.""" + +from __future__ import annotations + +from griffe.extensions import Extensions +from griffe.tests import temporary_visited_package + +from griffe_inherited_docstrings import InheritDocstringsExtension + + +def test_inherit_docstrings() -> None: + """Inherit docstrings from parent classes.""" + with temporary_visited_package( + "package", + modules={ + "__init__.py": """ + class Parent: + def method(self): + '''Docstring from parent method.''' + + class Child(Parent): + def method(self): + ... + """, + }, + extensions=Extensions(InheritDocstringsExtension()), + ) as package: + assert package["Child.method"].docstring.value == package["Parent.method"].docstring.value