Skip to content

Commit

Permalink
Merge branch '3.1.x' into 3.x
Browse files Browse the repository at this point in the history
  • Loading branch information
tk0miya committed Jul 4, 2020
2 parents f27dfe3 + f743df6 commit 9fd9ede
Show file tree
Hide file tree
Showing 18 changed files with 170 additions and 69 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Expand Up @@ -9,6 +9,6 @@ jobs:
- run: /python3.6/bin/pip install -U pip setuptools
- run: /python3.6/bin/pip install -U .[test]
- run: mkdir -p test-reports/pytest
- run: make test PYTHON=/python3.6/bin/python TEST=--junitxml=test-reports/pytest/results.xml
- run: make test PYTHON=/python3.6/bin/python TEST="--junitxml=test-reports/pytest/results.xml -vv"
- store_test_results:
path: test-reports
21 changes: 21 additions & 0 deletions .github/workflows/builddoc.yml
@@ -0,0 +1,21 @@
name: Build document

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: 3.6
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y graphviz
pip install -U tox
- name: Run Tox
run: tox -e docs
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: false
matrix:
tool: [docslint, flake8, mypy]
tool: [docslint, flake8, mypy, twine]

steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Expand Up @@ -18,4 +18,4 @@ jobs:
- name: Install dependencies
run: pip install -U tox
- name: Run Tox
run: tox -e py
run: tox -e py -- -vv
4 changes: 1 addition & 3 deletions .travis.yml
Expand Up @@ -27,8 +27,6 @@ jobs:
- python: 'nightly'
env:
- TOXENV=du16
- python: '3.6'
env: TOXENV=docs

- language: node_js
node_js: '10.7'
Expand All @@ -41,7 +39,7 @@ install:
- if [ $IS_PYTHON = false ]; then npm install; fi

script:
- if [ $IS_PYTHON = true ]; then tox -- -v; fi
- if [ $IS_PYTHON = true ]; then tox -- -vv; fi
- if [ $IS_PYTHON = false ]; then npm test; fi

after_success:
Expand Down
11 changes: 10 additions & 1 deletion CHANGES
Expand Up @@ -53,7 +53,16 @@ Features added
Bugs fixed
----------

* #7811: sphinx.util.inspect causes circular import problem
* #7844: autodoc: Failed to detect module when relative module name given
* #7856: autodoc: AttributeError is raised when non-class object is given to
the autoclass directive
* #7850: autodoc: KeyError is raised for invalid mark up when autodoc_typehints
is 'description'
* #7812: autodoc: crashed if the target name matches to both an attribute and
module that are same name
* #7812: autosummary: generates broken stub files if the target code contains
an attribute and module that are same name
* #7806: viewcode: Failed to resolve viewcode references on 3rd party builders

Testing
--------
Expand Down
27 changes: 10 additions & 17 deletions sphinx/ext/autodoc/__init__.py
Expand Up @@ -32,7 +32,6 @@
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import inspect
from sphinx.util import logging
from sphinx.util import split_full_qualified_name
from sphinx.util.docstrings import extract_metadata, prepare_docstring
from sphinx.util.inspect import getdoc, object_description, safe_getattr, stringify_signature
from sphinx.util.typing import stringify as stringify_typehint
Expand Down Expand Up @@ -826,7 +825,12 @@ def generate(self, more_content: Any = None, real_modname: str = None,
self.add_line('', sourcename)

# format the object's signature, if any
sig = self.format_signature()
try:
sig = self.format_signature()
except Exception as exc:
logger.warning(__('error while formatting signature for %s: %s'),
self.fullname, exc, type='autodoc')
return

# generate the directive header and options, if applicable
self.add_directive_header(sig)
Expand Down Expand Up @@ -975,14 +979,8 @@ def resolve_name(self, modname: str, parents: Any, path: str, base: Any
) -> Tuple[str, List[str]]:
if modname is None:
if path:
stripped = path.rstrip('.')
modname, qualname = split_full_qualified_name(stripped)
if qualname:
parents = qualname.split(".")
else:
parents = []

if modname is None:
modname = path.rstrip('.')
else:
# if documenting a toplevel object without explicit module,
# it can be contained in another auto directive ...
modname = self.env.temp_data.get('autodoc:module')
Expand Down Expand Up @@ -1015,13 +1013,8 @@ def resolve_name(self, modname: str, parents: Any, path: str, base: Any
# ... if still None, there's no way to know
if mod_cls is None:
return None, []

try:
modname, qualname = split_full_qualified_name(mod_cls)
parents = qualname.split(".") if qualname else []
except ImportError:
parents = mod_cls.split(".")

modname, sep, cls = mod_cls.rpartition('.')
parents = [cls]
# if the module name is still missing, get it like above
if not modname:
modname = self.env.temp_data.get('autodoc:module')
Expand Down
15 changes: 10 additions & 5 deletions sphinx/ext/autodoc/typehints.py
Expand Up @@ -46,11 +46,16 @@ def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element
if objtype == 'class' and app.config.autoclass_content not in ('init', 'both'):
return

signature = cast(addnodes.desc_signature, contentnode.parent[0])
if signature['module']:
fullname = '.'.join([signature['module'], signature['fullname']])
else:
fullname = signature['fullname']
try:
signature = cast(addnodes.desc_signature, contentnode.parent[0])
if signature['module']:
fullname = '.'.join([signature['module'], signature['fullname']])
else:
fullname = signature['fullname']
except KeyError:
# signature node does not have valid context info for the target object
return

annotations = app.env.temp_data.get('annotations', {})
if annotations.get(fullname, {}):
field_lists = [n for n in contentnode if isinstance(n, nodes.field_list)]
Expand Down
13 changes: 9 additions & 4 deletions sphinx/ext/autosummary/generate.py
Expand Up @@ -230,7 +230,8 @@ def scan(self, imported_members: bool) -> List[str]:
def generate_autosummary_content(name: str, obj: Any, parent: Any,
template: AutosummaryRenderer, template_name: str,
imported_members: bool, app: Any,
recursive: bool, context: Dict) -> str:
recursive: bool, context: Dict,
modname: str = None, qualname: str = None) -> str:
doc = get_documenter(app, obj, parent)

def skip_member(obj: Any, name: str, objtype: str) -> bool:
Expand Down Expand Up @@ -319,7 +320,9 @@ def get_modules(obj: Any) -> Tuple[List[str], List[str]]:
ns['attributes'], ns['all_attributes'] = \
get_members(obj, {'attribute', 'property'})

modname, qualname = split_full_qualified_name(name)
if modname is None or qualname is None:
modname, qualname = split_full_qualified_name(name)

if doc.objtype in ('method', 'attribute', 'property'):
ns['class'] = qualname.rsplit(".", 1)[0]

Expand Down Expand Up @@ -401,7 +404,8 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None,
ensuredir(path)

try:
name, obj, parent, mod_name = import_by_name(entry.name)
name, obj, parent, modname = import_by_name(entry.name)
qualname = name.replace(modname + ".", "")
except ImportError as e:
_warn(__('[autosummary] failed to import %r: %s') % (entry.name, e))
continue
Expand All @@ -411,7 +415,8 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None,
context.update(app.config.autosummary_context)

content = generate_autosummary_content(name, obj, parent, template, entry.template,
imported_members, app, entry.recursive, context)
imported_members, app, entry.recursive, context,
modname, qualname)

filename = os.path.join(path, name + suffix)
if os.path.isfile(filename):
Expand Down
6 changes: 2 additions & 4 deletions sphinx/ext/viewcode.py
Expand Up @@ -131,10 +131,8 @@ def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str],

def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnode: Node
) -> Node:
if app.builder.format != 'html':
return None
elif node['reftype'] == 'viewcode':
# resolve our "viewcode" reference nodes -- they need special treatment
# resolve our "viewcode" reference nodes -- they need special treatment
if node['reftype'] == 'viewcode':
return make_refnode(app.builder, node['refdoc'], node['reftarget'],
node['refid'], contnode)

Expand Down
20 changes: 5 additions & 15 deletions sphinx/util/__init__.py
Expand Up @@ -615,26 +615,16 @@ def split_full_qualified_name(name: str) -> Tuple[str, str]:
Therefore you need to mock 3rd party modules if needed before
calling this function.
"""
from sphinx.util import inspect

parts = name.split('.')
for i, part in enumerate(parts, 1):
try:
modname = ".".join(parts[:i])
module = import_module(modname)

# check the module has a member named as attrname
#
# Note: This is needed to detect the attribute having the same name
# as the module.
# ref: https://github.com/sphinx-doc/sphinx/issues/7812
attrname = parts[i]
if hasattr(module, attrname):
value = inspect.safe_getattr(module, attrname)
if not inspect.ismodule(value):
return ".".join(parts[:i]), ".".join(parts[i:])
import_module(modname)
except ImportError:
return ".".join(parts[:i - 1]), ".".join(parts[i - 1:])
if parts[:i - 1]:
return ".".join(parts[:i - 1]), ".".join(parts[i - 1:])
else:
return None, ".".join(parts)
except IndexError:
pass

Expand Down
5 changes: 5 additions & 0 deletions tests/roots/test-ext-autodoc/target/name_conflict/__init__.py
@@ -0,0 +1,5 @@
from .foo import bar

class foo:
"""docstring of target.name_conflict::foo."""
pass
2 changes: 2 additions & 0 deletions tests/roots/test-ext-autodoc/target/name_conflict/foo.py
@@ -0,0 +1,2 @@
class bar:
"""docstring of target.name_conflict.foo::bar."""
81 changes: 71 additions & 10 deletions tests/test_ext_autodoc.py
Expand Up @@ -121,15 +121,16 @@ def verify(objtype, name, result):
verify('class', 'Base', ('test_ext_autodoc', ['Base'], None, None))

# for members
directive.env.ref_context['py:module'] = 'foo'
verify('method', 'util.SphinxTestApp.cleanup',
('foo', ['util', 'SphinxTestApp', 'cleanup'], None, None))
directive.env.ref_context['py:module'] = 'util'
directive.env.ref_context['py:module'] = 'sphinx.testing.util'
verify('method', 'SphinxTestApp.cleanup',
('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None))
directive.env.ref_context['py:module'] = 'sphinx.testing.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', 'cleanup',
('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None))
verify('method', 'SphinxTestApp.cleanup',
('util', ['SphinxTestApp', 'cleanup'], None, None))
('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None))


def test_format_signature(app):
Expand Down Expand Up @@ -800,14 +801,14 @@ def test_autodoc_inner_class(app):
actual = do_autodoc(app, 'class', 'target.Outer.Inner', options)
assert list(actual) == [
'',
'.. py:class:: Outer.Inner()',
' :module: target',
'.. py:class:: Inner()',
' :module: target.Outer',
'',
' Foo',
'',
'',
' .. py:method:: Outer.Inner.meth()',
' :module: target',
' .. py:method:: Inner.meth()',
' :module: target.Outer',
'',
' Foo',
'',
Expand Down Expand Up @@ -1881,6 +1882,43 @@ def test_overload(app):
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_pymodule_for_ModuleLevelDocumenter(app):
app.env.ref_context['py:module'] = 'target.classes'
actual = do_autodoc(app, 'class', 'Foo')
assert list(actual) == [
'',
'.. py:class:: Foo()',
' :module: target.classes',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_pymodule_for_ClassLevelDocumenter(app):
app.env.ref_context['py:module'] = 'target.methods'
actual = do_autodoc(app, 'method', 'Base.meth')
assert list(actual) == [
'',
'.. py:method:: Base.meth()',
' :module: target.methods',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_pyclass_for_ClassLevelDocumenter(app):
app.env.ref_context['py:module'] = 'target.methods'
app.env.ref_context['py:class'] = 'Base'
actual = do_autodoc(app, 'method', 'meth')
assert list(actual) == [
'',
'.. py:method:: Base.meth()',
' :module: target.methods',
'',
]


@pytest.mark.sphinx('dummy', testroot='ext-autodoc')
def test_autodoc(app, status, warning):
app.builder.build_all()
Expand All @@ -1899,3 +1937,26 @@ def test_autodoc(app, status, warning):
alias of bug2437.autodoc_dummy_foo.Foo"""
assert warning.getvalue() == ''


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_name_conflict(app):
actual = do_autodoc(app, 'class', 'target.name_conflict.foo')
assert list(actual) == [
'',
'.. py:class:: foo()',
' :module: target.name_conflict',
'',
' docstring of target.name_conflict::foo.',
'',
]

actual = do_autodoc(app, 'class', 'target.name_conflict.foo.bar')
assert list(actual) == [
'',
'.. py:class:: bar()',
' :module: target.name_conflict.foo',
'',
' docstring of target.name_conflict.foo::bar.',
'',
]
4 changes: 2 additions & 2 deletions tests/test_ext_autodoc_autofunction.py
Expand Up @@ -85,8 +85,8 @@ def test_methoddescriptor(app):
actual = do_autodoc(app, 'function', 'builtins.int.__add__')
assert list(actual) == [
'',
'.. py:function:: int.__add__(self, value, /)',
' :module: builtins',
'.. py:function:: __add__(self, value, /)',
' :module: builtins.int',
'',
' Return self+value.',
'',
Expand Down
8 changes: 8 additions & 0 deletions tests/test_ext_autodoc_configs.py
Expand Up @@ -13,6 +13,8 @@

import pytest

from sphinx.testing import restructuredtext

from test_ext_autodoc import do_autodoc

IS_PYPY = platform.python_implementation() == 'PyPy'
Expand Down Expand Up @@ -633,6 +635,12 @@ def test_autodoc_typehints_description(app):
in context)


@pytest.mark.sphinx('text', testroot='ext-autodoc',
confoverrides={'autodoc_typehints': "description"})
def test_autodoc_typehints_description_for_invalid_node(app):
text = ".. py:function:: hello; world"
restructuredtext.parse(app, text) # raises no error


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_default_options(app):
Expand Down

0 comments on commit 9fd9ede

Please sign in to comment.