From 87029392fd73521a3d25e3ceba363c19d93a3c7a Mon Sep 17 00:00:00 2001 From: Lewis Haley Date: Thu, 26 Jul 2018 11:43:28 +0100 Subject: [PATCH 1/3] test_autodoc: fix mutable function default argument in do_autodoc Setting mutable types as default arguments is bad practice because the value is only initialised once. This means that defaults arguments of lists and dictionaries which are modified during code execution *stay* modified between calls. In this case, the `options` dictionary accumulated options as more and more test cases were executed. Without this change, the tests added in the next commit do not pass. See: https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument --- tests/test_autodoc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index d3e69766f..a54c9c2e6 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -34,7 +34,9 @@ else: ROGER_METHOD = ' .. py:classmethod:: Class.roger(a, e=5, f=6)' -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) From 6e1e35c98ac29397d4552caf72710ccf4bf98bea Mon Sep 17 00:00:00 2001 From: Lewis Haley Date: Wed, 8 Aug 2018 16:26:11 +0100 Subject: [PATCH 2/3] autodoc: allow specifying values to global arguments Previously, users could specify a *list* of flags in their config files. The flags were directive names that would otherwise be present in the .rst files. However, as a list, it was not possible to specify values to those flags, which *is* possible in .rst files. For example, in .rst you could say :special-members: __init__, __iter__ And this would cause autodoc to generate documents for these methods that it would otherwise ignore. This commit changes the config option to instead accept a dictionary. This is a dictionary whose keys can contain the same flag-names as before, but whose values can contain the arguments as seen in .rst files. The old list is still supported, for backwards compatibility, but the data is transformed into a dictionary when the user's config is loaded. --- doc/usage/extensions/autodoc.rst | 17 ++++ sphinx/ext/autodoc/__init__.py | 43 ++++++++- sphinx/ext/autodoc/directive.py | 2 +- .../roots/test-ext-autodoc/target/__init__.py | 15 ++++ tests/test_autodoc.py | 87 +++++++++++++++++-- 5 files changed, 155 insertions(+), 9 deletions(-) diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index 385254734..285756149 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -353,8 +353,25 @@ 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, + } + + 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..aabf6b47d 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -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 a54c9c2e6..a559fb164 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,6 +35,8 @@ 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=None): if options is None: @@ -1414,21 +1418,94 @@ 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 + + +@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 From f196a92055df2ef7603ad777abfaa2989d4b6744 Mon Sep 17 00:00:00 2001 From: Lewis Haley Date: Fri, 17 Aug 2018 16:02:11 +0100 Subject: [PATCH 3/3] autodoc: add 'exclude-members' to user global options As the previous commit explains, it is now possible to specify arguments to the global options in config files. This means that we can now include the `exclude-members` option in this global configuration. Previously, there was no point including this option because it makes no sense without arguments. Including this option means users have the flexibility of explicitly including which special methods they want using (e.g.): :special-members: __init__, __iter__ or explicitly excluding which special-members (or other members) they want using (e.g.): :exclude-members: __weakref__, __hash__ --- doc/usage/extensions/autodoc.rst | 4 ++- sphinx/ext/autodoc/directive.py | 2 +- tests/test_autodoc.py | 52 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index 285756149..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. @@ -361,6 +362,7 @@ There are also new config values that you can set: '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 diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index aabf6b47d..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): diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index a559fb164..0cd543ad7 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1484,6 +1484,32 @@ def test_autodoc_default_flags__with_flags(app): 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): @@ -1509,3 +1535,29 @@ def test_autodoc_default_flags__with_values(app): 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