From 99685d6680a663fce71928d72372690395aa925c Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Wed, 16 Nov 2022 09:43:30 +0100 Subject: [PATCH 01/16] WIP --- .pre-commit-config.yaml | 14 ++--- src/docstring_inheritance/__init__.py | 2 + src/docstring_inheritance/metaclass.py | 52 ++++++++++++++----- src/docstring_inheritance/processors/base.py | 42 ++++++++++----- .../processors/google.py | 10 ++-- src/docstring_inheritance/processors/numpy.py | 47 +++++++---------- 6 files changed, 102 insertions(+), 65 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f2910e..83de87f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -19,7 +19,7 @@ repos: - id: rst-inline-touching-normal - repo: https://github.com/PyCQA/autoflake - rev: v1.7.7 + rev: v2.0.0 hooks: - id: autoflake args: [ @@ -40,7 +40,7 @@ repos: ] - repo: https://github.com/myint/docformatter - rev: v1.5.0 + rev: v1.5.1 hooks: - id: docformatter exclude: ^tests/.*$ @@ -53,22 +53,22 @@ repos: ] - repo: https://github.com/asottile/pyupgrade - rev: v3.2.2 + rev: v3.3.1 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 22.12.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.0.0 hooks: - id: flake8 additional_dependencies: - - flake8-bugbear==22.9.23 + - flake8-bugbear==22.12.6 - flake8-docstrings==1.6.0 - flake8-print==5.0.0 - pep8-naming==0.13.2 diff --git a/src/docstring_inheritance/__init__.py b/src/docstring_inheritance/__init__.py index 02bd2f8..275a145 100644 --- a/src/docstring_inheritance/__init__.py +++ b/src/docstring_inheritance/__init__.py @@ -34,6 +34,7 @@ def DocstringInheritanceMeta( # noqa: N802 docstring_processor: DocstringProcessorType, + __init__in_class_doc: bool = False, ) -> type: metaclass = type( AbstractDocstringInheritanceMeta.__name__, @@ -41,6 +42,7 @@ def DocstringInheritanceMeta( # noqa: N802 dict(AbstractDocstringInheritanceMeta.__dict__), ) metaclass.docstring_processor = docstring_processor + metaclass.init_in_class = __init__in_class_doc return metaclass diff --git a/src/docstring_inheritance/metaclass.py b/src/docstring_inheritance/metaclass.py index 7fe1b07..6807a5f 100644 --- a/src/docstring_inheritance/metaclass.py +++ b/src/docstring_inheritance/metaclass.py @@ -19,9 +19,12 @@ # SOFTWARE. from __future__ import annotations +from copy import copy from types import FunctionType from typing import Any from typing import Callable +from typing import ClassVar +from typing import Iterable def create_dummy_func_with_doc(docstring: str | None) -> Callable: @@ -37,42 +40,45 @@ def func(): # pragma: no cover class AbstractDocstringInheritanceMeta(type): """Abstract metaclass for inheriting class docstrings.""" - docstring_processor: Callable[[str | None, Callable], str] + docstring_processor: ClassVar[Callable[[str | None, Callable], None]] + """The processor of the docstrings.""" + + init_in_class: bool + """Whether the ``__init__`` documentation is in the class docstring.""" def __new__( cls, class_name: str, class_bases: tuple[type], class_dict: dict[str, Any] ) -> AbstractDocstringInheritanceMeta: if class_bases: - cls._process_class_docstring(class_bases, class_dict) - cls._process_attrs_docstrings(class_bases, class_dict) + mro_classes = cls._get_mro_classes(class_bases) + cls._process_class_docstring(mro_classes, class_dict) + cls._process_attrs_docstrings(mro_classes, class_dict) return type.__new__(cls, class_name, class_bases, class_dict) @classmethod def _process_class_docstring( - cls, class_bases: tuple[type], class_dict: dict[str, Any] + cls, mro_classes: list[type], class_dict: dict[str, Any] ) -> None: - dummy_func = create_dummy_func_with_doc(class_dict.get("__doc__")) + func = cls._get_class_dummy_func(mro_classes, class_dict.get("__doc__")) - for base_class in cls._get_mro_classes(class_bases): - cls.docstring_processor(base_class.__doc__, dummy_func) + for mro_cls in mro_classes: + cls.docstring_processor(mro_cls.__doc__, func) - class_dict["__doc__"] = dummy_func.__doc__ + class_dict["__doc__"] = func.__doc__ @classmethod def _process_attrs_docstrings( - cls, class_bases: tuple[type], class_dict: dict[str, Any] + cls, mro_classes: list[type], class_dict: dict[str, Any] ) -> None: - mro_classes = cls._get_mro_classes(class_bases) - for attr_name, attr in class_dict.items(): if not isinstance(attr, FunctionType): continue for mro_cls in mro_classes: - if not hasattr(mro_cls, attr_name): + method = getattr(mro_cls, attr_name, None) + if method is None: continue - - parent_doc = getattr(mro_cls, attr_name).__doc__ + parent_doc = method.__doc__ if parent_doc is not None: break else: @@ -86,3 +92,21 @@ def _get_mro_classes(class_bases: tuple[type]) -> list[type]: # Do not inherit the docstrings from the object base class. mro_classes.remove(object) return mro_classes + + @classmethod + def _get_class_dummy_func( + cls, mro_classes: Iterable[type], docstring: str | None + ) -> Callable[..., None]: + """Return a dummy function with a given docstring. + + If ``cls.ìnit_in_class`` is true then the function is a copy of ``__init__``. + """ + if cls.init_in_class: + for mro_cls in mro_classes: + method = getattr(mro_cls, "__init__") # noqa:B009 + if method is not None: + func = copy(method) + func.__doc__ = docstring + return func + + return create_dummy_func_with_doc(docstring) diff --git a/src/docstring_inheritance/processors/base.py b/src/docstring_inheritance/processors/base.py index 29af4a0..5c2ac35 100644 --- a/src/docstring_inheritance/processors/base.py +++ b/src/docstring_inheritance/processors/base.py @@ -22,8 +22,10 @@ import abc import inspect import sys +from itertools import dropwhile from itertools import tee from typing import Callable +from typing import ClassVar from typing import Dict from typing import Optional from typing import Union @@ -31,9 +33,9 @@ SectionsType = Dict[Optional[str], Union[str, Dict[str, str]]] -if sys.version_info >= (3, 10): # pragma: >=3.10 cover +if sys.version_info >= (3, 10): from itertools import pairwise -else: # pragma: <3.10 cover +else: # See https://docs.python.org/3/library/itertools.html#itertools.pairwise def pairwise(iterable): a, b = tee(iterable) @@ -44,20 +46,36 @@ def pairwise(iterable): class AbstractDocstringProcessor: """Abstract base class for inheriting a docstring.""" - _SECTION_NAMES: list[str | None] - _ARGS_SECTION_ITEMS_NAMES: set[str] - _SECTION_ITEMS_NAMES: set[str] + _SECTION_NAMES: ClassVar[list[str | None]] = [ + None, + "Parameters", + "Returns", + "Yields", + "Receives", + "Other Parameters", + "Attributes", + "Methods", + "Raises", + "Warns", + "Warnings", + "See Also", + "Notes", + "References", + "Examples", + ] + _ARGS_SECTION_ITEMS_NAMES: ClassVar[set[str]] + _SECTION_ITEMS_NAMES: ClassVar[set[str]] # Description without formatting. - MISSING_ARG_DESCRIPTION = "The description is missing." + MISSING_ARG_DESCRIPTION: ClassVar[str] = "The description is missing." @classmethod - @abc.abstractmethod def _get_section_body(cls, reversed_section_body_lines: list[str]) -> str: - """Return the docstring part from reversed lines of a docstring section body. - - The trailing empty lines are removed. - """ + reversed_section_body_lines = list( + dropwhile(lambda x: not x, reversed_section_body_lines) + ) + reversed_section_body_lines.reverse() + return "\n".join(reversed_section_body_lines) @classmethod @abc.abstractmethod @@ -214,7 +232,7 @@ def _inherit_sections( if section_name in temp_sections } - # Add the remaining non standard sections. + # Add the remaining non-standard sections. new_child_sections.update(temp_sections) return new_child_sections diff --git a/src/docstring_inheritance/processors/google.py b/src/docstring_inheritance/processors/google.py index 0123783..7c969d0 100644 --- a/src/docstring_inheritance/processors/google.py +++ b/src/docstring_inheritance/processors/google.py @@ -20,6 +20,7 @@ from __future__ import annotations import textwrap +from typing import ClassVar from . import parse_section_items from .base import AbstractDocstringProcessor @@ -27,12 +28,14 @@ class GoogleDocstringProcessor(AbstractDocstringProcessor): - _SECTION_NAMES = list(NumpyDocstringProcessor._SECTION_NAMES) + _SECTION_NAMES: ClassVar[list[str | None]] = list( + AbstractDocstringProcessor._SECTION_NAMES + ) _SECTION_NAMES[1] = "Args" - _ARGS_SECTION_ITEMS_NAMES = {"Args"} + _ARGS_SECTION_ITEMS_NAMES: ClassVar[set[str]] = {"Args"} - _SECTION_ITEMS_NAMES = _ARGS_SECTION_ITEMS_NAMES | { + _SECTION_ITEMS_NAMES: ClassVar[set[str]] = _ARGS_SECTION_ITEMS_NAMES | { "Attributes", "Methods", } @@ -71,6 +74,7 @@ def _render_section( cls, section_name: str | None, section_body: str | dict[str, str] ) -> str: if section_name is None: + assert isinstance(section_body, str) return section_body if isinstance(section_body, dict): section_body = "\n".join( diff --git a/src/docstring_inheritance/processors/numpy.py b/src/docstring_inheritance/processors/numpy.py index 7976d75..2432315 100644 --- a/src/docstring_inheritance/processors/numpy.py +++ b/src/docstring_inheritance/processors/numpy.py @@ -19,43 +19,27 @@ # SOFTWARE. from __future__ import annotations -from itertools import dropwhile +from typing import ClassVar +from typing import overload from . import parse_section_items from .base import AbstractDocstringProcessor class NumpyDocstringProcessor(AbstractDocstringProcessor): - - _SECTION_NAMES = [ - None, - "Parameters", - "Returns", - "Yields", - "Receives", - "Other Parameters", - "Attributes", - "Methods", - "Raises", - "Warns", - "Warnings", - "See Also", - "Notes", - "References", - "Examples", - ] - - _ARGS_SECTION_ITEMS_NAMES = { + _ARGS_SECTION_ITEMS_NAMES: ClassVar[set[str]] = { "Parameters", "Other Parameters", } - _SECTION_ITEMS_NAMES = _ARGS_SECTION_ITEMS_NAMES | { + _SECTION_ITEMS_NAMES: ClassVar[set[str]] = _ARGS_SECTION_ITEMS_NAMES | { "Attributes", "Methods", } - MISSING_ARG_DESCRIPTION = f":\n{AbstractDocstringProcessor.MISSING_ARG_DESCRIPTION}" + MISSING_ARG_DESCRIPTION: ClassVar[ + str + ] = f":\n{AbstractDocstringProcessor.MISSING_ARG_DESCRIPTION}" @classmethod def _parse_section_items(cls, section_body: str) -> dict[str, str]: @@ -75,19 +59,24 @@ def _parse_one_section( return line1s, cls._get_section_body(reversed_section_body_lines) return None, None + @overload + @classmethod + def _render_section(cls, section_name: None, section_body: str) -> str: + ... + + @overload @classmethod - def _get_section_body(cls, reversed_section_body_lines: list[str]) -> str: - reversed_section_body_lines = list( - dropwhile(lambda x: not x, reversed_section_body_lines) - ) - reversed_section_body_lines.reverse() - return "\n".join(reversed_section_body_lines) + def _render_section( + cls, section_name: str, section_body: str | dict[str, str] + ) -> str: + ... @classmethod def _render_section( cls, section_name: str | None, section_body: str | dict[str, str] ) -> str: if section_name is None: + assert isinstance(section_body, str) return section_body if isinstance(section_body, dict): section_body = "\n".join( From e3ab316799252f5408cdae918f0f7be45a73201d Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Sun, 18 Dec 2022 21:15:30 +0100 Subject: [PATCH 02/16] Update to tox4 --- tox.ini | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index 9d5135f..ead6ea3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,12 @@ [tox] -minversion = 3.20.0 -isolated_build = true -envlist = py{37,38,39,310,311} - +min_version = 4 +env_list = py{37,38,39,310,311} [testenv] +package = wheel deps = -r requirements/test.txt -setenv = +set_env = coverage: __COVERAGE_POSARGS=--cov --cov-config=setup.cfg --cov-report=xml --cov-report=html commands = pytest {env:__COVERAGE_POSARGS:} {posargs} @@ -28,8 +27,8 @@ commands = [testenv:update-deps-test] description = update the test envs dependencies -basepython = python3.7 -setenv = +base_python = python3.7 +set_env = deps = pip-tools skip_install = true commands = From 64f6c2e0324a1d7dfa3b4713bb14745ab3e24de9 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Sun, 18 Dec 2022 21:15:41 +0100 Subject: [PATCH 03/16] Update to test deps --- requirements/test.txt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/requirements/test.txt b/requirements/test.txt index 02a0710..9782098 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # pip-compile --extra=test --output-file=requirements/test.txt # attrs==22.1.0 # via pytest -covdefaults==2.2.0 +covdefaults==2.2.2 # via docstring-inheritance (setup.py) coverage[toml]==6.5.0 # via @@ -14,18 +14,16 @@ coverage[toml]==6.5.0 # pytest-cov exceptiongroup==1.0.4 # via pytest -importlib-metadata==5.0.0 +importlib-metadata==5.1.0 # via # pluggy # pytest iniconfig==1.1.1 # via pytest -packaging==21.3 +packaging==22.0 # via pytest pluggy==1.0.0 # via pytest -pyparsing==3.0.9 - # via packaging pytest==7.2.0 # via # docstring-inheritance (setup.py) @@ -38,5 +36,5 @@ tomli==2.0.1 # pytest typing-extensions==4.4.0 # via importlib-metadata -zipp==3.10.0 +zipp==3.11.0 # via importlib-metadata From e534aac11633d3a6d51a89a7e824600874ff4c81 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Sun, 1 Jan 2023 12:15:12 +0100 Subject: [PATCH 04/16] Renaming and docs --- src/docstring_inheritance/metaclass.py | 82 +++++++++++++------- src/docstring_inheritance/processors/base.py | 11 ++- 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/docstring_inheritance/metaclass.py b/src/docstring_inheritance/metaclass.py index 6807a5f..599620a 100644 --- a/src/docstring_inheritance/metaclass.py +++ b/src/docstring_inheritance/metaclass.py @@ -28,9 +28,16 @@ def create_dummy_func_with_doc(docstring: str | None) -> Callable: - """Return a dummy function with a given docstring.""" + """Create a dummy function with a given docstring. - def func(): # pragma: no cover + Args: + docstring: The docstring to be assigned. + + Returns: + The function with the given docstring. + """ + + def func() -> None: # pragma: no cover pass func.__doc__ = docstring @@ -43,39 +50,69 @@ class AbstractDocstringInheritanceMeta(type): docstring_processor: ClassVar[Callable[[str | None, Callable], None]] """The processor of the docstrings.""" - init_in_class: bool - """Whether the ``__init__`` documentation is in the class docstring.""" + init_in_class: ClassVar[bool] + """Whether the ``__init__`` arguments documentation is in the class docstring.""" def __new__( cls, class_name: str, class_bases: tuple[type], class_dict: dict[str, Any] ) -> AbstractDocstringInheritanceMeta: if class_bases: - mro_classes = cls._get_mro_classes(class_bases) - cls._process_class_docstring(mro_classes, class_dict) - cls._process_attrs_docstrings(mro_classes, class_dict) + classes = cls._get_classes_mro(class_bases) + cls._inherit_class_docstring(classes, class_dict) + cls._inherit_attrs_docstrings(classes, class_dict) return type.__new__(cls, class_name, class_bases, class_dict) + @staticmethod + def _get_classes_mro(classes: tuple[type]) -> list[type]: + """Sort the classes according to the Method Resolution Order. + + The object class is removed because inheriting its docstring is useless. + + Args: + classes: The classes to sort. + + Returns: + The classes. + """ + classes = list( + dict.fromkeys([cls for base in classes for cls in base.__mro__]) + ) + classes.remove(object) + return classes + @classmethod - def _process_class_docstring( - cls, mro_classes: list[type], class_dict: dict[str, Any] + def _inherit_class_docstring( + cls, classes: list[type], class_dict: dict[str, Any] ) -> None: - func = cls._get_class_dummy_func(mro_classes, class_dict.get("__doc__")) + """Create the inherited docstring for the class docstring. + + Args: + classes: The classes to inherit from. + class_dict: The class definition. + """ + func = cls._get_class_dummy_func(classes, class_dict.get("__doc__")) - for mro_cls in mro_classes: - cls.docstring_processor(mro_cls.__doc__, func) + for cls_ in classes: + cls.docstring_processor(cls_.__doc__, func) class_dict["__doc__"] = func.__doc__ @classmethod - def _process_attrs_docstrings( - cls, mro_classes: list[type], class_dict: dict[str, Any] + def _inherit_attrs_docstrings( + cls, classes: list[type], class_dict: dict[str, Any] ) -> None: + """Create the inherited docstrings for the class attributes. + + Args: + classes: The classes to inherit from. + class_dict: The class definition. + """ for attr_name, attr in class_dict.items(): if not isinstance(attr, FunctionType): continue - for mro_cls in mro_classes: - method = getattr(mro_cls, attr_name, None) + for cls_ in classes: + method = getattr(cls_, attr_name, None) if method is None: continue parent_doc = method.__doc__ @@ -86,24 +123,17 @@ def _process_attrs_docstrings( cls.docstring_processor(parent_doc, attr) - @staticmethod - def _get_mro_classes(class_bases: tuple[type]) -> list[type]: - mro_classes = [mro_cls for base in class_bases for mro_cls in base.mro()] - # Do not inherit the docstrings from the object base class. - mro_classes.remove(object) - return mro_classes - @classmethod def _get_class_dummy_func( - cls, mro_classes: Iterable[type], docstring: str | None + cls, classes: Iterable[type], docstring: str | None ) -> Callable[..., None]: """Return a dummy function with a given docstring. If ``cls.ìnit_in_class`` is true then the function is a copy of ``__init__``. """ if cls.init_in_class: - for mro_cls in mro_classes: - method = getattr(mro_cls, "__init__") # noqa:B009 + for cls_ in classes: + method = getattr(cls_, "__init__") # noqa:B009 if method is not None: func = copy(method) func.__doc__ = docstring diff --git a/src/docstring_inheritance/processors/base.py b/src/docstring_inheritance/processors/base.py index 5c2ac35..7c027ef 100644 --- a/src/docstring_inheritance/processors/base.py +++ b/src/docstring_inheritance/processors/base.py @@ -37,7 +37,7 @@ from itertools import pairwise else: # See https://docs.python.org/3/library/itertools.html#itertools.pairwise - def pairwise(iterable): + def pairwise(iterable): # pragma: no cover a, b = tee(iterable) next(b, None) return zip(a, b) @@ -66,7 +66,6 @@ class AbstractDocstringProcessor: _ARGS_SECTION_ITEMS_NAMES: ClassVar[set[str]] _SECTION_ITEMS_NAMES: ClassVar[set[str]] - # Description without formatting. MISSING_ARG_DESCRIPTION: ClassVar[str] = "The description is missing." @classmethod @@ -88,7 +87,7 @@ def _parse_one_section( Returns: The name and docstring body parts of a section, - or `(None, None)` if no section is found. + or ``(None, None)`` if no section is found. """ @classmethod @@ -245,9 +244,9 @@ def _inherit_section_items_with_args( ) -> dict[str, str]: """Inherit section items for the args of a signature. - The argument `self` is removed. The arguments are ordered according to the - signature of `func`. An argument of `func` missing in `section_items` gets a - default description defined in :attr:`.MISSING_ARG_DESCRIPTION`. + The argument ``self`` is removed. The arguments are ordered according to the + signature of ``func``. An argument of ``func`` missing in ``section_items`` gets + a default description defined in :attr:`.MISSING_ARG_DESCRIPTION`. """ args, varargs, varkw, _, kwonlyargs = inspect.getfullargspec(func)[:5] From c507f7953be377415c3903ce88441f12bc5c77df Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Sun, 1 Jan 2023 18:44:56 +0100 Subject: [PATCH 05/16] Use standard MRO --- src/docstring_inheritance/metaclass.py | 36 ++++++++------------------ 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/src/docstring_inheritance/metaclass.py b/src/docstring_inheritance/metaclass.py index 599620a..02b5c31 100644 --- a/src/docstring_inheritance/metaclass.py +++ b/src/docstring_inheritance/metaclass.py @@ -56,33 +56,19 @@ class AbstractDocstringInheritanceMeta(type): def __new__( cls, class_name: str, class_bases: tuple[type], class_dict: dict[str, Any] ) -> AbstractDocstringInheritanceMeta: + new_cls = type.__new__(cls, class_name, class_bases, class_dict) if class_bases: - classes = cls._get_classes_mro(class_bases) - cls._inherit_class_docstring(classes, class_dict) - cls._inherit_attrs_docstrings(classes, class_dict) - return type.__new__(cls, class_name, class_bases, class_dict) - - @staticmethod - def _get_classes_mro(classes: tuple[type]) -> list[type]: - """Sort the classes according to the Method Resolution Order. - - The object class is removed because inheriting its docstring is useless. - - Args: - classes: The classes to sort. - - Returns: - The classes. - """ - classes = list( - dict.fromkeys([cls for base in classes for cls in base.__mro__]) - ) - classes.remove(object) - return classes + # Remove the class itself and object from the mro. + mro_classes = new_cls.mro()[1:-1] + cls._inherit_class_docstring(mro_classes, new_cls) + cls._inherit_attrs_docstrings(mro_classes, new_cls.__dict__) + return new_cls @classmethod def _inherit_class_docstring( - cls, classes: list[type], class_dict: dict[str, Any] + cls, + classes: list[type], + class_: type, ) -> None: """Create the inherited docstring for the class docstring. @@ -90,12 +76,12 @@ def _inherit_class_docstring( classes: The classes to inherit from. class_dict: The class definition. """ - func = cls._get_class_dummy_func(classes, class_dict.get("__doc__")) + func = cls._get_class_dummy_func(classes, class_.__doc__) for cls_ in classes: cls.docstring_processor(cls_.__doc__, func) - class_dict["__doc__"] = func.__doc__ + class_.__doc__ = func.__doc__ @classmethod def _inherit_attrs_docstrings( From dce79c565e04e02603db17ea76f0147d0e2beb9e Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Mon, 2 Jan 2023 20:26:14 +0100 Subject: [PATCH 06/16] WIP --- README.md | 123 +++++++++-------- src/docstring_inheritance/__init__.py | 66 +++++---- src/docstring_inheritance/class_processor.py | 127 +++++++++++++++++ .../__init__.py | 9 -- .../base.py | 15 +- .../google.py | 5 - .../numpy.py | 5 - src/docstring_inheritance/metaclass.py | 128 ------------------ tests/test_base_processor.py | 8 +- tests/test_inheritance_for_functions.py | 14 +- tests/test_numpy_processor.py | 2 +- 11 files changed, 254 insertions(+), 248 deletions(-) create mode 100644 src/docstring_inheritance/class_processor.py rename src/docstring_inheritance/{processors => docstring_processors}/__init__.py (79%) rename src/docstring_inheritance/{processors => docstring_processors}/base.py (97%) rename src/docstring_inheritance/{processors => docstring_processors}/google.py (94%) rename src/docstring_inheritance/{processors => docstring_processors}/numpy.py (94%) delete mode 100644 src/docstring_inheritance/metaclass.py diff --git a/README.md b/README.md index ba9f0c8..0f5e06a 100644 --- a/README.md +++ b/README.md @@ -76,38 +76,38 @@ from docstring_inheritance import NumpyDocstringInheritanceMeta class Parent(metaclass=NumpyDocstringInheritanceMeta): - def meth(self, x, y=None): - """Parent summary. - - Parameters - ---------- - x: - Description for x. - y: - Description for y. - - Notes - ----- - Parent notes. - """ + def meth(self, x, y=None): + """Parent summary. + + Parameters + ---------- + x: + Description for x. + y: + Description for y. + + Notes + ----- + Parent notes. + """ class Child(Parent): - def meth(self, x, z): - """ - Parameters - ---------- - z: - Description for z. - - Returns - ------- - Something. + def meth(self, x, z): + """ + Parameters + ---------- + z: + Description for z. - Notes - ----- - Child notes. - """ + Returns + ------- + Something. + + Notes + ----- + Child notes. + """ # The inherited docstring is @@ -140,7 +140,7 @@ Use the `inherit_google_docstring` function to inherit docstrings in google form Use the `inherit_numpy_docstring` function to inherit docstrings in numpy format. ```python -from docstring_inheritance import inherit_google_docstring +from docstring_inheritance import process_google_docstring def parent(): @@ -168,8 +168,7 @@ def child(): """ -inherit_google_docstring(parent.__doc__, child) - +process_google_docstring(parent.__doc__, child) # The inherited docstring is child.__doc__ = """Parent summary. @@ -236,25 +235,25 @@ from docstring_inheritance import NumpyDocstringInheritanceMeta class Parent(metaclass=NumpyDocstringInheritanceMeta): - """ - Attributes - ---------- - x: - Description for x - y: - Description for y - """ + """ + Attributes + ---------- + x: + Description for x + y: + Description for y + """ class Child(Parent): - """ - Attributes - ---------- - y: - Overridden description for y - z: - Description for z - """ + """ + Attributes + ---------- + y: + Overridden description for y + z: + Description for z + """ # The inherited docstring is @@ -291,22 +290,22 @@ from docstring_inheritance import GoogleDocstringInheritanceMeta class Parent(metaclass=GoogleDocstringInheritanceMeta): - def meth(self, w, x, y): - """ - Args: - w: Description for w - x: Description for x - y: Description for y - """ + def meth(self, w, x, y): + """ + Args: + w: Description for w + x: Description for x + y: Description for y + """ class Child(Parent): - def meth(self, w, y, z): - """ - Args: - z: Description for z - y: Overridden description for y - """ + def meth(self, w, y, z): + """ + Args: + z: Description for z + y: Overridden description for y + """ # The inherited docstring is @@ -336,11 +335,11 @@ from docstring_inheritance import NumpyDocstringInheritanceMeta class Meta(abc.ABCMeta, NumpyDocstringInheritanceMeta): - pass + pass class Parent(metaclass=Meta): - pass + pass ``` # Similar projects diff --git a/src/docstring_inheritance/__init__.py b/src/docstring_inheritance/__init__.py index 275a145..79c8c92 100644 --- a/src/docstring_inheritance/__init__.py +++ b/src/docstring_inheritance/__init__.py @@ -19,33 +19,53 @@ # SOFTWARE. from __future__ import annotations -from typing import Callable -from typing import Optional +from typing import Any -from .metaclass import AbstractDocstringInheritanceMeta -from .processors.google import GoogleDocstringProcessor -from .processors.numpy import NumpyDocstringProcessor +from .class_processor import ClassDocstringsInheritor +from .class_processor import DocstringProcessorType +from .docstring_processors.google import GoogleDocstringProcessor +from .docstring_processors.numpy import NumpyDocstringProcessor -DocstringProcessorType = Callable[[Optional[str], Callable], None] +process_numpy_docstring = NumpyDocstringProcessor() +process_google_docstring = GoogleDocstringProcessor() -inherit_numpy_docstring = NumpyDocstringProcessor() -inherit_google_docstring = GoogleDocstringProcessor() +class _BaseDocstringInheritanceMeta(type): + """Metaclass for inheriting class docstrings with a docstring processor.""" -def DocstringInheritanceMeta( # noqa: N802 - docstring_processor: DocstringProcessorType, - __init__in_class_doc: bool = False, -) -> type: - metaclass = type( - AbstractDocstringInheritanceMeta.__name__, - AbstractDocstringInheritanceMeta.__bases__, - dict(AbstractDocstringInheritanceMeta.__dict__), - ) - metaclass.docstring_processor = docstring_processor - metaclass.init_in_class = __init__in_class_doc - return metaclass + def __init__( + cls, + class_name: str, + class_bases: tuple[type], + class_dict: dict[str, Any], + docstring_processor: DocstringProcessorType, + ) -> None: + super().__init__(class_name, class_bases, class_dict) + if class_bases: + inheritor = ClassDocstringsInheritor(cls, docstring_processor) + inheritor.inherit_class_docstring() + inheritor.inherit_attrs_docstrings() -# Helper metaclasses for each docstring styles. -NumpyDocstringInheritanceMeta = DocstringInheritanceMeta(inherit_numpy_docstring) -GoogleDocstringInheritanceMeta = DocstringInheritanceMeta(inherit_google_docstring) +class GoogleDocstringInheritanceMeta(_BaseDocstringInheritanceMeta): + """Metaclass for inheriting docstrings in Google format.""" + + def __init__( + self, + class_name: str, + class_bases: tuple[type], + class_dict: dict[str, Any], + ) -> None: + super().__init__(class_name, class_bases, class_dict, process_google_docstring) + + +class NumpyDocstringInheritanceMeta(_BaseDocstringInheritanceMeta): + """Metaclass for inheriting docstrings in Numpy format.""" + + def __init__( + self, + class_name: str, + class_bases: tuple[type], + class_dict: dict[str, Any], + ) -> None: + super().__init__(class_name, class_bases, class_dict, process_numpy_docstring) diff --git a/src/docstring_inheritance/class_processor.py b/src/docstring_inheritance/class_processor.py new file mode 100644 index 0000000..9ceffbc --- /dev/null +++ b/src/docstring_inheritance/class_processor.py @@ -0,0 +1,127 @@ +# Copyright 2021 Antoine DECHAUME +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from __future__ import annotations + +from copy import copy +from types import FunctionType +from typing import Callable +from typing import Optional + +DocstringProcessorType = Callable[[Optional[str], Callable], None] + + +class ClassDocstringsInheritor: + """Processor for inheriting class docstrings.""" + + _cls: type + """The class to process.""" + + _processor: DocstringProcessorType + """The docstring processor.""" + + _init_in_class: bool + """Whether the ``__init__`` arguments documentation is in the class docstring.""" + + __mro_classes: list[type] + """The MRO classes.""" + + def __init__( + self, + cls: type, + docstring_processor: DocstringProcessorType, + ) -> None: + """ + Args: + cls: The class to process. + docstring_processor: The docstring processor. + """ + # Remove the new class itself and the object class from the mro. + self.__mro_classes = cls.mro()[1:-1] + self._cls = cls + self._processor = docstring_processor + self._init_in_class = False + + def inherit_class_docstring( + self, + ) -> None: + """Create the inherited docstring for the class docstring.""" + func = self._get_class_dummy_func() + + for cls_ in self.__mro_classes: + self._processor(cls_.__doc__, func) + + self._cls.__doc__ = func.__doc__ + + def inherit_attrs_docstrings( + self, + ) -> None: + """Create the inherited docstrings for the class attributes.""" + for attr_name, attr in self._cls.__dict__.items(): + if not isinstance(attr, FunctionType): + continue + + for cls_ in self.__mro_classes: + method = getattr(cls_, attr_name, None) + if method is None: + continue + parent_doc = method.__doc__ + if parent_doc is not None: + break + else: + continue + + self._processor(parent_doc, attr) + + def _get_class_dummy_func( + self, + ) -> Callable[..., None]: + """Return a dummy function with a given docstring. + + If ``self._ìnit_in_class`` is true then the function is a copy of ``__init__``. + + Returns: + The function with the class docstring. + """ + if self._init_in_class: + for cls_ in self.__mro_classes: + method = getattr(cls_, "__init__") # noqa:B009 + if method is not None: + func = copy(method) + func.__doc__ = self._cls.__doc__ + return func + + return self._create_dummy_func_with_doc(self._cls.__doc__) + + @staticmethod + def _create_dummy_func_with_doc(docstring: str | None) -> Callable: + """Create a dummy function with a given docstring. + + Args: + docstring: The docstring to be assigned. + + Returns: + The function with the given docstring. + """ + + def func() -> None: # pragma: no cover + pass + + func.__doc__ = docstring + return func diff --git a/src/docstring_inheritance/processors/__init__.py b/src/docstring_inheritance/docstring_processors/__init__.py similarity index 79% rename from src/docstring_inheritance/processors/__init__.py rename to src/docstring_inheritance/docstring_processors/__init__.py index 0c498e2..67879f8 100644 --- a/src/docstring_inheritance/processors/__init__.py +++ b/src/docstring_inheritance/docstring_processors/__init__.py @@ -18,12 +18,3 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations - -import re - -_SECTION_ITEMS_REGEX = re.compile(r"(\**\w+)(.*?)(?:$|(?=\n\**\w+))", flags=re.DOTALL) - - -def parse_section_items(section_body: str) -> dict[str, str]: - """Parse the section items for numpy and google docstrings.""" - return dict(_SECTION_ITEMS_REGEX.findall(section_body)) diff --git a/src/docstring_inheritance/processors/base.py b/src/docstring_inheritance/docstring_processors/base.py similarity index 97% rename from src/docstring_inheritance/processors/base.py rename to src/docstring_inheritance/docstring_processors/base.py index 7c027ef..748be31 100644 --- a/src/docstring_inheritance/processors/base.py +++ b/src/docstring_inheritance/docstring_processors/base.py @@ -21,6 +21,7 @@ import abc import inspect +import re import sys from itertools import dropwhile from itertools import tee @@ -97,11 +98,6 @@ def _render_section( ) -> str: """Return a rendered docstring section.""" - @classmethod - @abc.abstractmethod - def _parse_section_items(cls, section_body: str) -> dict[str, str]: - """Return the section items names bound to their descriptions.""" - def __call__(self, parent_doc: str | None, child_func: Callable) -> None: if parent_doc is None: return @@ -289,3 +285,12 @@ def _render_docstring(cls, sections: SectionsType) -> str: return "\n" + rendered return rendered + + _SECTION_ITEMS_REGEX = re.compile( + r"(\**\w+)(.*?)(?:$|(?=\n\**\w+))", flags=re.DOTALL + ) + + @classmethod + def _parse_section_items(cls, section_body: str) -> dict[str, str]: + """Parse the section items for numpy and google docstrings.""" + return dict(cls._SECTION_ITEMS_REGEX.findall(section_body)) diff --git a/src/docstring_inheritance/processors/google.py b/src/docstring_inheritance/docstring_processors/google.py similarity index 94% rename from src/docstring_inheritance/processors/google.py rename to src/docstring_inheritance/docstring_processors/google.py index 7c969d0..14c3af1 100644 --- a/src/docstring_inheritance/processors/google.py +++ b/src/docstring_inheritance/docstring_processors/google.py @@ -22,7 +22,6 @@ import textwrap from typing import ClassVar -from . import parse_section_items from .base import AbstractDocstringProcessor from .numpy import NumpyDocstringProcessor @@ -42,10 +41,6 @@ class GoogleDocstringProcessor(AbstractDocstringProcessor): MISSING_ARG_DESCRIPTION = f": {AbstractDocstringProcessor.MISSING_ARG_DESCRIPTION}" - @classmethod - def _parse_section_items(cls, section_body: str) -> dict[str, str]: - return parse_section_items(section_body) - @classmethod def _get_section_body(cls, reversed_section_body_lines: list[str]) -> str: return textwrap.dedent( diff --git a/src/docstring_inheritance/processors/numpy.py b/src/docstring_inheritance/docstring_processors/numpy.py similarity index 94% rename from src/docstring_inheritance/processors/numpy.py rename to src/docstring_inheritance/docstring_processors/numpy.py index 2432315..79192b4 100644 --- a/src/docstring_inheritance/processors/numpy.py +++ b/src/docstring_inheritance/docstring_processors/numpy.py @@ -22,7 +22,6 @@ from typing import ClassVar from typing import overload -from . import parse_section_items from .base import AbstractDocstringProcessor @@ -41,10 +40,6 @@ class NumpyDocstringProcessor(AbstractDocstringProcessor): str ] = f":\n{AbstractDocstringProcessor.MISSING_ARG_DESCRIPTION}" - @classmethod - def _parse_section_items(cls, section_body: str) -> dict[str, str]: - return parse_section_items(section_body) - @classmethod def _parse_one_section( cls, line1: str, line2_rstripped: str, reversed_section_body_lines: list[str] diff --git a/src/docstring_inheritance/metaclass.py b/src/docstring_inheritance/metaclass.py deleted file mode 100644 index 02b5c31..0000000 --- a/src/docstring_inheritance/metaclass.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright 2021 Antoine DECHAUME -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -# of the Software, and to permit persons to whom the Software is furnished to do -# so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -from __future__ import annotations - -from copy import copy -from types import FunctionType -from typing import Any -from typing import Callable -from typing import ClassVar -from typing import Iterable - - -def create_dummy_func_with_doc(docstring: str | None) -> Callable: - """Create a dummy function with a given docstring. - - Args: - docstring: The docstring to be assigned. - - Returns: - The function with the given docstring. - """ - - def func() -> None: # pragma: no cover - pass - - func.__doc__ = docstring - return func - - -class AbstractDocstringInheritanceMeta(type): - """Abstract metaclass for inheriting class docstrings.""" - - docstring_processor: ClassVar[Callable[[str | None, Callable], None]] - """The processor of the docstrings.""" - - init_in_class: ClassVar[bool] - """Whether the ``__init__`` arguments documentation is in the class docstring.""" - - def __new__( - cls, class_name: str, class_bases: tuple[type], class_dict: dict[str, Any] - ) -> AbstractDocstringInheritanceMeta: - new_cls = type.__new__(cls, class_name, class_bases, class_dict) - if class_bases: - # Remove the class itself and object from the mro. - mro_classes = new_cls.mro()[1:-1] - cls._inherit_class_docstring(mro_classes, new_cls) - cls._inherit_attrs_docstrings(mro_classes, new_cls.__dict__) - return new_cls - - @classmethod - def _inherit_class_docstring( - cls, - classes: list[type], - class_: type, - ) -> None: - """Create the inherited docstring for the class docstring. - - Args: - classes: The classes to inherit from. - class_dict: The class definition. - """ - func = cls._get_class_dummy_func(classes, class_.__doc__) - - for cls_ in classes: - cls.docstring_processor(cls_.__doc__, func) - - class_.__doc__ = func.__doc__ - - @classmethod - def _inherit_attrs_docstrings( - cls, classes: list[type], class_dict: dict[str, Any] - ) -> None: - """Create the inherited docstrings for the class attributes. - - Args: - classes: The classes to inherit from. - class_dict: The class definition. - """ - for attr_name, attr in class_dict.items(): - if not isinstance(attr, FunctionType): - continue - - for cls_ in classes: - method = getattr(cls_, attr_name, None) - if method is None: - continue - parent_doc = method.__doc__ - if parent_doc is not None: - break - else: - continue - - cls.docstring_processor(parent_doc, attr) - - @classmethod - def _get_class_dummy_func( - cls, classes: Iterable[type], docstring: str | None - ) -> Callable[..., None]: - """Return a dummy function with a given docstring. - - If ``cls.ìnit_in_class`` is true then the function is a copy of ``__init__``. - """ - if cls.init_in_class: - for cls_ in classes: - method = getattr(cls_, "__init__") # noqa:B009 - if method is not None: - func = copy(method) - func.__doc__ = docstring - return func - - return create_dummy_func_with_doc(docstring) diff --git a/tests/test_base_processor.py b/tests/test_base_processor.py index 9529796..16fa73d 100644 --- a/tests/test_base_processor.py +++ b/tests/test_base_processor.py @@ -23,8 +23,7 @@ import pytest -from docstring_inheritance.processors import parse_section_items -from docstring_inheritance.processors.base import AbstractDocstringProcessor +from docstring_inheritance.docstring_processors.base import AbstractDocstringProcessor def _test_parse_sections(parse_sections, unindented_docstring, expected_sections): @@ -53,7 +52,10 @@ def _test_parse_sections(parse_sections, unindented_docstring, expected_sections ), ) def test_section_items_regex(section_body, expected_matches): - assert parse_section_items(section_body) == expected_matches + assert ( + AbstractDocstringProcessor._parse_section_items(section_body) + == expected_matches + ) def func_none(): diff --git a/tests/test_inheritance_for_functions.py b/tests/test_inheritance_for_functions.py index 689d3cf..519abc2 100644 --- a/tests/test_inheritance_for_functions.py +++ b/tests/test_inheritance_for_functions.py @@ -23,9 +23,9 @@ import pytest -from docstring_inheritance import inherit_google_docstring -from docstring_inheritance import inherit_numpy_docstring -from docstring_inheritance.metaclass import create_dummy_func_with_doc +from docstring_inheritance import process_google_docstring +from docstring_inheritance import process_numpy_docstring +from docstring_inheritance.class_processor import ClassDocstringsInheritor def test_side_effect(): @@ -34,7 +34,7 @@ def f(x, y=None, **kwargs): ref_signature = inspect.signature(f) - inherit_numpy_docstring(None, f) + process_numpy_docstring(None, f) assert inspect.signature(f) == ref_signature @@ -133,12 +133,12 @@ def child(x, missing_doc, *child_varargs, **child_kwargs): Parent todo """ - inherit_google_docstring(parent.__doc__, child) + process_google_docstring(parent.__doc__, child) assert child.__doc__ == expected.strip("\n") @pytest.mark.parametrize( - "inherit_docstring", [inherit_numpy_docstring, inherit_google_docstring] + "inherit_docstring", [process_numpy_docstring, process_google_docstring] ) @pytest.mark.parametrize( "parent_docstring,child_docstring,expected_docstring", @@ -147,6 +147,6 @@ def child(x, missing_doc, *child_varargs, **child_kwargs): def test_simple( inherit_docstring, parent_docstring, child_docstring, expected_docstring ): - dummy_func = create_dummy_func_with_doc(child_docstring) + dummy_func = ClassDocstringsInheritor._create_dummy_func_with_doc(child_docstring) inherit_docstring(parent_docstring, dummy_func) assert dummy_func.__doc__ == expected_docstring diff --git a/tests/test_numpy_processor.py b/tests/test_numpy_processor.py index 043be94..9bd1d05 100644 --- a/tests/test_numpy_processor.py +++ b/tests/test_numpy_processor.py @@ -22,7 +22,7 @@ import pytest from test_base_processor import _test_parse_sections -from docstring_inheritance.processors.numpy import NumpyDocstringProcessor +from docstring_inheritance.docstring_processors.numpy import NumpyDocstringProcessor @pytest.mark.parametrize( From 4639c4ed1429ced85d7e0d33b76ac5cadc19fb4f Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Tue, 3 Jan 2023 22:42:37 +0100 Subject: [PATCH 07/16] Fix mypy errors --- .pre-commit-config.yaml | 9 +++-- README.md | 34 +++++++++---------- src/docstring_inheritance/__init__.py | 14 ++++---- src/docstring_inheritance/class_processor.py | 33 ++++++++++++------ .../docstring_processors/base.py | 20 +++++++---- .../docstring_processors/google.py | 2 ++ .../docstring_processors/numpy.py | 15 ++------ src/docstring_inheritance/py.typed | 0 tests/test_inheritance_for_functions.py | 10 +++--- 9 files changed, 74 insertions(+), 63 deletions(-) create mode 100644 src/docstring_inheritance/py.typed diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83de87f..fc1c571 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer - exclude: LICENSES/headers + exclude: ^LICENSES/headers - id: check-added-large-files - id: check-toml - id: destroyed-symlinks @@ -43,7 +43,7 @@ repos: rev: v1.5.1 hooks: - id: docformatter - exclude: ^tests/.*$ + exclude: ^tests/ args: [ --in-place, --wrap-summaries, @@ -71,12 +71,15 @@ repos: - flake8-bugbear==22.12.6 - flake8-docstrings==1.6.0 - flake8-print==5.0.0 - - pep8-naming==0.13.2 + - pep8-naming==0.13.3 - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.991 hooks: - id: mypy + exclude: ^(tests/|setup\.py) + args: + - --strict - repo: https://github.com/Lucas-C/pre-commit-hooks rev: v1.3.1 diff --git a/README.md b/README.md index 0f5e06a..f7698f9 100644 --- a/README.md +++ b/README.md @@ -140,35 +140,35 @@ Use the `inherit_google_docstring` function to inherit docstrings in google form Use the `inherit_numpy_docstring` function to inherit docstrings in numpy format. ```python -from docstring_inheritance import process_google_docstring +from docstring_inheritance import inherit_google_docstring def parent(): - """Parent summary. + """Parent summary. - Args: - x: Description for x. - y: Description for y. + Args: + x: Description for x. + y: Description for y. - Notes: - Parent notes. - """ + Notes: + Parent notes. + """ def child(): - """ - Args: - z: Description for z. + """ + Args: + z: Description for z. - Returns: - Something. + Returns: + Something. - Notes: - Child notes. - """ + Notes: + Child notes. + """ -process_google_docstring(parent.__doc__, child) +inherit_google_docstring(parent.__doc__, child) # The inherited docstring is child.__doc__ = """Parent summary. diff --git a/src/docstring_inheritance/__init__.py b/src/docstring_inheritance/__init__.py index 79c8c92..46b36dc 100644 --- a/src/docstring_inheritance/__init__.py +++ b/src/docstring_inheritance/__init__.py @@ -22,23 +22,23 @@ from typing import Any from .class_processor import ClassDocstringsInheritor -from .class_processor import DocstringProcessorType +from .class_processor import DocstringProcessor from .docstring_processors.google import GoogleDocstringProcessor from .docstring_processors.numpy import NumpyDocstringProcessor -process_numpy_docstring = NumpyDocstringProcessor() -process_google_docstring = GoogleDocstringProcessor() +inherit_numpy_docstring = NumpyDocstringProcessor() +inherit_google_docstring = GoogleDocstringProcessor() class _BaseDocstringInheritanceMeta(type): - """Metaclass for inheriting class docstrings with a docstring processor.""" + """Base metaclass for inheriting class docstrings with a docstring processor.""" def __init__( cls, class_name: str, class_bases: tuple[type], class_dict: dict[str, Any], - docstring_processor: DocstringProcessorType, + docstring_processor: DocstringProcessor, ) -> None: super().__init__(class_name, class_bases, class_dict) if class_bases: @@ -56,7 +56,7 @@ def __init__( class_bases: tuple[type], class_dict: dict[str, Any], ) -> None: - super().__init__(class_name, class_bases, class_dict, process_google_docstring) + super().__init__(class_name, class_bases, class_dict, inherit_google_docstring) class NumpyDocstringInheritanceMeta(_BaseDocstringInheritanceMeta): @@ -68,4 +68,4 @@ def __init__( class_bases: tuple[type], class_dict: dict[str, Any], ) -> None: - super().__init__(class_name, class_bases, class_dict, process_numpy_docstring) + super().__init__(class_name, class_bases, class_dict, inherit_numpy_docstring) diff --git a/src/docstring_inheritance/class_processor.py b/src/docstring_inheritance/class_processor.py index 9ceffbc..722253c 100644 --- a/src/docstring_inheritance/class_processor.py +++ b/src/docstring_inheritance/class_processor.py @@ -21,10 +21,11 @@ from copy import copy from types import FunctionType +from typing import Any from typing import Callable from typing import Optional -DocstringProcessorType = Callable[[Optional[str], Callable], None] +DocstringProcessor = Callable[[Optional[str], Callable[..., Any]], None] class ClassDocstringsInheritor: @@ -33,7 +34,7 @@ class ClassDocstringsInheritor: _cls: type """The class to process.""" - _processor: DocstringProcessorType + _processor: DocstringProcessor """The docstring processor.""" _init_in_class: bool @@ -45,14 +46,15 @@ class ClassDocstringsInheritor: def __init__( self, cls: type, - docstring_processor: DocstringProcessorType, + docstring_processor: DocstringProcessor, ) -> None: """ Args: cls: The class to process. docstring_processor: The docstring processor. """ - # Remove the new class itself and the object class from the mro. + # Remove the new class itself and the object class from the mro, + # we do not want to inherit from object's docstrings. self.__mro_classes = cls.mro()[1:-1] self._cls = cls self._processor = docstring_processor @@ -100,17 +102,26 @@ def _get_class_dummy_func( The function with the class docstring. """ if self._init_in_class: - for cls_ in self.__mro_classes: - method = getattr(cls_, "__init__") # noqa:B009 - if method is not None: - func = copy(method) - func.__doc__ = self._cls.__doc__ - return func + # for cls_ in self.__mro_classes: + # # Since object is no longer in the MRO classes, + # # there may be not __init__. + # method: Callable[..., None] = getattr(cls_, "__init__") # noqa:B009 + # if method is not None: + # func = copy(method) + # func.__doc__ = self._cls.__doc__ + # return func + # Since object is no longer in the MRO classes, + # there may be not __init__. + method: Callable[..., None] = getattr(self._cls, "__init__") # noqa:B009 + if method is not None: + func = copy(method) + func.__doc__ = self._cls.__doc__ + return func return self._create_dummy_func_with_doc(self._cls.__doc__) @staticmethod - def _create_dummy_func_with_doc(docstring: str | None) -> Callable: + def _create_dummy_func_with_doc(docstring: str | None) -> Callable[..., Any]: """Create a dummy function with a given docstring. Args: diff --git a/src/docstring_inheritance/docstring_processors/base.py b/src/docstring_inheritance/docstring_processors/base.py index 748be31..f7a40f2 100644 --- a/src/docstring_inheritance/docstring_processors/base.py +++ b/src/docstring_inheritance/docstring_processors/base.py @@ -25,7 +25,9 @@ import sys from itertools import dropwhile from itertools import tee +from typing import Any from typing import Callable +from typing import cast from typing import ClassVar from typing import Dict from typing import Optional @@ -98,7 +100,7 @@ def _render_section( ) -> str: """Return a rendered docstring section.""" - def __call__(self, parent_doc: str | None, child_func: Callable) -> None: + def __call__(self, parent_doc: str | None, child_func: Callable[..., Any]) -> None: if parent_doc is None: return @@ -165,8 +167,8 @@ def _parse_sections(cls, docstring: str | None) -> SectionsType: sections[None] = cls._get_section_body(reversed_section_body_lines) # dict.items() is not reversible in python < 3.8: cast to tuple. - for section_name, section_body in reversed(tuple(reversed_sections.items())): - sections[section_name] = section_body + for section_name_, section_body_ in reversed(tuple(reversed_sections.items())): + sections[section_name_] = section_body_ return sections @@ -175,7 +177,7 @@ def _inherit_sections( cls, parent_sections: SectionsType, child_sections: SectionsType, - child_func: Callable, + child_func: Callable[..., Any], ) -> SectionsType: # TODO: # prnt_only_raises = "Raises" in parent_sections and not ( @@ -209,8 +211,12 @@ def _inherit_sections( ) for section_name in common_section_names_with_items: - temp_section_items = parent_sections[section_name].copy() - temp_section_items.update(child_sections[section_name]) + temp_section_items = cast( + dict[str, str], parent_sections[section_name] + ).copy() + temp_section_items.update( + cast(dict[str, str], child_sections[section_name]) + ) if section_name in cls._ARGS_SECTION_ITEMS_NAMES: temp_section_items = cls._inherit_section_items_with_args( @@ -235,7 +241,7 @@ def _inherit_sections( @classmethod def _inherit_section_items_with_args( cls, - func: Callable, + func: Callable[..., Any], section_items: dict[str, str], ) -> dict[str, str]: """Inherit section items for the args of a signature. diff --git a/src/docstring_inheritance/docstring_processors/google.py b/src/docstring_inheritance/docstring_processors/google.py index 14c3af1..5f2760b 100644 --- a/src/docstring_inheritance/docstring_processors/google.py +++ b/src/docstring_inheritance/docstring_processors/google.py @@ -27,6 +27,8 @@ class GoogleDocstringProcessor(AbstractDocstringProcessor): + """A processor for docstrings in Google format.""" + _SECTION_NAMES: ClassVar[list[str | None]] = list( AbstractDocstringProcessor._SECTION_NAMES ) diff --git a/src/docstring_inheritance/docstring_processors/numpy.py b/src/docstring_inheritance/docstring_processors/numpy.py index 79192b4..8cee562 100644 --- a/src/docstring_inheritance/docstring_processors/numpy.py +++ b/src/docstring_inheritance/docstring_processors/numpy.py @@ -20,12 +20,13 @@ from __future__ import annotations from typing import ClassVar -from typing import overload from .base import AbstractDocstringProcessor class NumpyDocstringProcessor(AbstractDocstringProcessor): + """A processor for docstrings in Numpy format.""" + _ARGS_SECTION_ITEMS_NAMES: ClassVar[set[str]] = { "Parameters", "Other Parameters", @@ -54,18 +55,6 @@ def _parse_one_section( return line1s, cls._get_section_body(reversed_section_body_lines) return None, None - @overload - @classmethod - def _render_section(cls, section_name: None, section_body: str) -> str: - ... - - @overload - @classmethod - def _render_section( - cls, section_name: str, section_body: str | dict[str, str] - ) -> str: - ... - @classmethod def _render_section( cls, section_name: str | None, section_body: str | dict[str, str] diff --git a/src/docstring_inheritance/py.typed b/src/docstring_inheritance/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_inheritance_for_functions.py b/tests/test_inheritance_for_functions.py index 519abc2..12e766b 100644 --- a/tests/test_inheritance_for_functions.py +++ b/tests/test_inheritance_for_functions.py @@ -23,8 +23,8 @@ import pytest -from docstring_inheritance import process_google_docstring -from docstring_inheritance import process_numpy_docstring +from docstring_inheritance import inherit_google_docstring +from docstring_inheritance import inherit_numpy_docstring from docstring_inheritance.class_processor import ClassDocstringsInheritor @@ -34,7 +34,7 @@ def f(x, y=None, **kwargs): ref_signature = inspect.signature(f) - process_numpy_docstring(None, f) + inherit_numpy_docstring(None, f) assert inspect.signature(f) == ref_signature @@ -133,12 +133,12 @@ def child(x, missing_doc, *child_varargs, **child_kwargs): Parent todo """ - process_google_docstring(parent.__doc__, child) + inherit_google_docstring(parent.__doc__, child) assert child.__doc__ == expected.strip("\n") @pytest.mark.parametrize( - "inherit_docstring", [process_numpy_docstring, process_google_docstring] + "inherit_docstring", [inherit_numpy_docstring, inherit_google_docstring] ) @pytest.mark.parametrize( "parent_docstring,child_docstring,expected_docstring", From 7a6d66f454e8875615f5390328844adc109b0b90 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Wed, 4 Jan 2023 07:30:39 +0100 Subject: [PATCH 08/16] Better namings --- src/docstring_inheritance/__init__.py | 18 +++++++-------- ...essor.py => class_docstrings_inheritor.py} | 18 +++++++-------- .../__init__.py | 0 .../base.py | 2 +- .../google.py | 14 ++++++------ .../numpy.py | 8 +++---- ...se_processor.py => test_base_inheritor.py} | 14 ++++++------ ..._processor.py => test_google_inheritor.py} | 18 +++++++-------- tests/test_inheritance_for_functions.py | 2 +- ...y_processor.py => test_numpy_inheritor.py} | 22 +++++++++---------- 10 files changed, 58 insertions(+), 58 deletions(-) rename src/docstring_inheritance/{class_processor.py => class_docstrings_inheritor.py} (90%) rename src/docstring_inheritance/{docstring_processors => docstring_inheritors}/__init__.py (100%) rename src/docstring_inheritance/{docstring_processors => docstring_inheritors}/base.py (99%) rename src/docstring_inheritance/{docstring_processors => docstring_inheritors}/google.py (87%) rename src/docstring_inheritance/{docstring_processors => docstring_inheritors}/numpy.py (92%) rename tests/{test_base_processor.py => test_base_inheritor.py} (90%) rename tests/{test_google_processor.py => test_google_inheritor.py} (88%) rename tests/{test_numpy_processor.py => test_numpy_inheritor.py} (89%) diff --git a/src/docstring_inheritance/__init__.py b/src/docstring_inheritance/__init__.py index 46b36dc..1c9c3af 100644 --- a/src/docstring_inheritance/__init__.py +++ b/src/docstring_inheritance/__init__.py @@ -21,28 +21,28 @@ from typing import Any -from .class_processor import ClassDocstringsInheritor -from .class_processor import DocstringProcessor -from .docstring_processors.google import GoogleDocstringProcessor -from .docstring_processors.numpy import NumpyDocstringProcessor +from .class_docstrings_inheritor import ClassDocstringsInheritor +from .class_docstrings_inheritor import DocstringInheritor +from .docstring_inheritors.google import GoogleDocstringInheritor +from .docstring_inheritors.numpy import NumpyDocstringInheritor -inherit_numpy_docstring = NumpyDocstringProcessor() -inherit_google_docstring = GoogleDocstringProcessor() +inherit_numpy_docstring = NumpyDocstringInheritor() +inherit_google_docstring = GoogleDocstringInheritor() class _BaseDocstringInheritanceMeta(type): - """Base metaclass for inheriting class docstrings with a docstring processor.""" + """Base metaclass for inheriting class docstrings.""" def __init__( cls, class_name: str, class_bases: tuple[type], class_dict: dict[str, Any], - docstring_processor: DocstringProcessor, + docstring_inheritor: DocstringInheritor, ) -> None: super().__init__(class_name, class_bases, class_dict) if class_bases: - inheritor = ClassDocstringsInheritor(cls, docstring_processor) + inheritor = ClassDocstringsInheritor(cls, docstring_inheritor) inheritor.inherit_class_docstring() inheritor.inherit_attrs_docstrings() diff --git a/src/docstring_inheritance/class_processor.py b/src/docstring_inheritance/class_docstrings_inheritor.py similarity index 90% rename from src/docstring_inheritance/class_processor.py rename to src/docstring_inheritance/class_docstrings_inheritor.py index 722253c..4270011 100644 --- a/src/docstring_inheritance/class_processor.py +++ b/src/docstring_inheritance/class_docstrings_inheritor.py @@ -25,17 +25,17 @@ from typing import Callable from typing import Optional -DocstringProcessor = Callable[[Optional[str], Callable[..., Any]], None] +DocstringInheritor = Callable[[Optional[str], Callable[..., Any]], None] class ClassDocstringsInheritor: - """Processor for inheriting class docstrings.""" + """A class for inheriting class docstrings.""" _cls: type """The class to process.""" - _processor: DocstringProcessor - """The docstring processor.""" + _docstring_inheritor: DocstringInheritor + """The docstring inheritor.""" _init_in_class: bool """Whether the ``__init__`` arguments documentation is in the class docstring.""" @@ -46,18 +46,18 @@ class ClassDocstringsInheritor: def __init__( self, cls: type, - docstring_processor: DocstringProcessor, + docstring_inheritor: DocstringInheritor, ) -> None: """ Args: cls: The class to process. - docstring_processor: The docstring processor. + docstring_inheritor: The docstring inheritor. """ # Remove the new class itself and the object class from the mro, # we do not want to inherit from object's docstrings. self.__mro_classes = cls.mro()[1:-1] self._cls = cls - self._processor = docstring_processor + self._docstring_inheritor = docstring_inheritor self._init_in_class = False def inherit_class_docstring( @@ -67,7 +67,7 @@ def inherit_class_docstring( func = self._get_class_dummy_func() for cls_ in self.__mro_classes: - self._processor(cls_.__doc__, func) + self._docstring_inheritor(cls_.__doc__, func) self._cls.__doc__ = func.__doc__ @@ -89,7 +89,7 @@ def inherit_attrs_docstrings( else: continue - self._processor(parent_doc, attr) + self._docstring_inheritor(parent_doc, attr) def _get_class_dummy_func( self, diff --git a/src/docstring_inheritance/docstring_processors/__init__.py b/src/docstring_inheritance/docstring_inheritors/__init__.py similarity index 100% rename from src/docstring_inheritance/docstring_processors/__init__.py rename to src/docstring_inheritance/docstring_inheritors/__init__.py diff --git a/src/docstring_inheritance/docstring_processors/base.py b/src/docstring_inheritance/docstring_inheritors/base.py similarity index 99% rename from src/docstring_inheritance/docstring_processors/base.py rename to src/docstring_inheritance/docstring_inheritors/base.py index f7a40f2..f25c9ad 100644 --- a/src/docstring_inheritance/docstring_processors/base.py +++ b/src/docstring_inheritance/docstring_inheritors/base.py @@ -46,7 +46,7 @@ def pairwise(iterable): # pragma: no cover return zip(a, b) -class AbstractDocstringProcessor: +class AbstractDocstringInheritor: """Abstract base class for inheriting a docstring.""" _SECTION_NAMES: ClassVar[list[str | None]] = [ diff --git a/src/docstring_inheritance/docstring_processors/google.py b/src/docstring_inheritance/docstring_inheritors/google.py similarity index 87% rename from src/docstring_inheritance/docstring_processors/google.py rename to src/docstring_inheritance/docstring_inheritors/google.py index 5f2760b..2ff83ac 100644 --- a/src/docstring_inheritance/docstring_processors/google.py +++ b/src/docstring_inheritance/docstring_inheritors/google.py @@ -22,15 +22,15 @@ import textwrap from typing import ClassVar -from .base import AbstractDocstringProcessor -from .numpy import NumpyDocstringProcessor +from .base import AbstractDocstringInheritor +from .numpy import NumpyDocstringInheritor -class GoogleDocstringProcessor(AbstractDocstringProcessor): - """A processor for docstrings in Google format.""" +class GoogleDocstringInheritor(AbstractDocstringInheritor): + """A class for inheriting docstrings in Google format.""" _SECTION_NAMES: ClassVar[list[str | None]] = list( - AbstractDocstringProcessor._SECTION_NAMES + AbstractDocstringInheritor._SECTION_NAMES ) _SECTION_NAMES[1] = "Args" @@ -41,12 +41,12 @@ class GoogleDocstringProcessor(AbstractDocstringProcessor): "Methods", } - MISSING_ARG_DESCRIPTION = f": {AbstractDocstringProcessor.MISSING_ARG_DESCRIPTION}" + MISSING_ARG_DESCRIPTION = f": {AbstractDocstringInheritor.MISSING_ARG_DESCRIPTION}" @classmethod def _get_section_body(cls, reversed_section_body_lines: list[str]) -> str: return textwrap.dedent( - NumpyDocstringProcessor._get_section_body(reversed_section_body_lines) + NumpyDocstringInheritor._get_section_body(reversed_section_body_lines) ) @classmethod diff --git a/src/docstring_inheritance/docstring_processors/numpy.py b/src/docstring_inheritance/docstring_inheritors/numpy.py similarity index 92% rename from src/docstring_inheritance/docstring_processors/numpy.py rename to src/docstring_inheritance/docstring_inheritors/numpy.py index 8cee562..ee48975 100644 --- a/src/docstring_inheritance/docstring_processors/numpy.py +++ b/src/docstring_inheritance/docstring_inheritors/numpy.py @@ -21,11 +21,11 @@ from typing import ClassVar -from .base import AbstractDocstringProcessor +from .base import AbstractDocstringInheritor -class NumpyDocstringProcessor(AbstractDocstringProcessor): - """A processor for docstrings in Numpy format.""" +class NumpyDocstringInheritor(AbstractDocstringInheritor): + """A class for inheriting docstrings in Numpy format.""" _ARGS_SECTION_ITEMS_NAMES: ClassVar[set[str]] = { "Parameters", @@ -39,7 +39,7 @@ class NumpyDocstringProcessor(AbstractDocstringProcessor): MISSING_ARG_DESCRIPTION: ClassVar[ str - ] = f":\n{AbstractDocstringProcessor.MISSING_ARG_DESCRIPTION}" + ] = f":\n{AbstractDocstringInheritor.MISSING_ARG_DESCRIPTION}" @classmethod def _parse_one_section( diff --git a/tests/test_base_processor.py b/tests/test_base_inheritor.py similarity index 90% rename from tests/test_base_processor.py rename to tests/test_base_inheritor.py index 16fa73d..d8b8008 100644 --- a/tests/test_base_processor.py +++ b/tests/test_base_inheritor.py @@ -23,7 +23,7 @@ import pytest -from docstring_inheritance.docstring_processors.base import AbstractDocstringProcessor +from docstring_inheritance.docstring_inheritors.base import AbstractDocstringInheritor def _test_parse_sections(parse_sections, unindented_docstring, expected_sections): @@ -53,7 +53,7 @@ def _test_parse_sections(parse_sections, unindented_docstring, expected_sections ) def test_section_items_regex(section_body, expected_matches): assert ( - AbstractDocstringProcessor._parse_section_items(section_body) + AbstractDocstringInheritor._parse_section_items(section_body) == expected_matches ) @@ -114,7 +114,7 @@ def func_all(arg1, arg2=None, *varargs, **varkw): ( func_args_kwonlyargs, {"arg1": ""}, - {"arg1": "", "arg2": AbstractDocstringProcessor.MISSING_ARG_DESCRIPTION}, + {"arg1": "", "arg2": AbstractDocstringInheritor.MISSING_ARG_DESCRIPTION}, ), # Args are ordered according to the signature. ( @@ -126,7 +126,7 @@ def func_all(arg1, arg2=None, *varargs, **varkw): ( func_varargs, {}, - {"*varargs": AbstractDocstringProcessor.MISSING_ARG_DESCRIPTION}, + {"*varargs": AbstractDocstringInheritor.MISSING_ARG_DESCRIPTION}, ), ( func_varargs, @@ -137,7 +137,7 @@ def func_all(arg1, arg2=None, *varargs, **varkw): ( func_varkw, {}, - {"**varkw": AbstractDocstringProcessor.MISSING_ARG_DESCRIPTION}, + {"**varkw": AbstractDocstringInheritor.MISSING_ARG_DESCRIPTION}, ), ( func_varkw, @@ -148,7 +148,7 @@ def func_all(arg1, arg2=None, *varargs, **varkw): ( func_kwonlyargs, {}, - {"arg": AbstractDocstringProcessor.MISSING_ARG_DESCRIPTION}, + {"arg": AbstractDocstringInheritor.MISSING_ARG_DESCRIPTION}, ), ( func_kwonlyargs, @@ -183,6 +183,6 @@ def func_all(arg1, arg2=None, *varargs, **varkw): ) def test_inherit_section_items_with_args(func, section_items, expected): assert ( - AbstractDocstringProcessor._inherit_section_items_with_args(func, section_items) + AbstractDocstringInheritor._inherit_section_items_with_args(func, section_items) == expected ) diff --git a/tests/test_google_processor.py b/tests/test_google_inheritor.py similarity index 88% rename from tests/test_google_processor.py rename to tests/test_google_inheritor.py index d6a6652..ffdea74 100644 --- a/tests/test_google_processor.py +++ b/tests/test_google_inheritor.py @@ -20,9 +20,9 @@ from __future__ import annotations import pytest -from test_base_processor import _test_parse_sections +from test_base_inheritor import _test_parse_sections -from docstring_inheritance import GoogleDocstringProcessor +from docstring_inheritance import GoogleDocstringInheritor @pytest.mark.parametrize( @@ -98,7 +98,7 @@ ) def test_parse_sections(unindented_docstring, expected_sections): _test_parse_sections( - GoogleDocstringProcessor._parse_sections, + GoogleDocstringInheritor._parse_sections, unindented_docstring, expected_sections, ) @@ -135,7 +135,7 @@ def test_parse_sections(unindented_docstring, expected_sections): ) def test_render_section(section_name, section_body, expected_docstring): assert ( - GoogleDocstringProcessor._render_section(section_name, section_body) + GoogleDocstringInheritor._render_section(section_name, section_body) == expected_docstring ) @@ -150,7 +150,7 @@ def test_render_section(section_name, section_body, expected_docstring): ], ) def test_get_section_body(section_body, expected): - assert GoogleDocstringProcessor._get_section_body(section_body) == expected + assert GoogleDocstringInheritor._get_section_body(section_body) == expected @pytest.mark.parametrize( @@ -166,7 +166,7 @@ def test_get_section_body(section_body, expected): ], ) def test_parse_one_section(line1, line2s, expected): - assert GoogleDocstringProcessor._parse_one_section(line1, line2s, []) == expected + assert GoogleDocstringInheritor._parse_one_section(line1, line2s, []) == expected @pytest.mark.parametrize( @@ -193,17 +193,17 @@ def test_parse_one_section(line1, line2s, expected): ], ) def test_render_docstring(sections, expected): - assert GoogleDocstringProcessor._render_docstring(sections) == expected + assert GoogleDocstringInheritor._render_docstring(sections) == expected def test_inherit_section_items_with_args(): def func(arg): """""" - expected = {"arg": GoogleDocstringProcessor.MISSING_ARG_DESCRIPTION} + expected = {"arg": GoogleDocstringInheritor.MISSING_ARG_DESCRIPTION} assert ( - GoogleDocstringProcessor._inherit_section_items_with_args(func, {}) == expected + GoogleDocstringInheritor._inherit_section_items_with_args(func, {}) == expected ) diff --git a/tests/test_inheritance_for_functions.py b/tests/test_inheritance_for_functions.py index 12e766b..b9769f4 100644 --- a/tests/test_inheritance_for_functions.py +++ b/tests/test_inheritance_for_functions.py @@ -25,7 +25,7 @@ from docstring_inheritance import inherit_google_docstring from docstring_inheritance import inherit_numpy_docstring -from docstring_inheritance.class_processor import ClassDocstringsInheritor +from docstring_inheritance.class_docstrings_inheritor import ClassDocstringsInheritor def test_side_effect(): diff --git a/tests/test_numpy_processor.py b/tests/test_numpy_inheritor.py similarity index 89% rename from tests/test_numpy_processor.py rename to tests/test_numpy_inheritor.py index 9bd1d05..500ac14 100644 --- a/tests/test_numpy_processor.py +++ b/tests/test_numpy_inheritor.py @@ -20,9 +20,9 @@ from __future__ import annotations import pytest -from test_base_processor import _test_parse_sections +from test_base_inheritor import _test_parse_sections -from docstring_inheritance.docstring_processors.numpy import NumpyDocstringProcessor +from docstring_inheritance.docstring_inheritors.numpy import NumpyDocstringInheritor @pytest.mark.parametrize( @@ -102,7 +102,7 @@ ) def test_parse_sections(unindented_docstring, expected_sections): _test_parse_sections( - NumpyDocstringProcessor._parse_sections, unindented_docstring, expected_sections + NumpyDocstringInheritor._parse_sections, unindented_docstring, expected_sections ) @@ -140,7 +140,7 @@ def test_parse_sections(unindented_docstring, expected_sections): ) def test_render_section(section_name, section_body, expected_docstring): assert ( - NumpyDocstringProcessor._render_section(section_name, section_body) + NumpyDocstringInheritor._render_section(section_name, section_body) == expected_docstring ) @@ -155,7 +155,7 @@ def test_render_section(section_name, section_body, expected_docstring): ], ) def test_get_section_body(section_body, expected): - assert NumpyDocstringProcessor._get_section_body(section_body) == expected + assert NumpyDocstringInheritor._get_section_body(section_body) == expected @pytest.mark.parametrize( @@ -175,10 +175,10 @@ def test_get_section_body(section_body, expected): ], ) def test_parse_one_section(line1, line2s, expected): - assert NumpyDocstringProcessor._parse_one_section(line1, line2s, []) == expected + assert NumpyDocstringInheritor._parse_one_section(line1, line2s, []) == expected -# The following are test for methods of AbstractDocstringProcessor that depend +# The following are test for methods of AbstractDocstringInheritor that depend # concrete implementation of abstract methods. @@ -220,7 +220,7 @@ def test_parse_one_section(line1, line2s, expected): ], ) def test_inherit_sections(parent_sections, child_sections, expected_sections): - new_child_sections = NumpyDocstringProcessor._inherit_sections( + new_child_sections = NumpyDocstringInheritor._inherit_sections( parent_sections, child_sections, lambda: None ) assert new_child_sections == expected_sections @@ -254,17 +254,17 @@ def test_inherit_sections(parent_sections, child_sections, expected_sections): ], ) def test_render_docstring(sections, expected): - assert NumpyDocstringProcessor._render_docstring(sections) == expected + assert NumpyDocstringInheritor._render_docstring(sections) == expected def test_inherit_section_items_with_args(): def func(arg): """""" - expected = {"arg": NumpyDocstringProcessor.MISSING_ARG_DESCRIPTION} + expected = {"arg": NumpyDocstringInheritor.MISSING_ARG_DESCRIPTION} assert ( - NumpyDocstringProcessor._inherit_section_items_with_args(func, {}) == expected + NumpyDocstringInheritor._inherit_section_items_with_args(func, {}) == expected ) From 8c2dd6486e32bf86e0ea63940438d0749ef5f557 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Sat, 7 Jan 2023 12:54:07 +0100 Subject: [PATCH 09/16] Support init_in_class --- .gitignore | 1 + src/docstring_inheritance/__init__.py | 61 ++++++++++++-- .../class_docstrings_inheritor.py | 84 +++++++++++-------- tests/test_metaclass_google.py | 62 ++++++++++++-- tests/test_metaclass_numpy.py | 75 ++++++++++------- 5 files changed, 207 insertions(+), 76 deletions(-) diff --git a/.gitignore b/.gitignore index 1136ab4..53b2ba5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.tox /.coverage /htmlcov +/coverage.xml diff --git a/src/docstring_inheritance/__init__.py b/src/docstring_inheritance/__init__.py index 1c9c3af..89270cd 100644 --- a/src/docstring_inheritance/__init__.py +++ b/src/docstring_inheritance/__init__.py @@ -39,12 +39,13 @@ def __init__( class_bases: tuple[type], class_dict: dict[str, Any], docstring_inheritor: DocstringInheritor, + init_in_class: bool, ) -> None: super().__init__(class_name, class_bases, class_dict) if class_bases: - inheritor = ClassDocstringsInheritor(cls, docstring_inheritor) - inheritor.inherit_class_docstring() - inheritor.inherit_attrs_docstrings() + ClassDocstringsInheritor.inherit_docstring( + cls, docstring_inheritor, init_in_class + ) class GoogleDocstringInheritanceMeta(_BaseDocstringInheritanceMeta): @@ -56,7 +57,32 @@ def __init__( class_bases: tuple[type], class_dict: dict[str, Any], ) -> None: - super().__init__(class_name, class_bases, class_dict, inherit_google_docstring) + super().__init__( + class_name, + class_bases, + class_dict, + inherit_google_docstring, + init_in_class=False, + ) + + +class GoogleDocstringInheritanceInitMeta(_BaseDocstringInheritanceMeta): + """Metaclass for inheriting docstrings in Google format with ``__init__`` in the + class docstring.""" + + def __init__( + self, + class_name: str, + class_bases: tuple[type], + class_dict: dict[str, Any], + ) -> None: + super().__init__( + class_name, + class_bases, + class_dict, + inherit_google_docstring, + init_in_class=True, + ) class NumpyDocstringInheritanceMeta(_BaseDocstringInheritanceMeta): @@ -68,4 +94,29 @@ def __init__( class_bases: tuple[type], class_dict: dict[str, Any], ) -> None: - super().__init__(class_name, class_bases, class_dict, inherit_numpy_docstring) + super().__init__( + class_name, + class_bases, + class_dict, + inherit_numpy_docstring, + init_in_class=False, + ) + + +class NumpyDocstringInheritanceInitMeta(_BaseDocstringInheritanceMeta): + """Metaclass for inheriting docstrings in Numpy format with ``__init__`` in the + class docstring.""" + + def __init__( + self, + class_name: str, + class_bases: tuple[type], + class_dict: dict[str, Any], + ) -> None: + super().__init__( + class_name, + class_bases, + class_dict, + inherit_numpy_docstring, + init_in_class=True, + ) diff --git a/src/docstring_inheritance/class_docstrings_inheritor.py b/src/docstring_inheritance/class_docstrings_inheritor.py index 4270011..dad90ef 100644 --- a/src/docstring_inheritance/class_docstrings_inheritor.py +++ b/src/docstring_inheritance/class_docstrings_inheritor.py @@ -19,8 +19,8 @@ # SOFTWARE. from __future__ import annotations -from copy import copy from types import FunctionType +from types import WrapperDescriptorType from typing import Any from typing import Callable from typing import Optional @@ -47,31 +47,74 @@ def __init__( self, cls: type, docstring_inheritor: DocstringInheritor, + init_in_class: bool, ) -> None: """ Args: cls: The class to process. docstring_inheritor: The docstring inheritor. + init_in_class: Whether the ``__init__`` arguments documentation is in the + class docstring. """ # Remove the new class itself and the object class from the mro, - # we do not want to inherit from object's docstrings. + # object's docstrings have no interest. self.__mro_classes = cls.mro()[1:-1] self._cls = cls self._docstring_inheritor = docstring_inheritor - self._init_in_class = False + self._init_in_class = init_in_class - def inherit_class_docstring( + @classmethod + def inherit_docstring( + cls, + class_: type, + docstring_inheritor: DocstringInheritor, + init_in_class: bool, + ) -> None: + """Create the inherited docstring for all the docstrings of the class. + + Args: + class_: The class to process. + docstring_inheritor: The docstring inheritor. + init_in_class: Whether the ``__init__`` arguments documentation is in the + class docstring. + """ + inheritor = cls(class_, docstring_inheritor, init_in_class) + inheritor._inherit_attrs_docstrings() + inheritor._inherit_class_docstring() + + def _inherit_class_docstring( self, ) -> None: """Create the inherited docstring for the class docstring.""" - func = self._get_class_dummy_func() + # func = self._get_class_dummy_func() + func = None + old_init_doc = None + init_doc_changed = False + + if self._init_in_class: + init_method: Callable[..., None] = self._cls.__init__ # type: ignore + # Ignore the case when __init__ is from object since there is no docstring + # and its __doc__ cannot be assigned. + if init_method is not None and not isinstance( + init_method, WrapperDescriptorType + ): + old_init_doc = init_method.__doc__ + init_method.__doc__ = self._cls.__doc__ + func = init_method + init_doc_changed = True + + if func is None: + func = self._create_dummy_func_with_doc(self._cls.__doc__) for cls_ in self.__mro_classes: self._docstring_inheritor(cls_.__doc__, func) self._cls.__doc__ = func.__doc__ - def inherit_attrs_docstrings( + if self._init_in_class and init_doc_changed: + init_method.__doc__ = old_init_doc + + def _inherit_attrs_docstrings( self, ) -> None: """Create the inherited docstrings for the class attributes.""" @@ -91,35 +134,6 @@ def inherit_attrs_docstrings( self._docstring_inheritor(parent_doc, attr) - def _get_class_dummy_func( - self, - ) -> Callable[..., None]: - """Return a dummy function with a given docstring. - - If ``self._ìnit_in_class`` is true then the function is a copy of ``__init__``. - - Returns: - The function with the class docstring. - """ - if self._init_in_class: - # for cls_ in self.__mro_classes: - # # Since object is no longer in the MRO classes, - # # there may be not __init__. - # method: Callable[..., None] = getattr(cls_, "__init__") # noqa:B009 - # if method is not None: - # func = copy(method) - # func.__doc__ = self._cls.__doc__ - # return func - # Since object is no longer in the MRO classes, - # there may be not __init__. - method: Callable[..., None] = getattr(self._cls, "__init__") # noqa:B009 - if method is not None: - func = copy(method) - func.__doc__ = self._cls.__doc__ - return func - - return self._create_dummy_func_with_doc(self._cls.__doc__) - @staticmethod def _create_dummy_func_with_doc(docstring: str | None) -> Callable[..., Any]: """Create a dummy function with a given docstring. diff --git a/tests/test_metaclass_google.py b/tests/test_metaclass_google.py index b1470e2..87b4e53 100644 --- a/tests/test_metaclass_google.py +++ b/tests/test_metaclass_google.py @@ -19,11 +19,20 @@ # SOFTWARE. from __future__ import annotations +import pytest + +from docstring_inheritance import GoogleDocstringInheritanceInitMeta from docstring_inheritance import GoogleDocstringInheritanceMeta +parametrize_inheritance = pytest.mark.parametrize( + "inheritance_class", + (GoogleDocstringInheritanceMeta, GoogleDocstringInheritanceInitMeta), +) + -def test_args_inheritance(): - class Parent(metaclass=GoogleDocstringInheritanceMeta): +@parametrize_inheritance +def test_args_inheritance(inheritance_class): + class Parent(metaclass=inheritance_class): def meth(self, w, x, *args, y=None, **kwargs): """ Args: @@ -53,8 +62,9 @@ def meth(self, xx, x, *args, yy=None, y=None, **kwargs): assert Child.meth.__doc__ == excepted -def test_several_inheritance(): - class GrandParent(metaclass=GoogleDocstringInheritanceMeta): +@parametrize_inheritance +def test_class_doc_inheritance(inheritance_class): + class GrandParent(metaclass=inheritance_class): """Class GrandParent. Attributes: @@ -107,12 +117,50 @@ class Child(Parent): assert Child.__doc__ == excepted -def test_do_not_inherit_object(): - class Parent(metaclass=GoogleDocstringInheritanceMeta): +@parametrize_inheritance +def test_do_not_inherit_from_object(inheritance_class): + class Parent(metaclass=inheritance_class): def __init__(self): pass + assert Parent.__init__.__doc__ is None + + +def test_class_doc_inheritance_with_init(): + class Parent(metaclass=GoogleDocstringInheritanceInitMeta): + """Class Parent. + + Args: + a: a from Parent. + b: b from Parent. + """ + + def __init__(self, a, b): + pass + class Child(Parent): - pass + """Class Child. + + Args: + c: c from Child. + + Note: + From Child. + """ + + def __init__(self, b, c): + pass + + excepted = """\ +Class Child. +Args: + b: b from Parent. + c: c from Child. + +Note: + From Child.\ +""" + + assert Child.__doc__ == excepted assert Child.__init__.__doc__ is None diff --git a/tests/test_metaclass_numpy.py b/tests/test_metaclass_numpy.py index d1ff2f8..3ed6040 100644 --- a/tests/test_metaclass_numpy.py +++ b/tests/test_metaclass_numpy.py @@ -21,8 +21,16 @@ from inspect import getdoc +import pytest + +from docstring_inheritance import NumpyDocstringInheritanceInitMeta from docstring_inheritance import NumpyDocstringInheritanceMeta +parametrize_inheritance = pytest.mark.parametrize( + "inheritance_class", + (NumpyDocstringInheritanceMeta, NumpyDocstringInheritanceInitMeta), +) + def assert_args_inheritance(cls): excepted = """ @@ -39,8 +47,9 @@ def assert_args_inheritance(cls): assert cls.meth.__doc__ == excepted -def test_args_inheritance_parent_meta(): - class Parent(metaclass=NumpyDocstringInheritanceMeta): +@parametrize_inheritance +def test_args_inheritance_parent_meta(inheritance_class): + class Parent(metaclass=inheritance_class): def meth(self, w, x, *args, y=None, **kwargs): """ Parameters @@ -63,7 +72,8 @@ def meth(self, xx, x, *args, yy=None, y=None, **kwargs): assert_args_inheritance(Child) -def test_args_inheritance_child_meta(): +@parametrize_inheritance +def test_args_inheritance_child_meta(inheritance_class): class Parent: def meth(self, w, x, *args, y=None, **kwargs): """ @@ -76,7 +86,7 @@ def meth(self, w, x, *args, y=None, **kwargs): **kwargs: int """ - class Child(Parent, metaclass=NumpyDocstringInheritanceMeta): + class Child(Parent, metaclass=inheritance_class): def meth(self, xx, x, *args, yy=None, y=None, **kwargs): """ Parameters @@ -96,8 +106,9 @@ def assert_missing_attr(cls): assert cls.prop.__doc__ == excepted -def test_missing_parent_attr_parent_meta(): - class Parent(metaclass=NumpyDocstringInheritanceMeta): +@parametrize_inheritance +def test_missing_parent_attr_parent_meta(inheritance_class): + class Parent(metaclass=inheritance_class): pass class Child(Parent): @@ -119,11 +130,12 @@ def prop(self): assert_missing_attr(Child) -def test_missing_parent_attr_child_meta(): +@parametrize_inheritance +def test_missing_parent_attr_child_meta(inheritance_class): class Parent: pass - class Child(Parent, metaclass=NumpyDocstringInheritanceMeta): + class Child(Parent, metaclass=inheritance_class): def method(self, xx, x, *args, yy=None, y=None, **kwargs): """Summary""" @@ -142,8 +154,9 @@ def prop(self): assert_missing_attr(Child) -def test_missing_parent_doc_for_attr_parent_meta(): - class Parent(metaclass=NumpyDocstringInheritanceMeta): +@parametrize_inheritance +def test_missing_parent_doc_for_attr_parent_meta(inheritance_class): + class Parent(metaclass=inheritance_class): def method(self): pass @@ -178,7 +191,8 @@ def prop(self): assert_missing_attr(Child) -def test_missing_parent_doc_for_attr_child_meta(): +@parametrize_inheritance +def test_missing_parent_doc_for_attr_child_meta(inheritance_class): class Parent: def method(self): pass @@ -195,7 +209,7 @@ def staticmethod(): def prop(self): pass - class Child(Parent, metaclass=NumpyDocstringInheritanceMeta): + class Child(Parent, metaclass=inheritance_class): def method(self, xx, x, *args, yy=None, y=None, **kwargs): """Summary""" @@ -229,8 +243,9 @@ def assert_multiple_inheritance(cls): assert getdoc(cls) == excepted -def test_multiple_inheritance_parent_meta(): - class Parent1(metaclass=NumpyDocstringInheritanceMeta): +@parametrize_inheritance +def test_multiple_inheritance_parent_meta(inheritance_class): + class Parent1(metaclass=inheritance_class): """Parent summary Attributes @@ -260,7 +275,8 @@ class Child(Parent1, Parent2): assert_multiple_inheritance(Child) -def test_multiple_inheritance_child_meta(): +@parametrize_inheritance +def test_multiple_inheritance_child_meta(inheritance_class): class Parent1: """Parent summary @@ -277,7 +293,7 @@ class Parent2: method1 """ - class Child(Parent1, Parent2, metaclass=NumpyDocstringInheritanceMeta): + class Child(Parent1, Parent2, metaclass=inheritance_class): """ Attributes ---------- @@ -291,8 +307,9 @@ class Child(Parent1, Parent2, metaclass=NumpyDocstringInheritanceMeta): assert_multiple_inheritance(Child) -def test_several_parents_parent_meta(): - class GrandParent(metaclass=NumpyDocstringInheritanceMeta): +@parametrize_inheritance +def test_several_parents_parent_meta(inheritance_class): + class GrandParent(metaclass=inheritance_class): """GrandParent summary Attributes @@ -322,7 +339,8 @@ class Child(Parent): assert_multiple_inheritance(Child) -def test_several_parents_child_meta(): +@parametrize_inheritance +def test_several_parents_child_meta(inheritance_class): class GrandParent: """GrandParent summary @@ -339,7 +357,7 @@ class Parent(GrandParent): method1 """ - class Child(Parent, metaclass=NumpyDocstringInheritanceMeta): + class Child(Parent, metaclass=inheritance_class): """ Attributes ---------- @@ -353,23 +371,22 @@ class Child(Parent, metaclass=NumpyDocstringInheritanceMeta): assert_multiple_inheritance(Child) -def test_do_not_inherit_object_parent_meta(): - class Parent(metaclass=NumpyDocstringInheritanceMeta): +@parametrize_inheritance +def test_do_not_inherit_object_child_meta(inheritance_class): + class Parent: def __init__(self): pass - class Child(Parent): + class Child(Parent, metaclass=inheritance_class): pass assert Child.__init__.__doc__ is None -def test_do_not_inherit_object_child_meta(): - class Parent: +@parametrize_inheritance +def test_do_not_inherit_from_object(inheritance_class): + class Parent(metaclass=inheritance_class): def __init__(self): pass - class Child(Parent, metaclass=NumpyDocstringInheritanceMeta): - pass - - assert Child.__init__.__doc__ is None + assert Parent.__init__.__doc__ is None From c5689644f9d6a18e39cadf610eab6d9cd414bbfa Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Sat, 7 Jan 2023 13:07:31 +0100 Subject: [PATCH 10/16] Fixes and build updates --- requirements/test.txt | 10 +++++----- setup.cfg | 2 -- src/docstring_inheritance/docstring_inheritors/base.py | 10 +++++----- tox.ini | 4 ++-- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/requirements/test.txt b/requirements/test.txt index 9782098..4f34cfc 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,21 +4,21 @@ # # pip-compile --extra=test --output-file=requirements/test.txt # -attrs==22.1.0 +attrs==22.2.0 # via pytest covdefaults==2.2.2 # via docstring-inheritance (setup.py) -coverage[toml]==6.5.0 +coverage[toml]==7.0.3 # via # covdefaults # pytest-cov -exceptiongroup==1.0.4 +exceptiongroup==1.1.0 # via pytest -importlib-metadata==5.1.0 +importlib-metadata==6.0.0 # via # pluggy # pytest -iniconfig==1.1.1 +iniconfig==2.0.0 # via pytest packaging==22.0 # via pytest diff --git a/setup.cfg b/setup.cfg index 5c98d59..fd06c7b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,8 +51,6 @@ plugins = covdefaults source = docstring_inheritance [flake8] -# See http://www.pydocstyle.org/en/latest/error_codes.html for more details. -# https://github.com/PyCQA/flake8-bugbear#how-to-enable-opinionated-warnings ignore = # No docstring for standard and private methods. D105 diff --git a/src/docstring_inheritance/docstring_inheritors/base.py b/src/docstring_inheritance/docstring_inheritors/base.py index f25c9ad..82d09d5 100644 --- a/src/docstring_inheritance/docstring_inheritors/base.py +++ b/src/docstring_inheritance/docstring_inheritors/base.py @@ -36,11 +36,11 @@ SectionsType = Dict[Optional[str], Union[str, Dict[str, str]]] -if sys.version_info >= (3, 10): +if sys.version_info >= (3, 10): # pragma: >=3.10 cover from itertools import pairwise -else: +else: # pragma: <3.10 cover # See https://docs.python.org/3/library/itertools.html#itertools.pairwise - def pairwise(iterable): # pragma: no cover + def pairwise(iterable): a, b = tee(iterable) next(b, None) return zip(a, b) @@ -212,10 +212,10 @@ def _inherit_sections( for section_name in common_section_names_with_items: temp_section_items = cast( - dict[str, str], parent_sections[section_name] + Dict[str, str], parent_sections[section_name] ).copy() temp_section_items.update( - cast(dict[str, str], child_sections[section_name]) + cast(Dict[str, str], child_sections[section_name]) ) if section_name in cls._ARGS_SECTION_ITEMS_NAMES: diff --git a/tox.ini b/tox.ini index ead6ea3..0b48a00 100644 --- a/tox.ini +++ b/tox.ini @@ -15,8 +15,8 @@ commands = description = create the pypi distribution # See packaging info at https://pypi.org/help/#publishing. deps = - twine ==3.5.0 - build ==0.7.0 + twine + build skip_install = true whitelist_externals = rm commands = From 62eb910471bf68e2edd3ae547687cf45450893a0 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Sun, 8 Jan 2023 21:17:11 +0100 Subject: [PATCH 11/16] Remove useless stuff --- src/docstring_inheritance/class_docstrings_inheritor.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/docstring_inheritance/class_docstrings_inheritor.py b/src/docstring_inheritance/class_docstrings_inheritor.py index dad90ef..f620562 100644 --- a/src/docstring_inheritance/class_docstrings_inheritor.py +++ b/src/docstring_inheritance/class_docstrings_inheritor.py @@ -86,7 +86,6 @@ def _inherit_class_docstring( self, ) -> None: """Create the inherited docstring for the class docstring.""" - # func = self._get_class_dummy_func() func = None old_init_doc = None init_doc_changed = False @@ -95,9 +94,7 @@ def _inherit_class_docstring( init_method: Callable[..., None] = self._cls.__init__ # type: ignore # Ignore the case when __init__ is from object since there is no docstring # and its __doc__ cannot be assigned. - if init_method is not None and not isinstance( - init_method, WrapperDescriptorType - ): + if not isinstance(init_method, WrapperDescriptorType): old_init_doc = init_method.__doc__ init_method.__doc__ = self._cls.__doc__ func = init_method From d0af93bdfa1c98cd73a390c0e0d9b72d25ad8b43 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 12 Jan 2023 21:01:59 +0100 Subject: [PATCH 12/16] Update changelog and docs --- CHANGELOG.md | 12 ++++++++++++ README.md | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e974d0..c877ed6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2022-11 +### Added +- Support class docstrings with `__init__` signature description. +### Changed +- Some performance improvements. +### Fixed +- Meta-classes API no longer leaks into the classes they create. + +## [1.0.1] - 2022-11 +### Added +- Support for Python 3.11. + ## [1.0.0] - 2021-11 ### Changed - Metaclasses naming. diff --git a/README.md b/README.md index f7698f9..e919842 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ such that its derived classes fully or partly inherit the docstrings. - Handle docstrings for functions, classes, methods, class methods, static methods, properties. - Handle docstrings for classes with multiple or multi-level inheritance. - Docstring sections are inherited individually, - like methods for a classes. + like methods. - For docstring sections documenting signatures, the signature arguments are inherited individually. - Minimum performance cost: the inheritance is performed at import time, @@ -67,9 +67,15 @@ The docstring inheritance is performed for the docstrings of the: - staticmethods - properties -Use the `NumpyDocstringInheritanceMeta` metaclass to inherit docstrings in numpy format. +Use the `NumpyDocstringInheritanceMeta` metaclass to inherit docstrings in numpy format +if `__init__` method is documented in its own docstring. +Otherwise, if `__init__` method is documented in the class docstring, +use the `NumpyDocstringInheritanceInitMeta` metaclass. Use the `GoogleDocstringInheritanceMeta` metaclass to inherit docstrings in google format. +if `__init__` method is documented in its own docstring. +Otherwise, if `__init__` method is documented in the class docstring, +use the `GoogleDocstringInheritanceInitMeta` metaclass. ```python from docstring_inheritance import NumpyDocstringInheritanceMeta @@ -111,7 +117,7 @@ class Child(Parent): # The inherited docstring is -Child.meth.__doc__ = """Parent summary. +Child.meth.__doc__ == """Parent summary. Parameters ---------- @@ -171,7 +177,7 @@ def child(): inherit_google_docstring(parent.__doc__, child) # The inherited docstring is -child.__doc__ = """Parent summary. +child.__doc__ == """Parent summary. Args: x: Description for x. @@ -257,7 +263,7 @@ class Child(Parent): # The inherited docstring is -Child.__doc__ = """ +Child.__doc__ == """ Attributes ---------- x: @@ -309,7 +315,7 @@ class Child(Parent): # The inherited docstring is -Child.meth.__doc__ = """ +Child.meth.__doc__ == """ Args: w: Description for w y: Overridden description for y From a40441401d59cbbe2d553204512e717f8f6b64f8 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 12 Jan 2023 21:09:19 +0100 Subject: [PATCH 13/16] Add docstrings --- .../docstring_inheritors/base.py | 88 ++++++++++++++++--- 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/src/docstring_inheritance/docstring_inheritors/base.py b/src/docstring_inheritance/docstring_inheritors/base.py index 82d09d5..752ef26 100644 --- a/src/docstring_inheritance/docstring_inheritors/base.py +++ b/src/docstring_inheritance/docstring_inheritors/base.py @@ -47,7 +47,10 @@ def pairwise(iterable): class AbstractDocstringInheritor: - """Abstract base class for inheriting a docstring.""" + """Abstract base class for inheriting a docstring. + + This class produces a functor, it has no state and can only be called. + """ _SECTION_NAMES: ClassVar[list[str | None]] = [ None, @@ -71,8 +74,32 @@ class AbstractDocstringInheritor: MISSING_ARG_DESCRIPTION: ClassVar[str] = "The description is missing." + def __call__(self, parent_doc: str | None, child_func: Callable[..., Any]) -> None: + """ + Args: + parent_doc: The docstring of the parent. + child_func: The child function which docstring inherit from the parent. + """ + if parent_doc is None: + return + + parent_sections = self._parse_sections(parent_doc) + child_sections = self._parse_sections(child_func.__doc__) + child_sections = self._inherit_sections( + parent_sections, child_sections, child_func + ) + child_func.__doc__ = self._render_docstring(child_sections) + @classmethod def _get_section_body(cls, reversed_section_body_lines: list[str]) -> str: + """Create the docstring of a section. + + Args: + reversed_section_body_lines: The lines of docstrings in reversed order. + + Returns: + The docstring of a section. + """ reversed_section_body_lines = list( dropwhile(lambda x: not x, reversed_section_body_lines) ) @@ -98,21 +125,26 @@ def _parse_one_section( def _render_section( cls, section_name: str | None, section_body: str | dict[str, str] ) -> str: - """Return a rendered docstring section.""" + """Return a rendered docstring section. - def __call__(self, parent_doc: str | None, child_func: Callable[..., Any]) -> None: - if parent_doc is None: - return + Args: + section_name: The name of a docstring section. + section_body: The body of a docstring section. - parent_sections = self._parse_sections(parent_doc) - child_sections = self._parse_sections(child_func.__doc__) - child_sections = self._inherit_sections( - parent_sections, child_sections, child_func - ) - child_func.__doc__ = self._render_docstring(child_sections) + Returns: + The rendered docstring. + """ @classmethod def _parse_sections(cls, docstring: str | None) -> SectionsType: + """Parse the sections of a docstring. + + Args: + docstring: The docstring to parse. + + Returns: + The parsed sections. + """ if not docstring: return {} @@ -179,6 +211,16 @@ def _inherit_sections( child_sections: SectionsType, child_func: Callable[..., Any], ) -> SectionsType: + """Inherit the sections of a child from the parent sections. + + Args: + parent_sections: The parent docstring sections. + child_sections: The child docstring sections. + child_func: The child function which sections inherit from the parent. + + Returns: + The inherited sections. + """ # TODO: # prnt_only_raises = "Raises" in parent_sections and not ( # "Returns" in parent_sections or "Yields" in parent_sections @@ -249,6 +291,13 @@ def _inherit_section_items_with_args( The argument ``self`` is removed. The arguments are ordered according to the signature of ``func``. An argument of ``func`` missing in ``section_items`` gets a default description defined in :attr:`.MISSING_ARG_DESCRIPTION`. + + Args: + func: The function that provides the signature. + section_items: The docstring section items. + + Returns: + The section items filtered with the function signature. """ args, varargs, varkw, _, kwonlyargs = inspect.getfullargspec(func)[:5] @@ -275,6 +324,14 @@ def _inherit_section_items_with_args( @classmethod def _render_docstring(cls, sections: SectionsType) -> str: + """Render a docstring. + + Args: + sections: The docstring sections to render. + + Returns: + The rendered docstring. + """ if not sections: return "" @@ -298,5 +355,12 @@ def _render_docstring(cls, sections: SectionsType) -> str: @classmethod def _parse_section_items(cls, section_body: str) -> dict[str, str]: - """Parse the section items for numpy and google docstrings.""" + """Parse the section items for numpy and google docstrings. + + Args: + section_body: The body of a docstring section. + + Returns: + The parsed section body. + """ return dict(cls._SECTION_ITEMS_REGEX.findall(section_body)) From e72074b0fd053ebcb73c9b2b3ff160973af661b0 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 12 Jan 2023 21:09:43 +0100 Subject: [PATCH 14/16] Update precommit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc1c571..d643ea7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: check-symlinks - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: rst-backticks - id: rst-directive-colons From a292ee5a3c7c5720b6a4b74f96405731d9c36135 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 12 Jan 2023 21:12:12 +0100 Subject: [PATCH 15/16] Update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 53b2ba5..abbde00 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /.coverage /htmlcov /coverage.xml +/build From 0850550d85e9825a935fdaa34852107fc5b78d21 Mon Sep 17 00:00:00 2001 From: Antoine DECHAUME <> Date: Thu, 12 Jan 2023 21:14:49 +0100 Subject: [PATCH 16/16] Update test requirements --- requirements/test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/test.txt b/requirements/test.txt index 4f34cfc..dca251f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -8,7 +8,7 @@ attrs==22.2.0 # via pytest covdefaults==2.2.2 # via docstring-inheritance (setup.py) -coverage[toml]==7.0.3 +coverage[toml]==7.0.5 # via # covdefaults # pytest-cov @@ -20,7 +20,7 @@ importlib-metadata==6.0.0 # pytest iniconfig==2.0.0 # via pytest -packaging==22.0 +packaging==23.0 # via pytest pluggy==1.0.0 # via pytest