diff --git a/.gitignore b/.gitignore index 1136ab4..abbde00 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /.tox /.coverage /htmlcov +/coverage.xml +/build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f2910e..d643ea7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,25 @@ 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 - exclude: LICENSES/headers + exclude: ^LICENSES/headers - id: check-added-large-files - id: check-toml - id: destroyed-symlinks - 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 - id: rst-inline-touching-normal - repo: https://github.com/PyCQA/autoflake - rev: v1.7.7 + rev: v2.0.0 hooks: - id: autoflake args: [ @@ -40,10 +40,10 @@ repos: ] - repo: https://github.com/myint/docformatter - rev: v1.5.0 + rev: v1.5.1 hooks: - id: docformatter - exclude: ^tests/.*$ + exclude: ^tests/ args: [ --in-place, --wrap-summaries, @@ -53,30 +53,33 @@ 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 + - 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/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 ba9f0c8..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,51 +67,57 @@ 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 class Parent(metaclass=NumpyDocstringInheritanceMeta): - def meth(self, x, y=None): - """Parent summary. + def meth(self, x, y=None): + """Parent summary. - Parameters - ---------- - x: - Description for x. - y: - Description for y. + Parameters + ---------- + x: + Description for x. + y: + Description for y. - Notes - ----- - Parent notes. - """ + Notes + ----- + Parent notes. + """ class Child(Parent): - def meth(self, x, z): - """ - Parameters - ---------- - z: - Description for z. + def meth(self, x, z): + """ + Parameters + ---------- + z: + Description for z. - Returns - ------- - Something. + Returns + ------- + Something. - Notes - ----- - Child notes. - """ + Notes + ----- + Child notes. + """ # The inherited docstring is -Child.meth.__doc__ = """Parent summary. +Child.meth.__doc__ == """Parent summary. Parameters ---------- @@ -144,35 +150,34 @@ 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. + """ inherit_google_docstring(parent.__doc__, child) - # The inherited docstring is -child.__doc__ = """Parent summary. +child.__doc__ == """Parent summary. Args: x: Description for x. @@ -236,29 +241,29 @@ 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 -Child.__doc__ = """ +Child.__doc__ == """ Attributes ---------- x: @@ -291,26 +296,26 @@ 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 -Child.meth.__doc__ = """ +Child.meth.__doc__ == """ Args: w: Description for w y: Overridden description for y @@ -336,11 +341,11 @@ from docstring_inheritance import NumpyDocstringInheritanceMeta class Meta(abc.ABCMeta, NumpyDocstringInheritanceMeta): - pass + pass class Parent(metaclass=Meta): - pass + pass ``` # Similar projects diff --git a/requirements/test.txt b/requirements/test.txt index 02a0710..dca251f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,31 +1,29 @@ # -# 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 +attrs==22.2.0 # via pytest -covdefaults==2.2.0 +covdefaults==2.2.2 # via docstring-inheritance (setup.py) -coverage[toml]==6.5.0 +coverage[toml]==7.0.5 # via # covdefaults # pytest-cov -exceptiongroup==1.0.4 +exceptiongroup==1.1.0 # via pytest -importlib-metadata==5.0.0 +importlib-metadata==6.0.0 # via # pluggy # pytest -iniconfig==1.1.1 +iniconfig==2.0.0 # via pytest -packaging==21.3 +packaging==23.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 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/__init__.py b/src/docstring_inheritance/__init__.py index 02bd2f8..89270cd 100644 --- a/src/docstring_inheritance/__init__.py +++ b/src/docstring_inheritance/__init__.py @@ -19,31 +19,104 @@ # 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_docstrings_inheritor import ClassDocstringsInheritor +from .class_docstrings_inheritor import DocstringInheritor +from .docstring_inheritors.google import GoogleDocstringInheritor +from .docstring_inheritors.numpy import NumpyDocstringInheritor -DocstringProcessorType = Callable[[Optional[str], Callable], None] +inherit_numpy_docstring = NumpyDocstringInheritor() +inherit_google_docstring = GoogleDocstringInheritor() -inherit_numpy_docstring = NumpyDocstringProcessor() -inherit_google_docstring = GoogleDocstringProcessor() +class _BaseDocstringInheritanceMeta(type): + """Base metaclass for inheriting class docstrings.""" -def DocstringInheritanceMeta( # noqa: N802 - docstring_processor: DocstringProcessorType, -) -> type: - metaclass = type( - AbstractDocstringInheritanceMeta.__name__, - AbstractDocstringInheritanceMeta.__bases__, - dict(AbstractDocstringInheritanceMeta.__dict__), - ) - metaclass.docstring_processor = docstring_processor - return metaclass + def __init__( + cls, + class_name: str, + 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: + ClassDocstringsInheritor.inherit_docstring( + cls, docstring_inheritor, init_in_class + ) -# 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, + 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): + """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, + 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 new file mode 100644 index 0000000..f620562 --- /dev/null +++ b/src/docstring_inheritance/class_docstrings_inheritor.py @@ -0,0 +1,149 @@ +# 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 types import FunctionType +from types import WrapperDescriptorType +from typing import Any +from typing import Callable +from typing import Optional + +DocstringInheritor = Callable[[Optional[str], Callable[..., Any]], None] + + +class ClassDocstringsInheritor: + """A class for inheriting class docstrings.""" + + _cls: type + """The class to process.""" + + _docstring_inheritor: DocstringInheritor + """The docstring inheritor.""" + + _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_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, + # 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 = init_in_class + + @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 = 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 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__ + + 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.""" + 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._docstring_inheritor(parent_doc, attr) + + @staticmethod + def _create_dummy_func_with_doc(docstring: str | None) -> Callable[..., Any]: + """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_inheritors/__init__.py similarity index 79% rename from src/docstring_inheritance/processors/__init__.py rename to src/docstring_inheritance/docstring_inheritors/__init__.py index 0c498e2..67879f8 100644 --- a/src/docstring_inheritance/processors/__init__.py +++ b/src/docstring_inheritance/docstring_inheritors/__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_inheritors/base.py similarity index 69% rename from src/docstring_inheritance/processors/base.py rename to src/docstring_inheritance/docstring_inheritors/base.py index 29af4a0..752ef26 100644 --- a/src/docstring_inheritance/processors/base.py +++ b/src/docstring_inheritance/docstring_inheritors/base.py @@ -21,9 +21,14 @@ import abc import inspect +import re 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 from typing import Union @@ -41,23 +46,65 @@ def pairwise(iterable): return zip(a, b) -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] +class AbstractDocstringInheritor: + """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, + "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]] + + 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 - # Description without formatting. - MISSING_ARG_DESCRIPTION = "The description is missing." + 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 - @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. + """Create the docstring of a section. + + Args: + reversed_section_body_lines: The lines of docstrings in reversed order. - The trailing empty lines are removed. + Returns: + The docstring of a section. """ + 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 @@ -70,7 +117,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 @@ -78,26 +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. - @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 + 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 {} @@ -152,8 +199,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 @@ -162,8 +209,18 @@ def _inherit_sections( cls, parent_sections: SectionsType, child_sections: SectionsType, - child_func: Callable, + 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 @@ -196,8 +253,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( @@ -214,7 +275,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 @@ -222,14 +283,21 @@ 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. - 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: + 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] @@ -256,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 "" @@ -272,3 +348,19 @@ 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. + + Args: + section_body: The body of a docstring section. + + Returns: + The parsed section body. + """ + return dict(cls._SECTION_ITEMS_REGEX.findall(section_body)) diff --git a/src/docstring_inheritance/processors/google.py b/src/docstring_inheritance/docstring_inheritors/google.py similarity index 79% rename from src/docstring_inheritance/processors/google.py rename to src/docstring_inheritance/docstring_inheritors/google.py index 0123783..2ff83ac 100644 --- a/src/docstring_inheritance/processors/google.py +++ b/src/docstring_inheritance/docstring_inheritors/google.py @@ -20,33 +20,33 @@ from __future__ import annotations import textwrap +from typing import ClassVar -from . import parse_section_items -from .base import AbstractDocstringProcessor -from .numpy import NumpyDocstringProcessor +from .base import AbstractDocstringInheritor +from .numpy import NumpyDocstringInheritor -class GoogleDocstringProcessor(AbstractDocstringProcessor): - _SECTION_NAMES = list(NumpyDocstringProcessor._SECTION_NAMES) +class GoogleDocstringInheritor(AbstractDocstringInheritor): + """A class for inheriting docstrings in Google format.""" + + _SECTION_NAMES: ClassVar[list[str | None]] = list( + AbstractDocstringInheritor._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", } - 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) + 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 @@ -71,6 +71,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/docstring_inheritors/numpy.py similarity index 67% rename from src/docstring_inheritance/processors/numpy.py rename to src/docstring_inheritance/docstring_inheritors/numpy.py index 7976d75..ee48975 100644 --- a/src/docstring_inheritance/processors/numpy.py +++ b/src/docstring_inheritance/docstring_inheritors/numpy.py @@ -19,47 +19,27 @@ # SOFTWARE. from __future__ import annotations -from itertools import dropwhile +from typing import ClassVar -from . import parse_section_items -from .base import AbstractDocstringProcessor +from .base import AbstractDocstringInheritor -class NumpyDocstringProcessor(AbstractDocstringProcessor): +class NumpyDocstringInheritor(AbstractDocstringInheritor): + """A class for inheriting docstrings in Numpy format.""" - _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}" - - @classmethod - def _parse_section_items(cls, section_body: str) -> dict[str, str]: - return parse_section_items(section_body) + MISSING_ARG_DESCRIPTION: ClassVar[ + str + ] = f":\n{AbstractDocstringInheritor.MISSING_ARG_DESCRIPTION}" @classmethod def _parse_one_section( @@ -75,19 +55,12 @@ def _parse_one_section( return line1s, cls._get_section_body(reversed_section_body_lines) return None, None - @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) - @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( diff --git a/src/docstring_inheritance/metaclass.py b/src/docstring_inheritance/metaclass.py deleted file mode 100644 index 7fe1b07..0000000 --- a/src/docstring_inheritance/metaclass.py +++ /dev/null @@ -1,88 +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 types import FunctionType -from typing import Any -from typing import Callable - - -def create_dummy_func_with_doc(docstring: str | None) -> Callable: - """Return a dummy function with a given docstring.""" - - def func(): # pragma: no cover - pass - - func.__doc__ = docstring - return func - - -class AbstractDocstringInheritanceMeta(type): - """Abstract metaclass for inheriting class docstrings.""" - - docstring_processor: Callable[[str | None, Callable], str] - - 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) - 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] - ) -> None: - dummy_func = create_dummy_func_with_doc(class_dict.get("__doc__")) - - for base_class in cls._get_mro_classes(class_bases): - cls.docstring_processor(base_class.__doc__, dummy_func) - - class_dict["__doc__"] = dummy_func.__doc__ - - @classmethod - def _process_attrs_docstrings( - cls, class_bases: tuple[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): - continue - - parent_doc = getattr(mro_cls, attr_name).__doc__ - if parent_doc is not None: - break - else: - continue - - 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 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_base_processor.py b/tests/test_base_inheritor.py similarity index 89% rename from tests/test_base_processor.py rename to tests/test_base_inheritor.py index 9529796..d8b8008 100644 --- a/tests/test_base_processor.py +++ b/tests/test_base_inheritor.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_inheritors.base import AbstractDocstringInheritor 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 ( + AbstractDocstringInheritor._parse_section_items(section_body) + == expected_matches + ) def func_none(): @@ -112,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. ( @@ -124,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, @@ -135,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, @@ -146,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, @@ -181,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 689d3cf..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.metaclass import create_dummy_func_with_doc +from docstring_inheritance.class_docstrings_inheritor import ClassDocstringsInheritor def test_side_effect(): @@ -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_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 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 043be94..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.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 ) diff --git a/tox.ini b/tox.ini index 9d5135f..0ad1f2f 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} @@ -16,10 +15,10 @@ 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 +allowlist_externals = rm commands = rm -rf dist build python -m build @@ -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 =