From 83c3946376ff975b5e0da2eb1bb78ab6c63c066d Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 1 Jan 2020 14:40:13 +0900 Subject: [PATCH] Close #6830: autodoc: consider a member private if docstring has "private" metadata --- CHANGES | 5 ++ doc/extdev/appapi.rst | 8 ++++ doc/usage/extensions/autodoc.rst | 14 ++++++ doc/usage/restructuredtext/domains.rst | 7 +++ sphinx/directives/__init__.py | 4 ++ sphinx/ext/autodoc/__init__.py | 33 +++++++++++-- sphinx/util/docstrings.py | 32 ++++++++++++- .../roots/test-ext-autodoc/target/private.py | 5 ++ tests/test_ext_autodoc_private_members.py | 46 +++++++++++++++++++ tests/test_util_docstrings.py | 29 +++++++++++- 10 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 tests/roots/test-ext-autodoc/target/private.py create mode 100644 tests/test_ext_autodoc_private_members.py diff --git a/CHANGES b/CHANGES index ef1ccf6f075..de3b7a06b6d 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,8 @@ Incompatible changes :confval:`autosummary_generate_overwrite` to change the behavior * #5923: autodoc: the members of ``object`` class are not documented by default when ``:inherited-members:`` and ``:special-members:`` are given. +* #6830: py domain: ``meta`` fields in info-field-list becomes reserved. They + are not displayed on output document now Deprecated ---------- @@ -29,8 +31,11 @@ Features added old stub file * #5923: autodoc: ``:inherited-members:`` option takes a name of anchestor class not to document inherited members of the class and uppers +* #6830: autodoc: consider a member private if docstring contains + ``:meta private:`` in info-field-list * #6558: glossary: emit a warning for duplicated glossary entry * #6558: std domain: emit a warning for duplicated generic objects +* #6830: py domain: Add new event: :event:`object-description-transform` Bugs fixed ---------- diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index 46540595f7b..c32eb142704 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -216,6 +216,14 @@ connect handlers to the events. Example: .. versionadded:: 0.5 +.. event:: object-description-transform (app, domain, objtype, contentnode) + + Emitted when an object description directive has run. The *domain* and + *objtype* arguments are strings indicating object description of the object. + And *contentnode* is a content for the object. It can be modified in-place. + + .. versionadded:: 3.0 + .. event:: doctree-read (app, doctree) Emitted when a doctree has been parsed and read by the environment, and is diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index f6aa5947c40..78852fe1ef2 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -140,6 +140,20 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, .. versionadded:: 1.1 + * autodoc considers a member private if its docstring contains + ``:meta private:`` in its :ref:`info-field-lists`. + For example: + + .. code-block:: rst + + def my_function(my_arg, my_other_arg): + """blah blah blah + + :meta private: + """ + + .. versionadded:: 3.0 + * Python "special" members (that is, those named like ``__special__``) will be included if the ``special-members`` flag option is given:: diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index c0ee3f230ab..e107acac195 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -354,6 +354,9 @@ Info field lists ~~~~~~~~~~~~~~~~ .. versionadded:: 0.4 +.. versionchanged:: 3.0 + + meta fields are added. Inside Python object description directives, reST field lists with these fields are recognized and formatted nicely: @@ -367,6 +370,10 @@ are recognized and formatted nicely: * ``vartype``: Type of a variable. Creates a link if possible. * ``returns``, ``return``: Description of the return value. * ``rtype``: Return type. Creates a link if possible. +* ``meta``: Add metadata to description of the python object. The metadata will + not be shown on output document. For example, ``:meta private:`` indicates + the python object is private member. It is used in + :py:mod:`sphinx.ext.autodoc` for filtering members. .. note:: diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 09390a6df7d..9a2fb441205 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -193,6 +193,8 @@ def run(self) -> List[Node]: self.env.temp_data['object'] = self.names[0] self.before_content() self.state.nested_parse(self.content, self.content_offset, contentnode) + self.env.app.emit('object-description-transform', + self.domain, self.objtype, contentnode) DocFieldTransformer(self).transform_all(contentnode) self.env.temp_data['object'] = None self.after_content() @@ -295,6 +297,8 @@ def setup(app: "Sphinx") -> Dict[str, Any]: # new, more consistent, name directives.register_directive('object', ObjectDescription) + app.add_event('object-description-transform') + return { 'version': 'builtin', 'parallel_read_safe': True, diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index ea6a235c9da..2ecbcae462d 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -14,7 +14,10 @@ import warnings from types import ModuleType from typing import Any, Callable, Dict, Iterator, List, Sequence, Set, Tuple, Union +from typing import cast +from docutils import nodes +from docutils.nodes import Element from docutils.statemachine import StringList import sphinx @@ -29,7 +32,7 @@ from sphinx.util import inspect from sphinx.util import logging from sphinx.util import rpartition -from sphinx.util.docstrings import prepare_docstring +from sphinx.util.docstrings import extract_metadata, prepare_docstring from sphinx.util.inspect import ( Signature, getdoc, object_description, safe_getattr, safe_getmembers ) @@ -560,6 +563,13 @@ def is_filtered_inherited_member(name: str) -> bool: doc = None has_doc = bool(doc) + metadata = extract_metadata(doc) + if 'private' in metadata: + # consider a member private if docstring has "private" metadata + isprivate = True + else: + isprivate = membername.startswith('_') + keep = False if want_all and membername.startswith('__') and \ membername.endswith('__') and len(membername) > 4: @@ -575,14 +585,14 @@ def is_filtered_inherited_member(name: str) -> bool: if membername in self.options.special_members: keep = has_doc or self.options.undoc_members elif (namespace, membername) in attr_docs: - if want_all and membername.startswith('_'): + if want_all and isprivate: # ignore members whose name starts with _ by default keep = self.options.private_members else: # keep documented attributes keep = True isattr = True - elif want_all and membername.startswith('_'): + elif want_all and isprivate: # ignore members whose name starts with _ by default keep = self.options.private_members and \ (has_doc or self.options.undoc_members) @@ -1524,6 +1534,21 @@ def autodoc_attrgetter(app: Sphinx, obj: Any, name: str, *defargs: Any) -> Any: return safe_getattr(obj, name, *defargs) +def filter_meta_fields(app: Sphinx, domain: str, objtype: str, content: Element) -> None: + """Filter ``:meta:`` field from its docstring.""" + if domain != 'py': + return + + for node in content: + if isinstance(node, nodes.field_list): + fields = cast(List[nodes.field], node) + for field in fields: + field_name = cast(nodes.field_body, field[0]).astext().strip() + if field_name == 'meta' or field_name.startswith('meta '): + node.remove(field) + break + + def setup(app: Sphinx) -> Dict[str, Any]: app.add_autodocumenter(ModuleDocumenter) app.add_autodocumenter(ClassDocumenter) @@ -1550,4 +1575,6 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_event('autodoc-process-signature') app.add_event('autodoc-skip-member') + app.connect('object-description-transform', filter_meta_fields) + return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py index 8854a1f98a7..0dca8945fbf 100644 --- a/sphinx/util/docstrings.py +++ b/sphinx/util/docstrings.py @@ -8,8 +8,38 @@ :license: BSD, see LICENSE for details. """ +import re import sys -from typing import List +from typing import Dict, List + +from docutils.parsers.rst.states import Body + + +field_list_item_re = re.compile(Body.patterns['field_marker']) + + +def extract_metadata(s: str) -> Dict[str, str]: + """Extract metadata from docstring.""" + in_other_element = False + metadata = {} # type: Dict[str, str] + + if not s: + return metadata + + for line in s.splitlines(): + if line.strip() == '': + in_other_element = False + else: + matched = field_list_item_re.match(line) + if matched and not in_other_element: + field_name = matched.group()[1:].split(':', 1)[0] + if field_name.startswith('meta '): + name = field_name[5:].strip() + metadata[name] = line[matched.end():].strip() + else: + in_other_element = True + + return metadata def prepare_docstring(s: str, ignore: int = 1, tabsize: int = 8) -> List[str]: diff --git a/tests/roots/test-ext-autodoc/target/private.py b/tests/roots/test-ext-autodoc/target/private.py new file mode 100644 index 00000000000..38f27666354 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/private.py @@ -0,0 +1,5 @@ +def private_function(name): + """private_function is a docstring(). + + :meta private: + """ diff --git a/tests/test_ext_autodoc_private_members.py b/tests/test_ext_autodoc_private_members.py new file mode 100644 index 00000000000..9f8374bdbe8 --- /dev/null +++ b/tests/test_ext_autodoc_private_members.py @@ -0,0 +1,46 @@ +""" + test_ext_autodoc_private_members + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Test the autodoc extension. This tests mainly for private-members option. + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import pytest + +from test_autodoc import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_private_field(app): + app.config.autoclass_content = 'class' + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.private', options) + assert list(actual) == [ + '', + '.. py:module:: target.private', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_private_field_and_private_members(app): + app.config.autoclass_content = 'class' + options = {"members": None, + "private-members": None} + actual = do_autodoc(app, 'module', 'target.private', options) + assert list(actual) == [ + '', + '.. py:module:: target.private', + '', + '', + '.. py:function:: private_function(name)', + ' :module: target.private', + '', + ' private_function is a docstring().', + ' ', + ' :private:', + ' ' + ] diff --git a/tests/test_util_docstrings.py b/tests/test_util_docstrings.py index bfd5b58b41c..2f0901d06cc 100644 --- a/tests/test_util_docstrings.py +++ b/tests/test_util_docstrings.py @@ -8,7 +8,34 @@ :license: BSD, see LICENSE for details. """ -from sphinx.util.docstrings import prepare_docstring, prepare_commentdoc +from sphinx.util.docstrings import ( + extract_metadata, prepare_docstring, prepare_commentdoc +) + + +def test_extract_metadata(): + metadata = extract_metadata(":meta foo: bar\n" + ":meta baz:\n") + assert metadata == {'foo': 'bar', 'baz': ''} + + # field_list like text following just after paragaph is not a field_list + metadata = extract_metadata("blah blah blah\n" + ":meta foo: bar\n" + ":meta baz:\n") + assert metadata == {} + + # field_list like text following after blank line is a field_list + metadata = extract_metadata("blah blah blah\n" + "\n" + ":meta foo: bar\n" + ":meta baz:\n") + assert metadata == {'foo': 'bar', 'baz': ''} + + # non field_list item breaks field_list + metadata = extract_metadata(":meta foo: bar\n" + "blah blah blah\n" + ":meta baz:\n") + assert metadata == {'foo': 'bar'} def test_prepare_docstring():