From f2686652929a6c84f37e3850b640c8b7bc47f746 Mon Sep 17 00:00:00 2001 From: Quentin Soubeyran <45202794+QuentinSoubeyran@users.noreply.github.com> Date: Mon, 5 Oct 2020 14:15:01 +0200 Subject: [PATCH] added napoleon_google_attr_annotations option to use PEP 526 on google style --- doc/usage/extensions/example_google.py | 18 +++++++++++++ doc/usage/extensions/napoleon.rst | 36 +++++++++++++++++++++++++- sphinx/ext/napoleon/__init__.py | 7 ++++- sphinx/ext/napoleon/docstring.py | 23 +++++++++++++++- tests/test_ext_napoleon_docstring.py | 36 ++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 3 deletions(-) diff --git a/doc/usage/extensions/example_google.py b/doc/usage/extensions/example_google.py index 97ffe8a05..98da3870a 100644 --- a/doc/usage/extensions/example_google.py +++ b/doc/usage/extensions/example_google.py @@ -294,3 +294,21 @@ class ExampleClass: def _private_without_docstring(self): pass + +class ExamplePEP526Class: + """The summary line for a class docstring should fit on one line. + + If the class has public attributes, they may be documented here + in an ``Attributes`` section and follow the same formatting as a + function's ``Args`` section. If ``napoleon_google_attr_annotations`` + is True, types can be specified in the class body using ``PEP 526`` + annotations. + + Attributes: + attr1: Description of `attr1`. + attr2: Description of `attr2`. + + """ + + attr1: str + attr2: int \ No newline at end of file diff --git a/doc/usage/extensions/napoleon.rst b/doc/usage/extensions/napoleon.rst index b16577e2d..550f8b591 100644 --- a/doc/usage/extensions/napoleon.rst +++ b/doc/usage/extensions/napoleon.rst @@ -203,7 +203,8 @@ Type Annotations This is an alternative to expressing types directly in docstrings. One benefit of expressing types according to `PEP 484`_ is that type checkers and IDEs can take advantage of them for static code -analysis. +analysis. `PEP 484`_ was then extended by `PEP 526`_ which introduced +a similar way to annotate variables (and attributes). Google style with Python 3 type annotations:: @@ -221,6 +222,19 @@ Google style with Python 3 type annotations:: """ return True + + class Class: + """Summary line. + + Extended description of class + + Attributes: + attr1: Description of attr1 + attr2: Description of attr2 + """ + + attr1: int + attr2: str Google style with types in docstrings:: @@ -238,6 +252,16 @@ Google style with types in docstrings:: """ return True + + class Class: + """Summary line. + + Extended description of class + + Attributes: + attr1 (int): Description of attr1 + attr2 (str): Description of attr2 + """ .. Note:: `Python 2/3 compatible annotations`_ aren't currently @@ -246,6 +270,9 @@ Google style with types in docstrings:: .. _PEP 484: https://www.python.org/dev/peps/pep-0484/ +.. _PEP 526: + https://www.python.org/dev/peps/pep-0526/ + .. _Python 2/3 compatible annotations: https://www.python.org/dev/peps/pep-0484/#suggested-syntax-for-python-2-7-and-straddling-code @@ -275,6 +302,7 @@ sure that "sphinx.ext.napoleon" is enabled in `conf.py`:: napoleon_use_param = True napoleon_use_rtype = True napoleon_type_aliases = None + napoleon_google_attr_annotations = False .. _Google style: https://google.github.io/styleguide/pyguide.html @@ -511,3 +539,9 @@ sure that "sphinx.ext.napoleon" is enabled in `conf.py`:: :type arg2: :term:`dict-like ` .. versionadded:: 3.2 + +.. confval:: napoleon_google_attr_annotations + + True to allow using `PEP 526`_ attributes annotations in classes. + If an attribute is documented in the docstring without a type and + has an annotation in the class body, that type is used. \ No newline at end of file diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py index e2ff5439d..febeb0197 100644 --- a/sphinx/ext/napoleon/__init__.py +++ b/sphinx/ext/napoleon/__init__.py @@ -44,6 +44,7 @@ class Config: napoleon_preprocess_types = False napoleon_type_aliases = None napoleon_custom_sections = None + napoleon_google_attr_annotations = False .. _Google style: https://google.github.io/styleguide/pyguide.html @@ -257,6 +258,9 @@ class Config: section. If the entry is a tuple/list/indexed container, the first entry is the name of the section, the second is the section key to emulate. + napoleon_google_attr_annotations : :obj:`bool` (Defaults to False) + Use the type annotations of class attributes that are documented in the docstring + but do not have a type in the docstring. """ _config_values = { @@ -274,7 +278,8 @@ class Config: 'napoleon_use_keyword': (True, 'env'), 'napoleon_preprocess_types': (False, 'env'), 'napoleon_type_aliases': (None, 'env'), - 'napoleon_custom_sections': (None, 'env') + 'napoleon_custom_sections': (None, 'env'), + 'napoleon_google_attr_annotations': (False, 'env'), } def __init__(self, **settings: Any) -> None: diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index ddcf3f01b..d6605cc96 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -14,13 +14,14 @@ import collections import inspect import re from functools import partial -from typing import Any, Callable, Dict, List, Tuple, Union +from typing import Any, Callable, Dict, List, Tuple, Union, get_type_hints from sphinx.application import Sphinx from sphinx.config import Config as SphinxConfig from sphinx.ext.napoleon.iterators import modify_iter from sphinx.locale import _, __ from sphinx.util import logging +from sphinx.util.inspect import safe_getattr, stringify_annotation if False: # For type annotation @@ -600,6 +601,26 @@ class GoogleDocstring: def _parse_attributes_section(self, section: str) -> List[str]: lines = [] for _name, _type, _desc in self._consume_fields(): + # code adapted from autodoc.AttributeDocumenter:add_directive_header + if not _type and self._what == 'class' and self._obj: + if self._config.napoleon_google_attr_annotations: + # cache the class annotations + if not hasattr(self, "_annotations"): + try: + self._annotations = get_type_hints(self._obj) + except NameError: + # Failed to evaluate ForwardRef (maybe TYPE_CHECKING) + self._annotations = safe_getattr(self._obj, '__annotations__', {}) + except TypeError: + self._annotations = {} + except KeyError: + # a broken class found (refs: https://github.com/sphinx-doc/sphinx/issues/8084) + self._annotations = {} + except AttributeError: + # AttributeError is raised on 3.5.2 (fixed by 3.5.3) + self._annotations = {} + if _name in self._annotations: + _type = stringify_annotation(self._annotations[_name]) if self._config.napoleon_use_ivar: _name = self._qualify_name(_name, self._obj) field = ':ivar %s: ' % _name diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index 220a394d4..1ba66da19 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -45,6 +45,18 @@ class NamedtupleSubclass(namedtuple('NamedtupleSubclass', ('attr1', 'attr2'))): return super().__new__(cls, attr1, attr2) +class PEP526Class: + """Sample class with PEP 526 annotations + + Attributes: + attr1: Attr1 description. + attr2: Attr2 description. + """ + + attr1: int + attr2: str + + class BaseDocstringTest(TestCase): pass @@ -1091,6 +1103,30 @@ Do as you please :kwtype gotham_is_yours: None """ self.assertEqual(expected, actual) + + def test_pep_526_annotations(self): + config = Config( + napoleon_google_attr_annotations=True + ) + actual = str(GoogleDocstring(cleandoc(PEP526Class.__doc__), config, app=None, what="class", + obj=PEP526Class)) + print(actual) + expected = """\ +Sample class with PEP 526 annotations + +.. attribute:: attr1 + + Attr1 description. + + :type: int + +.. attribute:: attr2 + + Attr2 description. + + :type: str +""" + self.assertEqual(expected, actual) class NumpyDocstringTest(BaseDocstringTest):