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 1, 2020
1 parent 2e8dea1 commit 3364f28
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 3 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Expand Up @@ -29,6 +29,9 @@ Features added
down the build
* #6837: LaTeX: Support a nested table
* #6966: graphviz: Support ``:class:`` option
* #6830: autodoc: consider a member private if docstring contains ``: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 @@ -218,6 +218,14 @@ connect handlers to the events. Example:

.. versionadded:: 0.5

.. event:: object-description-transform (app, domain, objtype, contentnode)

Emitted when a 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:: 2.4

.. 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 ``:private:``
in its :ref:`info-field-lists`.
For example:

.. code-block:: rst
def my_function(my_arg, my_other_arg):
"""A function just for me.
:private:
"""
.. versionadded:: 2.4

* Python "special" members (that is, those named like ``__special__``) will
be included if the ``special-members`` flag option is given::

Expand Down
4 changes: 4 additions & 0 deletions doc/usage/restructuredtext/domains.rst
Expand Up @@ -354,6 +354,9 @@ Info field lists
~~~~~~~~~~~~~~~~

.. versionadded:: 0.4
.. versionchanged:: 2.4

private field has been added.

Inside Python object description directives, reST field lists with these fields
are recognized and formatted nicely:
Expand All @@ -367,6 +370,7 @@ 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.
* ``private``: Indicates the Python object is private member.

.. 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 @@ -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,
Expand Down
31 changes: 28 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 @@ -31,7 +34,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 is_private_member, prepare_docstring
from sphinx.util.inspect import (
Signature, getdoc, object_description, safe_getattr, safe_getmembers
)
Expand Down Expand Up @@ -543,6 +546,12 @@ def filter_members(self, members: List[Tuple[str, Any]], want_all: bool
doc = None
has_doc = bool(doc)

if has_doc and is_private_member(doc):
# consider a member private if docstring contains ``:private:`` field
isprivate = True
else:
isprivate = membername.startswith('_')

keep = False
if want_all and membername.startswith('__') and \
membername.endswith('__') and len(membername) > 4:
Expand All @@ -555,14 +564,14 @@ def filter_members(self, members: List[Tuple[str, Any]], want_all: bool
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 +1533,21 @@ def merge_autodoc_default_flags(app: Sphinx, config: Config) -> None:
)


def filter_private_field(app: Sphinx, domain: str, objtype: str, content: Element) -> None:
"""Filter ``:private:`` 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])
if field_name.astext() == 'private':
node.remove(field)
break


from sphinx.ext.autodoc.mock import _MockImporter # NOQA

deprecated_alias('sphinx.ext.autodoc',
Expand Down Expand Up @@ -1560,5 +1584,6 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_event('autodoc-skip-member')

app.connect('config-inited', merge_autodoc_default_flags)
app.connect('object-description-transform', filter_private_field)

return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
7 changes: 7 additions & 0 deletions sphinx/util/docstrings.py
Expand Up @@ -8,6 +8,7 @@
:license: BSD, see LICENSE for details.
"""

import re
import sys
from typing import List

Expand Down Expand Up @@ -61,3 +62,9 @@ def prepare_commentdoc(s: str) -> List[str]:
if result and result[-1]:
result.append('')
return result


def is_private_member(s: str) -> bool:
"""Check given docstring *s* contains ``:private:`` field-list item or not."""
s = '\n'.join(prepare_docstring(s))
return bool(re.search('(\\A|\n):private:(\\Z|\n)', s))
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().
: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:',
' '
]

0 comments on commit 3364f28

Please sign in to comment.