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}