Skip to content

Commit

Permalink
Close sphinx-doc#6830: autodoc: consider a member private if docstrin…
Browse files Browse the repository at this point in the history
…g contains :private:
  • Loading branch information
tk0miya committed Jan 3, 2020
1 parent 1fc4e8c commit fe292be
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 5 deletions.
5 changes: 5 additions & 0 deletions CHANGES
Expand Up @@ -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
----------
Expand All @@ -27,6 +29,9 @@ 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
* #6830: py domain: Add new event: :event:`object-description-transform`

Bugs fixed
----------
Expand Down
8 changes: 8 additions & 0 deletions doc/extdev/appapi.rst
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions doc/usage/extensions/autodoc.rst
Expand Up @@ -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::

Expand Down
7 changes: 7 additions & 0 deletions doc/usage/restructuredtext/domains.rst
Expand Up @@ -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:
Expand All @@ -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::

Expand Down
4 changes: 4 additions & 0 deletions sphinx/directives/__init__.py
Expand Up @@ -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()
Expand Down Expand Up @@ -294,6 +296,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,
Expand Down
33 changes: 30 additions & 3 deletions sphinx/ext/autodoc/__init__.py
Expand Up @@ -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
Expand All @@ -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
)
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -1524,6 +1534,21 @@ def autodoc_attrgetter(app: Sphinx, obj: Any, name: str, *defargs) -> 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)
Expand All @@ -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}
32 changes: 31 additions & 1 deletion sphinx/util/docstrings.py
Expand Up @@ -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]:
Expand Down
5 changes: 5 additions & 0 deletions tests/roots/test-ext-autodoc/target/private.py
@@ -0,0 +1,5 @@
def private_function(name):
"""private_function is a docstring().
:meta private:
"""
46 changes: 46 additions & 0 deletions 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:',
' '
]
29 changes: 28 additions & 1 deletion tests/test_util_docstrings.py
Expand Up @@ -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():
Expand Down

0 comments on commit fe292be

Please sign in to comment.