diff --git a/.travis.yml b/.travis.yml index 3f8b7ee3a..b415507ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "2.7" - "3.3" - "3.4" + - "3.5" - "pypy" env: - DOCUTILS=0.11 diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 65159927a..f8e732a63 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -636,10 +636,6 @@ def test_generate(): ('attribute', 'test_autodoc.Class.inst_attr_string'), ('method', 'test_autodoc.Class.moore'), ]) - if six.PY3 and sys.version_info[:2] >= (3, 5): - should.extend([ - ('method', 'test_autodoc.Class.do_coroutine'), - ]) options.members = ALL assert_processes(should, 'class', 'Class') options.undoc_members = True @@ -838,11 +834,6 @@ class Base(object): def inheritedmeth(self): """Inherited function.""" -if six.PY3 and sys.version_info[:2] >= (3, 5): - - async def _other_coro_func(): - return "run" - class Class(Base): """Class to document.""" @@ -900,13 +891,6 @@ class Class(Base): # undocumented special method pass - if six.PY3 and sys.version_info[:2] >= (3, 5): - - async def do_coroutine(self): - """A documented coroutine function""" - - attr_coro_result = await _other_coro_func() - class CustomDict(dict): """Docstring.""" diff --git a/tests/test_autodoc_py35.py b/tests/test_autodoc_py35.py new file mode 100644 index 000000000..dc1bbee08 --- /dev/null +++ b/tests/test_autodoc_py35.py @@ -0,0 +1,346 @@ +# -*- coding: utf-8 -*- +""" + 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-2015 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +# "raises" imported for usage by autodoc +from util import TestApp, Struct, raises, SkipTest +from nose.tools import with_setup, eq_ + +from six import StringIO +from docutils.statemachine import ViewList + +from sphinx.ext.autodoc import AutoDirective, add_documenter, \ + ModuleLevelDocumenter, FunctionDocumenter, cut_lines, between, ALL + +app = None + +def setup_module(): + global app + app = TestApp() + 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) + + +def teardown_module(): + app.cleanup() + + +directive = options = None + +def setup_test(): + global options, directive + global processed_docstrings, processed_signatures, _warnings + + options = Struct( + 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(), + ) + + directive = Struct( + env = app.builder.env, + genopt = options, + result = ViewList(), + warn = warnfunc, + filename_set = set(), + ) + + processed_docstrings = [] + processed_signatures = [] + _warnings = [] + + +_warnings = [] + +def warnfunc(msg): + _warnings.append(msg) + + +processed_docstrings = [] + +def process_docstring(app, what, name, obj, options, lines): + processed_docstrings.append((what, name)) + if name == 'bar': + lines.extend(['42', '']) + +processed_signatures = [] + +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 + + +@with_setup(setup_test) +def test_generate(): + def assert_warns(warn_str, objtype, name, **kw): + inst = AutoDirective._registry[objtype](directive, name) + inst.generate(**kw) + assert len(directive.result) == 0, directive.result + assert len(_warnings) == 1, _warnings + assert warn_str in _warnings[0], _warnings + del _warnings[:] + + def assert_works(objtype, name, **kw): + inst = AutoDirective._registry[objtype](directive, name) + inst.generate(**kw) + assert directive.result + #print '\n'.join(directive.result) + assert len(_warnings) == 0, _warnings + del directive.result[:] + + def assert_processes(items, objtype, name, **kw): + del processed_docstrings[:] + del processed_signatures[:] + assert_works(objtype, name, **kw) + assert set(processed_docstrings) | set(processed_signatures) == \ + set(items) + + def assert_result_contains(item, objtype, name, **kw): + inst = AutoDirective._registry[objtype](directive, name) + inst.generate(**kw) + #print '\n'.join(directive.result) + assert len(_warnings) == 0, _warnings + assert item in directive.result + del directive.result[:] + + def assert_order(items, objtype, name, member_order, **kw): + inst = AutoDirective._registry[objtype](directive, name) + inst.options.member_order = member_order + inst.generate(**kw) + assert len(_warnings) == 0, _warnings + items = list(reversed(items)) + lineiter = iter(directive.result) + #for line in directive.result: + # if line.strip(): + # print repr(line) + while items: + item = items.pop() + for line in lineiter: + if line == item: + break + else: # ran out of items! + assert False, 'item %r not found in result or not in the ' \ + ' correct order' % item + del directive.result[:] + + options.members = [] + + # no module found? + assert_warns("import for autodocumenting 'foobar'", + 'function', 'foobar', more_content=None) + # importing + assert_warns("failed to import module 'test_foobar'", + 'module', 'test_foobar', more_content=None) + # attributes missing + assert_warns("failed to import function 'foobar' from module 'util'", + 'function', 'util.foobar', more_content=None) + # method missing + assert_warns("failed to import method 'Class.foobar' from module 'test_autodoc_py35';", + 'method', 'test_autodoc_py35.Class.foobar', more_content=None) + + # test auto and given content mixing + directive.env.ref_context['py:module'] = 'test_autodoc_py35' + 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) + + # test check_module + inst = FunctionDocumenter(directive, 'raises') + inst.generate(check_module=True) + assert len(directive.result) == 0 + + # assert that exceptions can be documented + assert_works('exception', 'test_autodoc_py35.CustomEx', all_members=True) + assert_works('exception', 'test_autodoc_py35.CustomEx') + + # test diverse inclusion settings for members + should = [('class', 'test_autodoc_py35.Class')] + assert_processes(should, 'class', 'Class') + should.extend([('method', 'test_autodoc_py35.Class.meth')]) + options.members = ['meth'] + options.exclude_members = set(['excludemeth']) + assert_processes(should, 'class', 'Class') + should.extend([('attribute', 'test_autodoc_py35.Class.prop'), + ('attribute', 'test_autodoc_py35.Class.descr'), + ('attribute', 'test_autodoc_py35.Class.attr'), + ('attribute', 'test_autodoc_py35.Class.docattr'), + ('attribute', 'test_autodoc_py35.Class.udocattr'), + ('attribute', 'test_autodoc_py35.Class.mdocattr'), + ('attribute', 'test_autodoc_py35.Class.inst_attr_comment'), + ('attribute', 'test_autodoc_py35.Class.inst_attr_inline'), + ('attribute', 'test_autodoc_py35.Class.inst_attr_string'), + ('method', 'test_autodoc_py35.Class.moore'), + ]) + if six.PY3 and sys.version_info[:2] >= (3, 5): + should.extend([ + ('method', 'test_autodoc_py35.Class.do_coroutine'), + ]) + options.members = ALL + assert_processes(should, 'class', 'Class') + options.undoc_members = True + should.extend((('attribute', 'test_autodoc_py35.Class.skipattr'), + ('method', 'test_autodoc_py35.Class.undocmeth'), + ('method', 'test_autodoc_py35.Class.roger'))) + assert_processes(should, 'class', 'Class') + options.inherited_members = True + should.append(('method', 'test_autodoc_py35.Class.inheritedmeth')) + assert_processes(should, 'class', 'Class') + + # test special members + options.special_members = ['__special1__'] + should.append(('method', 'test_autodoc_py35.Class.__special1__')) + assert_processes(should, 'class', 'Class') + options.special_members = ALL + should.append(('method', 'test_autodoc_py35.Class.__special2__')) + assert_processes(should, 'class', 'Class') + options.special_members = False + + +# --- generate fodder ------------ +import six, sys + +__all__ = ['Class'] + +#: documentation for the integer +integer = 1 + +class CustomEx(Exception): + """My custom exception.""" + + def f(self): + """Exception method.""" + +class CustomDataDescriptor(object): + """Descriptor class docstring.""" + + def __init__(self, doc): + self.__doc__ = doc + + def __get__(self, obj, type=None): + if obj is None: + return self + return 42 + + def meth(self): + """Function.""" + return "The Answer" + +def _funky_classmethod(name, b, c, d, docstring=None): + """Generates a classmethod for a class from a template by filling out + some arguments.""" + def template(cls, a, b, c, d=4, e=5, f=6): + return a, b, c, d, e, f + from functools import partial + function = partial(template, b=b, c=c, d=d) + function.__name__ = name + function.__doc__ = docstring + return classmethod(function) + +class Base(object): + def inheritedmeth(self): + """Inherited function.""" + +if six.PY3 and sys.version_info[:2] >= (3, 5): + + async def _other_coro_func(): + return "run" + + +class Class(Base): + """Class to document.""" + + descr = CustomDataDescriptor("Descriptor instance docstring.") + + def meth(self): + """Function.""" + + def undocmeth(self): + pass + + def skipmeth(self): + """Method that should be skipped.""" + + def excludemeth(self): + """Method that should be excluded.""" + + # should not be documented + skipattr = 'foo' + + #: should be documented -- süß + attr = 'bar' + + @property + def prop(self): + """Property.""" + + docattr = 'baz' + """should likewise be documented -- süß""" + + udocattr = 'quux' + u"""should be documented as well - süß""" + + # initialized to any class imported from another module + mdocattr = StringIO() + """should be documented as well - süß""" + + roger = _funky_classmethod("roger", 2, 3, 4) + + moore = _funky_classmethod("moore", 9, 8, 7, + docstring="moore(a, e, f) -> happiness") + + def __init__(self, arg): + self.inst_attr_inline = None #: an inline documented instance attr + #: a documented instance attribute + self.inst_attr_comment = None + self.inst_attr_string = None + """a documented instance attribute""" + + def __special1__(self): + """documented special method""" + + def __special2__(self): + # undocumented special method + pass + + if six.PY3 and sys.version_info[:2] >= (3, 5): + + async def do_coroutine(self): + """A documented coroutine function""" + + attr_coro_result = await _other_coro_func() diff --git a/tox.ini b/tox.ini index 2855db009..8fcb7b177 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=flake8,py26,py27,py33,py34,pypy,du12,du11,du10 +envlist=flake8,py26,py27,py33,py34,py35,pypy,du12,du11,du10 [testenv] deps= @@ -9,7 +9,7 @@ deps= setenv = SPHINX_TEST_TEMPDIR = {envdir}/testbuild commands= - {envpython} tests/run.py -m '^[tT]est' {posargs} + {envpython} tests/run.py -I py35 -m '^[tT]est' {posargs} sphinx-build -q -W -b html -d {envtmpdir}/doctrees doc {envtmpdir}/html [testenv:py26] @@ -49,3 +49,8 @@ deps= [testenv:flake8] deps=flake8 commands=flake8 + +[testenv:py35] +commands= + {envpython} tests/run.py -m '^[tT]est' {posargs} + sphinx-build -q -W -b html -d {envtmpdir}/doctrees doc {envtmpdir}/html