""" test_autodoc ~~~~~~~~~~~~ Test the autodoc extension. This tests mainly the Documenters; the auto directives are tested in a test source file translated by test_build. :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re import platform import sys from warnings import catch_warnings import pytest from docutils.statemachine import ViewList from sphinx.ext.autodoc import ( ModuleLevelDocumenter, cut_lines, between, ALL, merge_autodoc_default_flags, Options ) from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options from sphinx.testing.util import SphinxTestApp, Struct # NOQA from sphinx.util import logging from sphinx.util.docutils import LoggingReporter app = None IS_PYPY = platform.python_implementation() == 'PyPy' def do_autodoc(app, objtype, name, options=None): if options is None: options = {} app.env.temp_data.setdefault('docname', 'index') # set dummy docname doccls = app.registry.documenters[objtype] docoptions = process_documenter_options(doccls, app.config, options) bridge = DocumenterBridge(app.env, LoggingReporter(''), docoptions, 1) documenter = doccls(bridge, name) documenter.generate() return bridge.result @pytest.fixture(scope='module', autouse=True) def setup_module(rootdir, sphinx_test_tempdir): try: global app srcdir = sphinx_test_tempdir / 'autodoc-root' if not srcdir.exists(): (rootdir / 'test-root').copytree(srcdir) testroot = rootdir / 'test-ext-autodoc' sys.path.append(testroot) app = SphinxTestApp(srcdir=srcdir) app.builder.env.app = app app.builder.env.temp_data['docname'] = 'dummy' app.connect('autodoc-process-docstring', process_docstring) app.connect('autodoc-process-signature', process_signature) app.connect('autodoc-skip-member', skip_member) yield finally: app.cleanup() sys.path.remove(testroot) directive = options = None @pytest.fixture def setup_test(): global options, directive global processed_docstrings, processed_signatures options = Options( inherited_members = False, undoc_members = False, private_members = False, special_members = False, imported_members = False, show_inheritance = False, noindex = False, annotation = None, synopsis = '', platform = '', deprecated = False, members = [], member_order = 'alphabetic', exclude_members = set(), ignore_module_all = False, ) directive = Struct( env = app.builder.env, genopt = options, result = ViewList(), filename_set = set(), ) processed_docstrings = [] processed_signatures = [] app._status.truncate(0) app._warning.truncate(0) yield app.registry.autodoc_attrgettrs.clear() processed_docstrings = [] processed_signatures = [] def process_docstring(app, what, name, obj, options, lines): processed_docstrings.append((what, name)) if name == 'bar': lines.extend(['42', '']) def process_signature(app, what, name, obj, options, args, retann): processed_signatures.append((what, name)) if name == 'bar': return '42', None def skip_member(app, what, name, obj, skip, options): if name in ('__special1__', '__special2__'): return skip if name.startswith('__'): return True if name == 'skipmeth': return True @pytest.mark.usefixtures('setup_test') def test_parse_name(): logging.setup(app, app._status, app._warning) def verify(objtype, name, result): inst = app.registry.documenters[objtype](directive, name) assert inst.parse_name() assert (inst.modname, inst.objpath, inst.args, inst.retann) == result # for modules verify('module', 'test_autodoc', ('test_autodoc', [], None, None)) verify('module', 'test.test_autodoc', ('test.test_autodoc', [], None, None)) verify('module', 'test(arg)', ('test', [], 'arg', None)) assert 'signature arguments' in app._warning.getvalue() # for functions/classes verify('function', 'test_autodoc.raises', ('test_autodoc', ['raises'], None, None)) verify('function', 'test_autodoc.raises(exc) -> None', ('test_autodoc', ['raises'], 'exc', 'None')) directive.env.temp_data['autodoc:module'] = 'test_autodoc' verify('function', 'raises', ('test_autodoc', ['raises'], None, None)) del directive.env.temp_data['autodoc:module'] directive.env.ref_context['py:module'] = 'test_autodoc' verify('function', 'raises', ('test_autodoc', ['raises'], None, None)) verify('class', 'Base', ('test_autodoc', ['Base'], None, None)) # for members directive.env.ref_context['py:module'] = 'foo' verify('method', 'util.SphinxTestApp.cleanup', ('util', ['SphinxTestApp', 'cleanup'], None, None)) directive.env.ref_context['py:module'] = 'util' directive.env.ref_context['py:class'] = 'Foo' directive.env.temp_data['autodoc:class'] = 'SphinxTestApp' verify('method', 'cleanup', ('util', ['SphinxTestApp', 'cleanup'], None, None)) verify('method', 'SphinxTestApp.cleanup', ('util', ['SphinxTestApp', 'cleanup'], None, None)) # and clean up del directive.env.ref_context['py:module'] del directive.env.ref_context['py:class'] del directive.env.temp_data['autodoc:class'] @pytest.mark.usefixtures('setup_test') def test_format_signature(): def formatsig(objtype, name, obj, args, retann): inst = app.registry.documenters[objtype](directive, name) inst.fullname = name inst.doc_as_attr = False # for class objtype inst.object = obj inst.objpath = [name] inst.args = args inst.retann = retann res = inst.format_signature() print(res) return res # no signatures for modules assert formatsig('module', 'test', None, None, None) == '' # test for functions def f(a, b, c=1, **d): pass def g(a='\n'): pass assert formatsig('function', 'f', f, None, None) == '(a, b, c=1, **d)' assert formatsig('function', 'f', f, 'a, b, c, d', None) == '(a, b, c, d)' assert formatsig('function', 'f', f, None, 'None') == '(a, b, c=1, **d) -> None' assert formatsig('function', 'g', g, None, None) == r"(a='\\n')" # test for classes class D: pass class E: pass # no signature for classes without __init__ for C in (D, E): assert formatsig('class', 'D', C, None, None) == '' class F: def __init__(self, a, b=None): pass class G(F): pass for C in (F, G): assert formatsig('class', 'C', C, None, None) == '(a, b=None)' assert formatsig('class', 'C', D, 'a, b', 'X') == '(a, b) -> X' # __init__ have signature at first line of docstring directive.env.config.autoclass_content = 'both' class F2: '''some docstring for F2.''' def __init__(self, *args, **kw): ''' __init__(a1, a2, kw1=True, kw2=False) some docstring for __init__. ''' class G2(F2): pass assert formatsig('class', 'F2', F2, None, None) == \ '(a1, a2, kw1=True, kw2=False)' assert formatsig('class', 'G2', G2, None, None) == \ '(a1, a2, kw1=True, kw2=False)' # test for methods class H: def foo1(self, b, *c): pass def foo2(b, *c): pass def foo3(self, d='\n'): pass assert formatsig('method', 'H.foo', H.foo1, None, None) == '(b, *c)' assert formatsig('method', 'H.foo', H.foo1, 'a', None) == '(a)' assert formatsig('method', 'H.foo', H.foo2, None, None) == '(*c)' assert formatsig('method', 'H.foo', H.foo3, None, None) == r"(d='\\n')" # test exception handling (exception is caught and args is '') directive.env.config.autodoc_docstring_signature = False assert formatsig('function', 'int', int, None, None) == '' # test processing by event handler assert formatsig('method', 'bar', H.foo1, None, None) == '42' # test functions created via functools.partial from functools import partial curried1 = partial(lambda a, b, c: None, 'A') assert formatsig('function', 'curried1', curried1, None, None) == \ '(b, c)' curried2 = partial(lambda a, b, c=42: None, 'A') assert formatsig('function', 'curried2', curried2, None, None) == \ '(b, c=42)' curried3 = partial(lambda a, b, *c: None, 'A') assert formatsig('function', 'curried3', curried3, None, None) == \ '(b, *c)' curried4 = partial(lambda a, b, c=42, *d, **e: None, 'A') assert formatsig('function', 'curried4', curried4, None, None) == \ '(b, c=42, *d, **e)' @pytest.mark.usefixtures('setup_test') def test_get_doc(): def getdocl(objtype, obj): inst = app.registry.documenters[objtype](directive, 'tmp') inst.object = obj inst.objpath = [obj.__name__] inst.doc_as_attr = False inst.format_signature() # handle docstring signatures! ds = inst.get_doc() # for testing purposes, concat them and strip the empty line at the end res = sum(ds, [])[:-1] print(res) return res # objects without docstring def f(): pass assert getdocl('function', f) == [] # standard function, diverse docstring styles... def f(): """Docstring""" def g(): """ Docstring """ for func in (f, g): assert getdocl('function', func) == ['Docstring'] # first line vs. other lines indentation def f(): """First line Other lines """ assert getdocl('function', f) == ['First line', '', 'Other', ' lines'] # charset guessing (this module is encoded in utf-8) def f(): """Döcstring""" assert getdocl('function', f) == ['Döcstring'] # already-unicode docstrings must be taken literally def f(): """Döcstring""" assert getdocl('function', f) == ['Döcstring'] # class docstring: depends on config value which one is taken class C: """Class docstring""" def __init__(self): """Init docstring""" def __new__(cls): """New docstring""" directive.env.config.autoclass_content = 'class' assert getdocl('class', C) == ['Class docstring'] directive.env.config.autoclass_content = 'init' assert getdocl('class', C) == ['Init docstring'] directive.env.config.autoclass_content = 'both' assert getdocl('class', C) == ['Class docstring', '', 'Init docstring'] class D: """Class docstring""" def __init__(self): """Init docstring Other lines """ # Indentation is normalized for 'both' assert getdocl('class', D) == ['Class docstring', '', 'Init docstring', '', 'Other', ' lines'] # __init__ have signature at first line of docstring class E: """Class docstring""" def __init__(self, *args, **kw): """ __init__(a1, a2, kw1=True, kw2=False) Init docstring """ # signature line in the docstring will be kept when # autodoc_docstring_signature == False directive.env.config.autodoc_docstring_signature = False directive.env.config.autoclass_content = 'class' assert getdocl('class', E) == ['Class docstring'] directive.env.config.autoclass_content = 'init' assert getdocl('class', E) == ['__init__(a1, a2, kw1=True, kw2=False)', '', 'Init docstring'] directive.env.config.autoclass_content = 'both' assert getdocl('class', E) == ['Class docstring', '', '__init__(a1, a2, kw1=True, kw2=False)', '', 'Init docstring'] # signature line in the docstring will be removed when # autodoc_docstring_signature == True directive.env.config.autodoc_docstring_signature = True # default directive.env.config.autoclass_content = 'class' assert getdocl('class', E) == ['Class docstring'] directive.env.config.autoclass_content = 'init' assert getdocl('class', E) == ['Init docstring'] directive.env.config.autoclass_content = 'both' assert getdocl('class', E) == ['Class docstring', '', 'Init docstring'] # class does not have __init__ method class F: """Class docstring""" # docstring in the __init__ method of base class will be discard for f in (False, True): directive.env.config.autodoc_docstring_signature = f directive.env.config.autoclass_content = 'class' assert getdocl('class', F) == ['Class docstring'] directive.env.config.autoclass_content = 'init' assert getdocl('class', F) == ['Class docstring'] directive.env.config.autoclass_content = 'both' assert getdocl('class', F) == ['Class docstring'] # class has __init__ method with no docstring class G: """Class docstring""" def __init__(self): pass # docstring in the __init__ method of base class will not be used for f in (False, True): directive.env.config.autodoc_docstring_signature = f directive.env.config.autoclass_content = 'class' assert getdocl('class', G) == ['Class docstring'] directive.env.config.autoclass_content = 'init' assert getdocl('class', G) == ['Class docstring'] directive.env.config.autoclass_content = 'both' assert getdocl('class', G) == ['Class docstring'] # class has __new__ method with docstring # class docstring: depends on config value which one is taken class H: """Class docstring""" def __init__(self): pass def __new__(cls): """New docstring""" directive.env.config.autoclass_content = 'class' assert getdocl('class', H) == ['Class docstring'] directive.env.config.autoclass_content = 'init' assert getdocl('class', H) == ['New docstring'] directive.env.config.autoclass_content = 'both' assert getdocl('class', H) == ['Class docstring', '', 'New docstring'] # class has __init__ method without docstring and # __new__ method with docstring # class docstring: depends on config value which one is taken class I: # NOQA """Class docstring""" def __new__(cls): """New docstring""" directive.env.config.autoclass_content = 'class' assert getdocl('class', I) == ['Class docstring'] directive.env.config.autoclass_content = 'init' assert getdocl('class', I) == ['New docstring'] directive.env.config.autoclass_content = 'both' assert getdocl('class', I) == ['Class docstring', '', 'New docstring'] from target import Base, Derived # NOTE: inspect.getdoc seems not to work with locally defined classes directive.env.config.autodoc_inherit_docstrings = False assert getdocl('method', Base.inheritedmeth) == ['Inherited function.'] assert getdocl('method', Derived.inheritedmeth) == [] directive.env.config.autodoc_inherit_docstrings = True assert getdocl('method', Derived.inheritedmeth) == ['Inherited function.'] @pytest.mark.usefixtures('setup_test') def test_docstring_processing(): def process(objtype, name, obj): inst = app.registry.documenters[objtype](directive, name) inst.object = obj inst.fullname = name return list(inst.process_doc(inst.get_doc())) class E: def __init__(self): """Init docstring""" # docstring processing by event handler assert process('class', 'bar', E) == ['Init docstring', '', '42', ''] lid = app.connect('autodoc-process-docstring', cut_lines(1, 1, ['function'])) def f(): """ first line second line third line """ assert process('function', 'f', f) == ['second line', ''] app.disconnect(lid) lid = app.connect('autodoc-process-docstring', between('---', ['function'])) def g(): """ first line --- second line --- third line """ assert process('function', 'g', g) == ['second line', ''] app.disconnect(lid) lid = app.connect('autodoc-process-docstring', between('---', ['function'], exclude=True)) def h(): """ first line --- second line --- third line """ assert process('function', 'h', h) == ['first line', 'third line', ''] app.disconnect(lid) @pytest.mark.usefixtures('setup_test') def test_new_documenter(): logging.setup(app, app._status, app._warning) class MyDocumenter(ModuleLevelDocumenter): objtype = 'integer' directivetype = 'data' priority = 100 @classmethod def can_document_member(cls, member, membername, isattr, parent): return isinstance(member, int) def document_members(self, all_members=False): return app.add_autodocumenter(MyDocumenter) def assert_result_contains(item, objtype, name, **kw): app._warning.truncate(0) inst = app.registry.documenters[objtype](directive, name) inst.generate(**kw) # print '\n'.join(directive.result) assert app._warning.getvalue() == '' assert item in directive.result del directive.result[:] options.members = ['integer'] assert_result_contains('.. py:data:: integer', 'module', 'target') @pytest.mark.usefixtures('setup_test') def test_attrgetter_using(): from target import Class def assert_getter_works(objtype, name, obj, attrs=[], **kw): getattr_spy = [] def special_getattr(obj, name, *defargs): if name in attrs: getattr_spy.append((obj, name)) return None return getattr(obj, name, *defargs) app.add_autodoc_attrgetter(type, special_getattr) del getattr_spy[:] inst = app.registry.documenters[objtype](directive, name) inst.generate(**kw) hooked_members = [s[1] for s in getattr_spy] documented_members = [s[1] for s in processed_signatures] for attr in attrs: fullname = '.'.join((name, attr)) assert attr in hooked_members assert fullname not in documented_members, \ '%r was not hooked by special_attrgetter function' % fullname with catch_warnings(record=True): options.members = ALL options.inherited_members = False assert_getter_works('class', 'target.Class', Class, ['meth']) options.inherited_members = True assert_getter_works('class', 'target.Class', Class, ['meth', 'inheritedmeth']) @pytest.mark.usefixtures('setup_test') def test_generate(): def assert_result_contains(item, objtype, name, **kw): inst = app.registry.documenters[objtype](directive, name) inst.generate(**kw) assert item in directive.result del directive.result[:] # test auto and given content mixing directive.env.ref_context['py:module'] = 'target' assert_result_contains(' Function.', 'method', 'Class.meth') add_content = ViewList() add_content.append('Content.', '', 0) assert_result_contains(' Function.', 'method', 'Class.meth', more_content=add_content) assert_result_contains(' Content.', 'method', 'Class.meth', more_content=add_content) @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_exception(app): actual = do_autodoc(app, 'exception', 'target.CustomEx') assert list(actual) == [ '', '.. py:exception:: CustomEx', ' :module: target', '', ' My custom exception.', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_warnings(app, warning): app.env.temp_data['docname'] = 'dummy' # can't import module do_autodoc(app, 'module', 'unknown') assert "failed to import module 'unknown'" in warning.getvalue() # missing function do_autodoc(app, 'function', 'unknown') assert "import for autodocumenting 'unknown'" in warning.getvalue() do_autodoc(app, 'function', 'target.unknown') assert "failed to import function 'unknown' from module 'target'" in warning.getvalue() # missing method do_autodoc(app, 'method', 'target.Class.unknown') assert "failed to import method 'Class.unknown' from module 'target'" in warning.getvalue() @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_attributes(app): options = {"synopsis": 'Synopsis', "platform": "Platform", "deprecated": None} actual = do_autodoc(app, 'module', 'target', options) assert list(actual) == [ '', '.. py:module:: target', ' :synopsis: Synopsis', ' :platform: Platform', ' :deprecated:', '' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_members(app): # default (no-members) actual = do_autodoc(app, 'class', 'target.Base') assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Base', ] # default ALL-members options = {"members": None} actual = do_autodoc(app, 'class', 'target.Base', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Base', ' .. py:classmethod:: Base.inheritedclassmeth()', ' .. py:method:: Base.inheritedmeth()', ' .. py:staticmethod:: Base.inheritedstaticmeth(cls)' ] # default specific-members options = {"members": "inheritedmeth,inheritedstaticmeth"} actual = do_autodoc(app, 'class', 'target.Base', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Base', ' .. py:method:: Base.inheritedmeth()', ' .. py:staticmethod:: Base.inheritedstaticmeth(cls)' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_exclude_members(app): options = {"members": None, "exclude-members": "inheritedmeth,inheritedstaticmeth"} actual = do_autodoc(app, 'class', 'target.Base', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Base', ' .. py:classmethod:: Base.inheritedclassmeth()' ] # members vs exclude-members options = {"members": "inheritedmeth", "exclude-members": "inheritedmeth"} actual = do_autodoc(app, 'class', 'target.Base', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Base', ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_undoc_members(app): options = {"members": None, "undoc-members": None} actual = do_autodoc(app, 'class', 'target.Class', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Class(arg)', ' .. py:attribute:: Class.attr', ' .. py:attribute:: Class.descr', ' .. py:attribute:: Class.docattr', ' .. py:method:: Class.excludemeth()', ' .. py:attribute:: Class.inst_attr_comment', ' .. py:attribute:: Class.inst_attr_inline', ' .. py:attribute:: Class.inst_attr_string', ' .. py:attribute:: Class.mdocattr', ' .. py:method:: Class.meth()', ' .. py:classmethod:: Class.moore(a, e, f) -> happiness', ' .. py:attribute:: Class.prop', ' .. py:classmethod:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', ' .. py:attribute:: Class.skipattr', ' .. py:method:: Class.skipmeth()', ' .. py:attribute:: Class.udocattr', ' .. py:method:: Class.undocmeth()' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_inherited_members(app): options = {"members": None, "inherited-members": None} actual = do_autodoc(app, 'class', 'target.Class', options) assert list(filter(lambda l: 'method::' in l, actual)) == [ ' .. py:method:: Class.excludemeth()', ' .. py:classmethod:: Class.inheritedclassmeth()', ' .. py:method:: Class.inheritedmeth()', ' .. py:staticmethod:: Class.inheritedstaticmeth(cls)', ' .. py:method:: Class.meth()', ' .. py:classmethod:: Class.moore(a, e, f) -> happiness', ' .. py:method:: Class.skipmeth()' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_imported_members(app): options = {"members": None, "imported-members": None, "ignore-module-all": None} actual = do_autodoc(app, 'module', 'target', options) assert '.. py:function:: save_traceback(app)' in actual @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_special_members(app): # specific special methods options = {"undoc-members": None, "special-members": "__init__,__special1__"} actual = do_autodoc(app, 'class', 'target.Class', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Class(arg)', ' .. py:method:: Class.__init__(arg)', ' .. py:method:: Class.__special1__()', ] # combination with specific members options = {"members": "attr,docattr", "undoc-members": None, "special-members": "__init__,__special1__"} actual = do_autodoc(app, 'class', 'target.Class', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Class(arg)', ' .. py:method:: Class.__init__(arg)', ' .. py:method:: Class.__special1__()', ' .. py:attribute:: Class.attr', ' .. py:attribute:: Class.docattr', ] # all special methods options = {"members": None, "undoc-members": None, "special-members": None} actual = do_autodoc(app, 'class', 'target.Class', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Class(arg)', ' .. py:method:: Class.__init__(arg)', ' .. py:attribute:: Class.__module__', ' .. py:method:: Class.__special1__()', ' .. py:method:: Class.__special2__()', ' .. py:attribute:: Class.attr', ' .. py:attribute:: Class.descr', ' .. py:attribute:: Class.docattr', ' .. py:method:: Class.excludemeth()', ' .. py:attribute:: Class.inst_attr_comment', ' .. py:attribute:: Class.inst_attr_inline', ' .. py:attribute:: Class.inst_attr_string', ' .. py:attribute:: Class.mdocattr', ' .. py:method:: Class.meth()', ' .. py:classmethod:: Class.moore(a, e, f) -> happiness', ' .. py:attribute:: Class.prop', ' .. py:classmethod:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', ' .. py:attribute:: Class.skipattr', ' .. py:method:: Class.skipmeth()', ' .. py:attribute:: Class.udocattr', ' .. py:method:: Class.undocmeth()' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_ignore_module_all(app): # default (no-ignore-module-all) options = {"members": None} actual = do_autodoc(app, 'module', 'target', options) assert list(filter(lambda l: 'class::' in l, actual)) == [ '.. py:class:: Class(arg)', ] # ignore-module-all options = {"members": None, "ignore-module-all": None} actual = do_autodoc(app, 'module', 'target', options) assert list(filter(lambda l: 'class::' in l, actual)) == [ '.. py:class:: Class(arg)', '.. py:class:: CustomDataDescriptor(doc)', '.. py:class:: CustomDataDescriptor2(doc)', '.. py:class:: CustomDataDescriptorMeta', '.. py:class:: CustomDict', '.. py:class:: InstAttCls()', '.. py:class:: Outer', ' .. py:class:: Outer.Inner', '.. py:class:: StrRepr' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_noindex(app): options = {"noindex": True} actual = do_autodoc(app, 'module', 'target', options) assert list(actual) == [ '', '.. py:module:: target', ' :noindex:', '' ] # TODO: :noindex: should be propagated to children of target item. actual = do_autodoc(app, 'class', 'target.Base', options) assert list(actual) == [ '', '.. py:class:: Base', ' :noindex:', ' :module: target', '' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_subclass_of_builtin_class(app): options = {"members": None} actual = do_autodoc(app, 'class', 'target.CustomDict', options) assert list(actual) == [ '', '.. py:class:: CustomDict', ' :module: target', '', ' Docstring.', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_inner_class(app): options = {"members": None} actual = do_autodoc(app, 'class', 'target.Outer', options) assert list(actual) == [ '', '.. py:class:: Outer', ' :module: target', '', ' Foo', ' ', ' ', ' .. py:class:: Outer.Inner', ' :module: target', ' ', ' Foo', ' ', ' ', ' .. py:method:: Outer.Inner.meth()', ' :module: target', ' ', ' Foo', ' ', ' ', ' .. py:attribute:: Outer.factory', ' :module: target', ' ', ' alias of :class:`builtins.dict`' ] actual = do_autodoc(app, 'class', 'target.Outer.Inner', options) assert list(actual) == [ '', '.. py:class:: Inner', ' :module: target.Outer', '', ' Foo', ' ', ' ', ' .. py:method:: Inner.meth()', ' :module: target.Outer', ' ', ' Foo', ' ', ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_descriptor(app): actual = do_autodoc(app, 'attribute', 'target.Class.descr') assert list(actual) == [ '', '.. py:attribute:: Class.descr', ' :module: target', '', ' Descriptor instance docstring.', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_c_module(app): actual = do_autodoc(app, 'function', 'time.asctime') assert list(actual) == [ '', '.. py:function:: asctime([tuple]) -> string', ' :module: time', '', " Convert a time tuple to a string, e.g. 'Sat Jun 06 16:26:11 1998'.", ' When the time tuple is not present, current time as returned by localtime()', ' is used.', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_member_order(app): # case member-order='bysource' options = {"members": None, 'member-order': 'bysource', "undoc-members": True, 'private-members': True} actual = do_autodoc(app, 'class', 'target.Class', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Class(arg)', ' .. py:attribute:: Class.descr', ' .. py:method:: Class.meth()', ' .. py:method:: Class.undocmeth()', ' .. py:method:: Class.skipmeth()', ' .. py:method:: Class.excludemeth()', ' .. py:attribute:: Class.skipattr', ' .. py:attribute:: Class.attr', ' .. py:attribute:: Class.prop', ' .. py:attribute:: Class.docattr', ' .. py:attribute:: Class.udocattr', ' .. py:attribute:: Class.mdocattr', ' .. py:classmethod:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', ' .. py:classmethod:: Class.moore(a, e, f) -> happiness', ' .. py:attribute:: Class.inst_attr_inline', ' .. py:attribute:: Class.inst_attr_comment', ' .. py:attribute:: Class.inst_attr_string', ' .. py:attribute:: Class._private_inst_attr' ] # case member-order='groupwise' options = {"members": None, 'member-order': 'groupwise', "undoc-members": True, 'private-members': True} actual = do_autodoc(app, 'class', 'target.Class', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Class(arg)', ' .. py:method:: Class.excludemeth()', ' .. py:method:: Class.meth()', ' .. py:classmethod:: Class.moore(a, e, f) -> happiness', ' .. py:classmethod:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', ' .. py:method:: Class.skipmeth()', ' .. py:method:: Class.undocmeth()', ' .. py:attribute:: Class._private_inst_attr', ' .. py:attribute:: Class.attr', ' .. py:attribute:: Class.descr', ' .. py:attribute:: Class.docattr', ' .. py:attribute:: Class.inst_attr_comment', ' .. py:attribute:: Class.inst_attr_inline', ' .. py:attribute:: Class.inst_attr_string', ' .. py:attribute:: Class.mdocattr', ' .. py:attribute:: Class.prop', ' .. py:attribute:: Class.skipattr', ' .. py:attribute:: Class.udocattr' ] # case member-order=None options = {"members": None, "undoc-members": True, 'private-members': True} actual = do_autodoc(app, 'class', 'target.Class', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Class(arg)', ' .. py:attribute:: Class._private_inst_attr', ' .. py:attribute:: Class.attr', ' .. py:attribute:: Class.descr', ' .. py:attribute:: Class.docattr', ' .. py:method:: Class.excludemeth()', ' .. py:attribute:: Class.inst_attr_comment', ' .. py:attribute:: Class.inst_attr_inline', ' .. py:attribute:: Class.inst_attr_string', ' .. py:attribute:: Class.mdocattr', ' .. py:method:: Class.meth()', ' .. py:classmethod:: Class.moore(a, e, f) -> happiness', ' .. py:attribute:: Class.prop', ' .. py:classmethod:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', ' .. py:attribute:: Class.skipattr', ' .. py:method:: Class.skipmeth()', ' .. py:attribute:: Class.udocattr', ' .. py:method:: Class.undocmeth()' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_module_scope(app): app.env.temp_data['autodoc:module'] = 'target' actual = do_autodoc(app, 'attribute', 'Class.mdocattr') assert list(actual) == [ '', '.. py:attribute:: Class.mdocattr', ' :module: target', ' :annotation: = <_io.StringIO object>', '', ' should be documented as well - süß', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_class_scope(app): app.env.temp_data['autodoc:module'] = 'target' app.env.temp_data['autodoc:class'] = 'Class' actual = do_autodoc(app, 'attribute', 'mdocattr') assert list(actual) == [ '', '.. py:attribute:: Class.mdocattr', ' :module: target', ' :annotation: = <_io.StringIO object>', '', ' should be documented as well - süß', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_docstring_signature(app): options = {"members": None} actual = do_autodoc(app, 'class', 'target.DocstringSig', options) assert list(actual) == [ '', '.. py:class:: DocstringSig', ' :module: target', '', ' ', ' .. py:method:: DocstringSig.meth(FOO, BAR=1) -> BAZ', ' :module: target', ' ', ' First line of docstring', ' ', ' rest of docstring', ' ', ' ', ' .. py:method:: DocstringSig.meth2()', ' :module: target', ' ', ' First line, no signature', ' Second line followed by indentation::', ' ', ' indented line', ' ', ' ', ' .. py:attribute:: DocstringSig.prop1', ' :module: target', ' ', ' First line of docstring', ' ', ' ', ' .. py:attribute:: DocstringSig.prop2', ' :module: target', ' ', ' First line of docstring', ' Second line of docstring', ' ' ] # disable autodoc_docstring_signature app.config.autodoc_docstring_signature = False actual = do_autodoc(app, 'class', 'target.DocstringSig', options) assert list(actual) == [ '', '.. py:class:: DocstringSig', ' :module: target', '', ' ', ' .. py:method:: DocstringSig.meth()', ' :module: target', ' ', ' meth(FOO, BAR=1) -> BAZ', ' First line of docstring', ' ', ' rest of docstring', ' ', ' ', ' ', ' .. py:method:: DocstringSig.meth2()', ' :module: target', ' ', ' First line, no signature', ' Second line followed by indentation::', ' ', ' indented line', ' ', ' ', ' .. py:attribute:: DocstringSig.prop1', ' :module: target', ' ', ' DocstringSig.prop1(self)', ' First line of docstring', ' ', ' ', ' .. py:attribute:: DocstringSig.prop2', ' :module: target', ' ', ' First line of docstring', ' Second line of docstring', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_class_attributes(app): options = {"members": None, "undoc-members": True} actual = do_autodoc(app, 'class', 'target.AttCls', options) assert list(actual) == [ '', '.. py:class:: AttCls', ' :module: target', '', ' ', ' .. py:attribute:: AttCls.a1', ' :module: target', ' :annotation: = hello world', ' ', ' ', ' .. py:attribute:: AttCls.a2', ' :module: target', ' :annotation: = None', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_instance_attributes(app): options = {"members": None} actual = do_autodoc(app, 'class', 'target.InstAttCls', options) assert list(actual) == [ '', '.. py:class:: InstAttCls()', ' :module: target', '', ' Class with documented class and instance attributes.', ' ', ' ', ' .. py:attribute:: InstAttCls.ca1', ' :module: target', " :annotation: = 'a'", ' ', ' Doc comment for class attribute InstAttCls.ca1.', ' It can have multiple lines.', ' ', ' ', ' .. py:attribute:: InstAttCls.ca2', ' :module: target', " :annotation: = 'b'", ' ', ' Doc comment for InstAttCls.ca2. One line only.', ' ', ' ', ' .. py:attribute:: InstAttCls.ca3', ' :module: target', " :annotation: = 'c'", ' ', ' Docstring for class attribute InstAttCls.ca3.', ' ', ' ', ' .. py:attribute:: InstAttCls.ia1', ' :module: target', ' :annotation: = None', ' ', ' Doc comment for instance attribute InstAttCls.ia1', ' ', ' ', ' .. py:attribute:: InstAttCls.ia2', ' :module: target', ' :annotation: = None', ' ', ' Docstring for instance attribute InstAttCls.ia2.', ' ' ] # pick up arbitrary attributes options = {"members": 'ca1,ia1'} actual = do_autodoc(app, 'class', 'target.InstAttCls', options) assert list(actual) == [ '', '.. py:class:: InstAttCls()', ' :module: target', '', ' Class with documented class and instance attributes.', ' ', ' ', ' .. py:attribute:: InstAttCls.ca1', ' :module: target', " :annotation: = 'a'", ' ', ' Doc comment for class attribute InstAttCls.ca1.', ' It can have multiple lines.', ' ', ' ', ' .. py:attribute:: InstAttCls.ia1', ' :module: target', ' :annotation: = None', ' ', ' Doc comment for instance attribute InstAttCls.ia1', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_enum_class(app): options = {"members": None, "undoc-members": True} actual = do_autodoc(app, 'class', 'target.enum.EnumCls', options) assert list(actual) == [ '', '.. py:class:: EnumCls', ' :module: target.enum', '', ' this is enum class', ' ', ' ', ' .. py:method:: EnumCls.say_hello()', ' :module: target.enum', ' ', ' a method says hello to you.', ' ', ' ', ' .. py:attribute:: EnumCls.val1', ' :module: target.enum', ' :annotation: = 12', ' ', ' doc for val1', ' ', ' ', ' .. py:attribute:: EnumCls.val2', ' :module: target.enum', ' :annotation: = 23', ' ', ' doc for val2', ' ', ' ', ' .. py:attribute:: EnumCls.val3', ' :module: target.enum', ' :annotation: = 34', ' ', ' doc for val3', ' ', ' ', ' .. py:attribute:: EnumCls.val4', ' :module: target.enum', ' :annotation: = 34', ' ' ] # checks for an attribute of EnumClass actual = do_autodoc(app, 'attribute', 'target.enum.EnumCls.val1') assert list(actual) == [ '', '.. py:attribute:: EnumCls.val1', ' :module: target.enum', ' :annotation: = 12', '', ' doc for val1', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_descriptor_class(app): options = {"members": 'CustomDataDescriptor,CustomDataDescriptor2'} actual = do_autodoc(app, 'module', 'target', options) assert list(actual) == [ '', '.. py:module:: target', '', '', '.. py:class:: CustomDataDescriptor(doc)', ' :module: target', '', ' Descriptor class docstring.', ' ', ' ', ' .. py:method:: CustomDataDescriptor.meth()', ' :module: target', ' ', ' Function.', ' ', '', '.. py:class:: CustomDataDescriptor2(doc)', ' :module: target', '', ' Descriptor class with custom metaclass docstring.', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autofunction_for_callable(app): actual = do_autodoc(app, 'function', 'target.callable.function') assert list(actual) == [ '', '.. py:function:: function(arg1, arg2, **kwargs)', ' :module: target.callable', '', ' A callable object that behaves like a function.', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_mocked_module_imports(app, warning): # no autodoc_mock_imports options = {"members": 'TestAutodoc,decoratedFunction'} actual = do_autodoc(app, 'module', 'target.need_mocks', options) assert list(actual) == [] assert "autodoc: failed to import module 'need_mocks'" in warning.getvalue() # with autodoc_mock_imports app.config.autodoc_mock_imports = [ 'missing_module', 'missing_package1', 'missing_package2', 'missing_package3', 'sphinx.missing_module4', ] warning.truncate(0) actual = do_autodoc(app, 'module', 'target.need_mocks', options) assert list(actual) == [ '', '.. py:module:: target.need_mocks', '', '', '.. py:class:: TestAutodoc', ' :module: target.need_mocks', '', ' TestAutodoc docstring.', ' ', ' ', ' .. py:method:: TestAutodoc.decoratedMethod()', ' :module: target.need_mocks', ' ', ' TestAutodoc::decoratedMethod docstring', ' ', '', '.. py:function:: decoratedFunction()', ' :module: target.need_mocks', '', ' decoratedFunction docstring', ' ' ] assert warning.getvalue() == '' @pytest.mark.usefixtures('setup_test') def test_partialfunction(): options = {"members": None} actual = do_autodoc(app, 'module', 'target.partialfunction', options) assert list(actual) == [ '', '.. py:module:: target.partialfunction', '', '', '.. py:function:: func1()', ' :module: target.partialfunction', '', ' docstring of func1', ' ', '', '.. py:function:: func2()', ' :module: target.partialfunction', '', ' docstring of func1', ' ', '', '.. py:function:: func3()', ' :module: target.partialfunction', '', ' docstring of func3', ' ' ] @pytest.mark.usefixtures('setup_test') def test_coroutine(): options = {"members": None} actual = do_autodoc(app, 'class', 'target.coroutine.AsyncClass', options) assert list(actual) == [ '', '.. py:class:: AsyncClass', ' :module: target.coroutine', '', ' ', ' .. py:method:: AsyncClass.do_coroutine()', ' :module: target.coroutine', ' ', ' A documented coroutine function', ' ' ] @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_partialmethod(app): expected = [ '', '.. py:class:: Cell', ' :module: target.partialmethod', '', ' An example for partialmethod.', ' ', ' refs: https://docs.python.jp/3/library/functools.html#functools.partialmethod', ' ', ' ', ' .. py:method:: Cell.set_alive() -> None', ' :module: target.partialmethod', ' ', ' Make a cell alive.', ' ', ' ', ' .. py:method:: Cell.set_dead() -> None', ' :module: target.partialmethod', ' ', ' Make a cell dead.', ' ', ' ', ' .. py:method:: Cell.set_state(state)', ' :module: target.partialmethod', ' ', ' Update state of cell to *state*.', ' ', ] if (sys.version_info < (3, 5, 4) or (3, 6, 5) <= sys.version_info < (3, 7) or (3, 7, 0, 'beta', 3) <= sys.version_info): # TODO: this condition should be updated after 3.7-final release. expected = '\n'.join(expected).replace(' -> None', '').split('\n') options = {"members": None} actual = do_autodoc(app, 'class', 'target.partialmethod.Cell', options) assert list(actual) == expected @pytest.mark.sphinx('html', testroot='ext-autodoc') @pytest.mark.filterwarnings('ignore:autodoc_default_flags is now deprecated.') def test_merge_autodoc_default_flags1(app): app.config.autodoc_default_flags = ['members', 'undoc-members'] merge_autodoc_default_flags(app, app.config) assert app.config.autodoc_default_options == {'members': None, 'undoc-members': None} @pytest.mark.sphinx('html', testroot='ext-autodoc') @pytest.mark.filterwarnings('ignore:autodoc_default_flags is now deprecated.') def test_merge_autodoc_default_flags2(app): app.config.autodoc_default_flags = ['members', 'undoc-members'] app.config.autodoc_default_options = {'members': 'this,that,order', 'inherited-members': 'this'} merge_autodoc_default_flags(app, app.config) assert app.config.autodoc_default_options == {'members': None, 'undoc-members': None, 'inherited-members': 'this'} @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_default_options(app): # no settings actual = do_autodoc(app, 'class', 'target.enum.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_options = {'members': None} actual = do_autodoc(app, 'class', 'target.enum.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_options = { 'members': None, 'undoc-members': None, } actual = do_autodoc(app, 'class', 'target.enum.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_options = { '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_options = { 'members': None, 'exclude-members': None, } actual = do_autodoc(app, 'class', 'target.enum.EnumCls') assert ' .. py:attribute:: EnumCls.val1' in actual assert ' .. py:attribute:: EnumCls.val4' not in actual app.config.autodoc_default_options = { '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_options_with_values(app): # with :members: app.config.autodoc_default_options = {'members': 'val1,val2'} actual = do_autodoc(app, 'class', 'target.enum.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 :member-order: app.config.autodoc_default_options = { 'members': None, 'member-order': 'bysource', } actual = do_autodoc(app, 'class', 'target.Class') assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Class(arg)', ' .. py:attribute:: Class.descr', ' .. py:method:: Class.meth()', ' .. py:method:: Class.skipmeth()', ' .. py:method:: Class.excludemeth()', ' .. py:attribute:: Class.attr', ' .. py:attribute:: Class.prop', ' .. py:attribute:: Class.docattr', ' .. py:attribute:: Class.udocattr', ' .. py:attribute:: Class.mdocattr', ' .. py:classmethod:: Class.moore(a, e, f) -> happiness', ' .. py:attribute:: Class.inst_attr_inline', ' .. py:attribute:: Class.inst_attr_comment', ' .. py:attribute:: Class.inst_attr_string', ] # with :special-members: app.config.autodoc_default_options = { '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_options = { 'members': None, 'exclude-members': 'val1' } actual = do_autodoc(app, 'class', 'target.enum.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_options = { '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 @pytest.mark.sphinx('html', testroot='pycode-egg') def test_autodoc_for_egged_code(app): options = {"members": None, "undoc-members": None} actual = do_autodoc(app, 'module', 'sample', options) assert list(actual) == [ '', '.. py:module:: sample', '', '', '.. py:data:: CONSTANT', ' :module: sample', ' :annotation: = 1', '', ' constant on sample.py', ' ', '', '.. py:function:: hello(s)', ' :module: sample', '' ]