diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 7acfef6d2..6d5a8aa0f 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,9 +1,47 @@
-name: CI on Windows
+name: CI
on: [push, pull_request]
jobs:
- build:
+ ubuntu:
+ runs-on: ubuntu-16.04
+ strategy:
+ fail-fast: false
+ matrix:
+ name: [py36, py37, py38]
+ include:
+ - name: py36
+ python: 3.6
+ docutils: du13
+ - name: py37
+ python: 3.7
+ docutils: du14
+ - name: py38
+ python: 3.8
+ docutils: du15
+ coverage: "--cov ./ --cov-append --cov-config setup.cfg"
+ env:
+ PYTEST_ADDOPTS: ${{ matrix.coverage }}
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python ${{ matrix.python }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python }}
+ - name: Check Python version
+ run: python --version
+ - name: Install graphviz
+ run: sudo apt-get install graphviz
+ - name: Install dependencies
+ run: pip install -U tox codecov
+ - name: Run Tox
+ run: tox -e ${{ matrix.docutils }} -- -vv
+ - name: codecov
+ uses: codecov/codecov-action@v1
+ if: matrix.coverage
+
+ windows:
runs-on: windows-latest
strategy:
matrix:
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
new file mode 100644
index 000000000..d7a7c95f1
--- /dev/null
+++ b/.github/workflows/nodejs.yml
@@ -0,0 +1,21 @@
+name: CI (node.js)
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ env:
+ node-version: 10.7
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Use Node.js ${{ env.node-version }}
+ uses: actions/setup-node@v1
+ with:
+ node-version: ${{ env.node-version }}
+ - run: npm install
+ - name: Run headless test
+ uses: GabrielBB/xvfb-action@v1
+ with:
+ run: npm test
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index fdeec0112..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-os: linux
-dist: xenial
-language: python
-cache: pip
-
-env:
- global:
- - PYTHONFAULTHANDLER=x
- - SKIP_LATEX_BUILD=1
- - IS_PYTHON=true
-
-jobs:
- include:
- - python: '3.6'
- env:
- - TOXENV=du14
- - python: '3.7'
- env:
- - TOXENV=du15
- - python: '3.8'
- env:
- - TOXENV=du16
- - PYTEST_ADDOPTS="--cov ./ --cov-append --cov-config setup.cfg"
- - python: '3.9'
- env:
- - TOXENV=py39
-
- - language: node_js
- node_js: '10.7'
- env: IS_PYTHON=false
- services: xvfb
-
-install:
- - "sudo apt-get install graphviz"
- - if [ $IS_PYTHON = true ]; then pip install -U tox codecov; fi
- - if [ $IS_PYTHON = false ]; then npm install; fi
-
-script:
- - if [ $IS_PYTHON = true ]; then tox -- -vv; fi
- - if [ $IS_PYTHON = false ]; then npm test; fi
-
-after_success:
- - if [[ -e .coverage ]]; then codecov -e $TOXENV; fi
diff --git a/CHANGES b/CHANGES
index d1506694d..208bd2715 100644
--- a/CHANGES
+++ b/CHANGES
@@ -64,9 +64,15 @@ Deprecated
Features added
--------------
+* #6914: Add a new event :event:`warn-missing-reference` to custom warning
+ messages when failed to resolve a cross-reference
+* #6914: Emit a detailed warning when failed to resolve a ``:ref:`` reference
+
Bugs fixed
----------
+* #7613: autodoc: autodoc does not respect __signature__ of the class
+
Testing
--------
diff --git a/doc/develop.rst b/doc/develop.rst
index 1287a6539..3bbc220b8 100644
--- a/doc/develop.rst
+++ b/doc/develop.rst
@@ -22,9 +22,9 @@ Extensions
To learn how to write your own extension, see :ref:`dev-extensions`.
-The `sphinx-contrib `_
-repository contains many contributed extensions. Some of them have their own
-releases on PyPI, others you can install from a checkout.
+The `sphinx-contrib `_ repository contains many
+contributed extensions. Some of them have their own releases on PyPI, others you
+can install from a checkout.
This is the current list of contributed extensions in that repository:
diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst
index df3eb3d67..9f2c10676 100644
--- a/doc/extdev/appapi.rst
+++ b/doc/extdev/appapi.rst
@@ -186,6 +186,7 @@ type for that event::
13. apply post-transforms (by priority): docutils.document -> docutils.document
14. event.doctree-resolved(app, doctree, docname)
- (for any reference node that fails to resolve) event.missing-reference(env, node, contnode)
+ - (for any reference node that fails to resolve) event.warn-missing-reference(domain, node)
15. Generate output files
16. event.build-finished(app, exception)
@@ -284,6 +285,14 @@ Here is a more detailed list of these events.
.. versionadded:: 0.5
+.. event:: warn-missing-reference (app, domain, node)
+
+ Emitted when a cross-reference to an object cannot be resolved even after
+ :event:`missing-reference`. If the event handler can emit warnings for
+ the missing reference, it should return ``True``.
+
+ .. versionadded:: 3.4
+
.. event:: doctree-resolved (app, doctree, docname)
Emitted when a doctree has been "resolved" by the environment, that is, all
diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py
index 198548410..aa4793b66 100644
--- a/sphinx/domains/std.py
+++ b/sphinx/domains/std.py
@@ -595,8 +595,6 @@ class StandardDomain(Domain):
dangling_warnings = {
'term': 'term not in glossary: %(target)s',
- 'ref': 'undefined label: %(target)s (if the link has no caption '
- 'the label must precede a section header)',
'numref': 'undefined label: %(target)s',
'keyword': 'unknown keyword: %(target)s',
'doc': 'unknown document: %(target)s',
@@ -1075,8 +1073,23 @@ class StandardDomain(Domain):
return None
+def warn_missing_reference(app: "Sphinx", domain: Domain, node: pending_xref) -> bool:
+ if domain.name != 'std' or node['reftype'] != 'ref':
+ return None
+ else:
+ target = node['reftarget']
+ if target not in domain.anonlabels: # type: ignore
+ msg = __('undefined label: %s')
+ else:
+ msg = __('Failed to create a cross reference. A title or caption not found: %s')
+
+ logger.warning(msg % target, location=node, type='ref', subtype=node['reftype'])
+ return True
+
+
def setup(app: "Sphinx") -> Dict[str, Any]:
app.add_domain(StandardDomain)
+ app.connect('warn-missing-reference', warn_missing_reference)
return {
'version': 'builtin',
diff --git a/sphinx/events.py b/sphinx/events.py
index ff753b3b6..59d38daf0 100644
--- a/sphinx/events.py
+++ b/sphinx/events.py
@@ -45,6 +45,7 @@ core_events = {
'doctree-read': 'the doctree before being pickled',
'env-merge-info': 'env, read docnames, other env instance',
'missing-reference': 'env, node, contnode',
+ 'warn-missing-reference': 'domain, node',
'doctree-resolved': 'doctree, docname',
'env-updated': 'env',
'html-collect-pages': 'builder',
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index 4c28d7b3c..482b48d88 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -1376,7 +1376,12 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
# This sequence is copied from inspect._signature_from_callable.
# ValueError means that no signature could be found, so we keep going.
- # First, let's see if it has an overloaded __call__ defined
+ # First, we check the obj has a __signature__ attribute
+ if (hasattr(self.object, '__signature__') and
+ isinstance(self.object.__signature__, Signature)):
+ return None, None, self.object.__signature__
+
+ # Next, let's see if it has an overloaded __call__ defined
# in its metaclass
call = get_user_defined_function_or_method(type(self.object), '__call__')
diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py
index 7dc14af52..6633d6434 100644
--- a/sphinx/transforms/post_transforms/__init__.py
+++ b/sphinx/transforms/post_transforms/__init__.py
@@ -166,7 +166,10 @@ class ReferencesResolver(SphinxPostTransform):
warn = False
if not warn:
return
- if domain and typ in domain.dangling_warnings:
+
+ if self.app.emit_firstresult('warn-missing-reference', domain, node):
+ return
+ elif domain and typ in domain.dangling_warnings:
msg = domain.dangling_warnings[typ]
elif node.get('refdomain', 'std') not in ('', 'std'):
msg = (__('%s:%s reference target not found: %%(target)s') %
diff --git a/tests/roots/test-domain-py-xref-warning/conf.py b/tests/roots/test-domain-py-xref-warning/conf.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/roots/test-domain-py-xref-warning/index.rst b/tests/roots/test-domain-py-xref-warning/index.rst
new file mode 100644
index 000000000..6f2cab795
--- /dev/null
+++ b/tests/roots/test-domain-py-xref-warning/index.rst
@@ -0,0 +1,7 @@
+test-domain-py-xref-warning
+===========================
+
+.. _existing-label:
+
+:ref:`no-label`
+:ref:`existing-label`
diff --git a/tests/roots/test-ext-autodoc/target/classes.py b/tests/roots/test-ext-autodoc/target/classes.py
index dc471a6f3..52c23748b 100644
--- a/tests/roots/test-ext-autodoc/target/classes.py
+++ b/tests/roots/test-ext-autodoc/target/classes.py
@@ -1,3 +1,6 @@
+from inspect import Parameter, Signature
+
+
class Foo:
pass
@@ -10,3 +13,11 @@ class Bar:
class Baz:
def __new__(cls, x, y):
pass
+
+
+class Qux:
+ __signature__ = Signature(parameters=[Parameter('foo', Parameter.POSITIONAL_OR_KEYWORD),
+ Parameter('bar', Parameter.POSITIONAL_OR_KEYWORD)])
+
+ def __init__(self, x, y):
+ pass
diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py
index eb3afae1d..e9facc0e2 100644
--- a/tests/test_domain_py.py
+++ b/tests/test_domain_py.py
@@ -876,3 +876,11 @@ def test_noindexentry(app):
assert_node(doctree, (addnodes.index, desc, addnodes.index, desc))
assert_node(doctree[0], addnodes.index, entries=[('single', 'f (built-in class)', 'f', '', None)])
assert_node(doctree[2], addnodes.index, entries=[])
+
+
+@pytest.mark.sphinx('dummy', testroot='domain-py-xref-warning')
+def test_warn_missing_reference(app, status, warning):
+ app.build()
+ assert 'index.rst:6: WARNING: undefined label: no-label' in warning.getvalue()
+ assert ('index.rst:6: WARNING: Failed to create a cross reference. A title or caption not found: existing-label'
+ in warning.getvalue())
diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py
index c0676f23f..703cc13f6 100644
--- a/tests/test_ext_autodoc.py
+++ b/tests/test_ext_autodoc.py
@@ -1832,19 +1832,26 @@ def test_autodoc_for_egged_code(app):
def test_singledispatch(app):
options = {"members": None}
actual = do_autodoc(app, 'module', 'target.singledispatch', options)
- assert list(actual) == [
- '',
- '.. py:module:: target.singledispatch',
- '',
- '',
- '.. py:function:: func(arg, kwarg=None)',
- ' func(arg: int, kwarg=None)',
- ' func(arg: str, kwarg=None)',
- ' :module: target.singledispatch',
- '',
- ' A function for general use.',
- '',
- ]
+ if sys.version_info < (3, 6):
+ # check the result via "in" because the order of singledispatch signatures is
+ # usually changed (because dict is not OrderedDict yet!)
+ assert '.. py:function:: func(arg, kwarg=None)' in actual
+ assert ' func(arg: int, kwarg=None)' in actual
+ assert ' func(arg: str, kwarg=None)' in actual
+ else:
+ assert list(actual) == [
+ '',
+ '.. py:module:: target.singledispatch',
+ '',
+ '',
+ '.. py:function:: func(arg, kwarg=None)',
+ ' func(arg: int, kwarg=None)',
+ ' func(arg: str, kwarg=None)',
+ ' :module: target.singledispatch',
+ '',
+ ' A function for general use.',
+ '',
+ ]
@pytest.mark.skipif(sys.version_info < (3, 8),
diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py
new file mode 100644
index 000000000..89a79c2c7
--- /dev/null
+++ b/tests/test_ext_autodoc_autoclass.py
@@ -0,0 +1,50 @@
+"""
+ test_ext_autodoc_autoclass
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ 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-2020 by the Sphinx team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import pytest
+
+from test_ext_autodoc import do_autodoc
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_classes(app):
+ actual = do_autodoc(app, 'function', 'target.classes.Foo')
+ assert list(actual) == [
+ '',
+ '.. py:function:: Foo()',
+ ' :module: target.classes',
+ '',
+ ]
+
+ actual = do_autodoc(app, 'function', 'target.classes.Bar')
+ assert list(actual) == [
+ '',
+ '.. py:function:: Bar(x, y)',
+ ' :module: target.classes',
+ '',
+ ]
+
+ actual = do_autodoc(app, 'function', 'target.classes.Baz')
+ assert list(actual) == [
+ '',
+ '.. py:function:: Baz(x, y)',
+ ' :module: target.classes',
+ '',
+ ]
+
+ actual = do_autodoc(app, 'function', 'target.classes.Qux')
+ assert list(actual) == [
+ '',
+ '.. py:function:: Qux(foo, bar)',
+ ' :module: target.classes',
+ '',
+ ]
+
diff --git a/tests/test_ext_autodoc_autofunction.py b/tests/test_ext_autodoc_autofunction.py
index bb292bc6a..3c8165995 100644
--- a/tests/test_ext_autodoc_autofunction.py
+++ b/tests/test_ext_autodoc_autofunction.py
@@ -9,6 +9,8 @@
:license: BSD, see LICENSE for details.
"""
+import sys
+
import pytest
from test_ext_autodoc import do_autodoc
@@ -40,6 +42,14 @@ def test_classes(app):
'',
]
+ actual = do_autodoc(app, 'function', 'target.classes.Qux')
+ assert list(actual) == [
+ '',
+ '.. py:function:: Qux(foo, bar)',
+ ' :module: target.classes',
+ '',
+ ]
+
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_callable(app):
@@ -108,16 +118,23 @@ def test_decorated(app):
def test_singledispatch(app):
options = {}
actual = do_autodoc(app, 'function', 'target.singledispatch.func', options)
- assert list(actual) == [
- '',
- '.. py:function:: func(arg, kwarg=None)',
- ' func(arg: int, kwarg=None)',
- ' func(arg: str, kwarg=None)',
- ' :module: target.singledispatch',
- '',
- ' A function for general use.',
- '',
- ]
+ if sys.version_info < (3, 6):
+ # check the result via "in" because the order of singledispatch signatures is
+ # usually changed (because dict is not OrderedDict yet!)
+ assert '.. py:function:: func(arg, kwarg=None)' in actual
+ assert ' func(arg: int, kwarg=None)' in actual
+ assert ' func(arg: str, kwarg=None)' in actual
+ else:
+ assert list(actual) == [
+ '',
+ '.. py:function:: func(arg, kwarg=None)',
+ ' func(arg: int, kwarg=None)',
+ ' func(arg: str, kwarg=None)',
+ ' :module: target.singledispatch',
+ '',
+ ' A function for general use.',
+ '',
+ ]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
diff --git a/tox.ini b/tox.ini
index 01881dd97..3d40d2f97 100644
--- a/tox.ini
+++ b/tox.ini
@@ -24,7 +24,7 @@ extras =
test
setenv =
PYTHONWARNINGS = all,ignore::ImportWarning:importlib._bootstrap_external,ignore::DeprecationWarning:site,ignore::DeprecationWarning:distutils
- PYTEST_ADDOPTS = --color yes
+ PYTEST_ADDOPTS = {env:PYTEST_ADDOPTS:} --color yes
commands=
pytest --durations 25 {posargs}