diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index 385254734..2b2a5f3ac 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -341,7 +341,8 @@ There are also new config values that you can set: This value is a list of autodoc directive flags that should be automatically applied to all autodoc directives. The supported flags are ``'members'``, ``'undoc-members'``, ``'private-members'``, ``'special-members'``, - ``'inherited-members'``, ``'show-inheritance'`` and ``'ignore-module-all'``. + ``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'`` + and ``'exclude-members'``. If you set one of these flags in this config value, you can use a negated form, :samp:`'no-{flag}'`, in an autodoc directive, to disable it once. @@ -353,8 +354,26 @@ There are also new config values that you can set: the directive will be interpreted as if only ``:members:`` was given. + You can also set `autodoc_default_flags` to a dictionary, mapping option + names to the values which can used in .rst files. For example:: + + autodoc_default_flags = { + 'members': 'var1, var2', + 'member-order': 'bysource', + 'special-members': '__init__', + 'undoc-members': None, + 'exclude-members': '__weakref__' + } + + Setting ``None`` is equivalent to giving the option name in the list format + (i.e. it means "yes/true/on"). + .. versionadded:: 1.0 + .. versionchanged:: 1.8 + + Specifying in dictionary format added. + .. confval:: autodoc_docstring_signature Functions imported from C modules cannot be introspected, and therefore the diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 4a9b59537..6671df579 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -15,6 +15,7 @@ import inspect import re import sys import warnings +from typing import Any from docutils.statemachine import ViewList from six import iteritems, itervalues, text_type, class_types, string_types @@ -41,6 +42,7 @@ if False: from docutils import nodes # NOQA from docutils.utils import Reporter # NOQA from sphinx.application import Sphinx # NOQA + from sphinx.config import Config # NOQA from sphinx.environment import BuildEnvironment # NOQA from sphinx.ext.autodoc.directive import DocumenterBridge # NOQA @@ -679,8 +681,13 @@ class Documenter(object): # remove members given by exclude-members if self.options.exclude_members: - members = [(membername, member) for (membername, member) in members - if membername not in self.options.exclude_members] + members = [ + (membername, member) for (membername, member) in members + if ( + self.options.exclude_members is ALL or + membername not in self.options.exclude_members + ) + ] # document non-skipped members memberdocumenters = [] # type: List[Tuple[Documenter, bool]] @@ -1528,6 +1535,34 @@ def autodoc_attrgetter(app, obj, name, *defargs): return safe_getattr(obj, name, *defargs) +def convert_autodoc_default_flags(app, config): + # type: (Sphinx, Config) -> None + """This converts the old list-of-flags (strings) to a dict of Nones.""" + if isinstance(config.autodoc_default_flags, dict): + # Already new-style + return + + elif not isinstance(config.autodoc_default_flags, list): + # Not old-style list + logger.error( + __("autodoc_default_flags is invalid type %r"), + config.autodoc_default_flags.__class__.__name__ + ) + return + + autodoc_default_flags = {} # type: Dict[unicode, Any] + for option in config.autodoc_default_flags: + if isinstance(option, string_types): + autodoc_default_flags[option] = None + else: + logger.warning( + __("Ignoring invalid option in autodoc_default_flags: %r"), + option + ) + + config.autodoc_default_flags = autodoc_default_flags # type: ignore + + def setup(app): # type: (Sphinx) -> Dict[unicode, Any] app.add_autodocumenter(ModuleDocumenter) @@ -1541,7 +1576,7 @@ def setup(app): app.add_config_value('autoclass_content', 'class', True) app.add_config_value('autodoc_member_order', 'alphabetic', True) - app.add_config_value('autodoc_default_flags', [], True) + app.add_config_value('autodoc_default_flags', {}, True, Any) app.add_config_value('autodoc_docstring_signature', True, True) app.add_config_value('autodoc_mock_imports', [], True) app.add_config_value('autodoc_warningiserror', True, True) @@ -1550,4 +1585,6 @@ def setup(app): app.add_event('autodoc-process-signature') app.add_event('autodoc-skip-member') + app.connect('config-inited', convert_autodoc_default_flags) + return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index 64d19fcc7..34f7567d4 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -31,7 +31,7 @@ logger = logging.getLogger(__name__) # common option names for autodoc directives AUTODOC_DEFAULT_OPTIONS = ['members', 'undoc-members', 'inherited-members', 'show-inheritance', 'private-members', 'special-members', - 'ignore-module-all'] + 'ignore-module-all', 'exclude-members'] class DummyOptionSpec(object): @@ -68,7 +68,7 @@ def process_documenter_options(documenter, config, options): else: negated = options.pop('no-' + name, True) is None if name in config.autodoc_default_flags and not negated: - options[name] = None + options[name] = config.autodoc_default_flags[name] return Options(assemble_option_dict(options.items(), documenter.option_spec)) diff --git a/tests/roots/test-ext-autodoc/target/__init__.py b/tests/roots/test-ext-autodoc/target/__init__.py index d94665bbf..201e84efd 100644 --- a/tests/roots/test-ext-autodoc/target/__init__.py +++ b/tests/roots/test-ext-autodoc/target/__init__.py @@ -234,3 +234,18 @@ class EnumCls(enum.Enum): val3 = 34 """doc for val3""" val4 = 34 + + +class CustomIter(object): + def __init__(self): + """Create a new `CustomIter`.""" + self.values = range(10) + + def __iter__(self): + """Iterate squares of each value.""" + for i in self.values: + yield i ** 2 + + def snafucate(self): + """Makes this snafucated.""" + print("snafucated") diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index d3e69766f..0cd543ad7 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -11,6 +11,7 @@ """ import re +import platform import sys from warnings import catch_warnings @@ -19,7 +20,8 @@ from docutils.statemachine import ViewList from six import PY3 from sphinx.ext.autodoc import ( - AutoDirective, ModuleLevelDocumenter, cut_lines, between, ALL + AutoDirective, ModuleLevelDocumenter, cut_lines, between, ALL, + convert_autodoc_default_flags ) from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options from sphinx.testing.util import SphinxTestApp, Struct # NOQA @@ -33,8 +35,12 @@ if PY3: else: ROGER_METHOD = ' .. py:classmethod:: Class.roger(a, e=5, f=6)' +IS_PYPY = platform.python_implementation() == 'PyPy' -def do_autodoc(app, objtype, name, options={}): + +def do_autodoc(app, objtype, name, options=None): + if options is None: + options = {} doccls = app.registry.documenters[objtype] docoptions = process_documenter_options(doccls, app.config, options) bridge = DocumenterBridge(app.env, LoggingReporter(''), docoptions, 1) @@ -1412,21 +1418,146 @@ def test_partialmethod(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') -def test_autodoc_default_flags(app): +def test_autodoc_default_flags__as_list__converted(app): + orig = [ + 'members', + 'undoc-members', + ('skipped', 1, 2), + {'also': 'skipped'}, + ] + expected = { + 'members': None, + 'undoc-members': None, + } + app.config.autodoc_default_flags = orig + convert_autodoc_default_flags(app, app.config) + assert app.config.autodoc_default_flags == expected + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_default_flags__as_dict__no_conversion(app): + orig = { + 'members': 'this,that,other', + 'undoc-members': None, + } + app.config.autodoc_default_flags = orig + convert_autodoc_default_flags(app, app.config) + assert app.config.autodoc_default_flags == orig + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_default_flags__with_flags(app): # no settings actual = do_autodoc(app, 'class', 'target.EnumCls') assert ' .. py:attribute:: EnumCls.val1' not in actual assert ' .. py:attribute:: EnumCls.val4' not in actual + actual = do_autodoc(app, 'class', 'target.CustomIter') + assert ' .. py:method:: target.CustomIter' not in actual # with :members: - app.config.autodoc_default_flags = ['members'] + app.config.autodoc_default_flags = {'members': None} actual = do_autodoc(app, 'class', 'target.EnumCls') assert ' .. py:attribute:: EnumCls.val1' in actual assert ' .. py:attribute:: EnumCls.val4' not in actual # with :members: and :undoc-members: - app.config.autodoc_default_flags = ['members', - 'undoc-members'] + app.config.autodoc_default_flags = { + 'members': None, + 'undoc-members': None, + } actual = do_autodoc(app, 'class', 'target.EnumCls') assert ' .. py:attribute:: EnumCls.val1' in actual assert ' .. py:attribute:: EnumCls.val4' in actual + + # with :special-members: + # Note that :members: must be *on* for :special-members: to work. + app.config.autodoc_default_flags = { + 'members': None, + 'special-members': None + } + actual = do_autodoc(app, 'class', 'target.CustomIter') + assert ' .. py:method:: CustomIter.__init__()' in actual + assert ' Create a new `CustomIter`.' in actual + assert ' .. py:method:: CustomIter.__iter__()' in actual + assert ' Iterate squares of each value.' in actual + if not IS_PYPY: + assert ' .. py:attribute:: CustomIter.__weakref__' in actual + assert ' list of weak references to the object (if defined)' in actual + + # :exclude-members: None - has no effect. Unlike :members:, + # :special-members:, etc. where None == "include all", here None means + # "no/false/off". + app.config.autodoc_default_flags = { + 'members': None, + 'exclude-members': None, + } + actual = do_autodoc(app, 'class', 'target.EnumCls') + assert ' .. py:attribute:: EnumCls.val1' in actual + assert ' .. py:attribute:: EnumCls.val4' not in actual + app.config.autodoc_default_flags = { + 'members': None, + 'special-members': None, + 'exclude-members': None, + } + actual = do_autodoc(app, 'class', 'target.CustomIter') + assert ' .. py:method:: CustomIter.__init__()' in actual + assert ' Create a new `CustomIter`.' in actual + assert ' .. py:method:: CustomIter.__iter__()' in actual + assert ' Iterate squares of each value.' in actual + if not IS_PYPY: + assert ' .. py:attribute:: CustomIter.__weakref__' in actual + assert ' list of weak references to the object (if defined)' in actual + assert ' .. py:method:: CustomIter.snafucate()' in actual + assert ' Makes this snafucated.' in actual + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_default_flags__with_values(app): + # with :members: + app.config.autodoc_default_flags = {'members': 'val1,val2'} + actual = do_autodoc(app, 'class', 'target.EnumCls') + assert ' .. py:attribute:: EnumCls.val1' in actual + assert ' .. py:attribute:: EnumCls.val2' in actual + assert ' .. py:attribute:: EnumCls.val3' not in actual + assert ' .. py:attribute:: EnumCls.val4' not in actual + + # with :special-members: + # Note that :members: must be *on* for :special-members: to work. + app.config.autodoc_default_flags = { + 'members': None, + 'special-members': '__init__,__iter__', + } + actual = do_autodoc(app, 'class', 'target.CustomIter') + assert ' .. py:method:: CustomIter.__init__()' in actual + assert ' Create a new `CustomIter`.' in actual + assert ' .. py:method:: CustomIter.__iter__()' in actual + assert ' Iterate squares of each value.' in actual + if not IS_PYPY: + assert ' .. py:attribute:: CustomIter.__weakref__' not in actual + assert ' list of weak references to the object (if defined)' not in actual + + # with :exclude-members: + app.config.autodoc_default_flags = { + 'members': None, + 'exclude-members': 'val1' + } + actual = do_autodoc(app, 'class', 'target.EnumCls') + assert ' .. py:attribute:: EnumCls.val1' not in actual + assert ' .. py:attribute:: EnumCls.val2' in actual + assert ' .. py:attribute:: EnumCls.val3' in actual + assert ' .. py:attribute:: EnumCls.val4' not in actual + app.config.autodoc_default_flags = { + 'members': None, + 'special-members': None, + 'exclude-members': '__weakref__,snafucate', + } + actual = do_autodoc(app, 'class', 'target.CustomIter') + assert ' .. py:method:: CustomIter.__init__()' in actual + assert ' Create a new `CustomIter`.' in actual + assert ' .. py:method:: CustomIter.__iter__()' in actual + assert ' Iterate squares of each value.' in actual + if not IS_PYPY: + assert ' .. py:attribute:: CustomIter.__weakref__' not in actual + assert ' list of weak references to the object (if defined)' not in actual + assert ' .. py:method:: CustomIter.snafucate()' not in actual + assert ' Makes this snafucated.' not in actual