diff options
26 files changed, 323 insertions, 97 deletions
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7acfef6d2..0159ac34d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,53 @@ -name: CI on Windows +name: CI on: [push, pull_request] jobs: - build: + ubuntu: + runs-on: ubuntu-16.04 + strategy: + fail-fast: false + matrix: + name: [py35, py36, py37, py38, py39] + include: + - name: py35 + python: 3.5 + docutils: du12 + - name: py36 + python: 3.6 + docutils: du13 + - name: py37 + python: 3.7 + docutils: du14 + - name: py38 + python: 3.8 + docutils: du15 + - name: py39 + python: 3.9 + docutils: du16 + 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 47a8e7c7a..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.5' - env: - - TOXENV=du12 - - python: '3.6' - env: - - TOXENV=du13 - - python: '3.7' - env: - - TOXENV=du14 - - python: '3.8' - env: - - TOXENV=du15 - - PYTEST_ADDOPTS="--cov ./ --cov-append --cov-config setup.cfg" - - - 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 @@ -7,19 +7,32 @@ Dependencies Incompatible changes -------------------- +* #8105: autodoc: the signature of class constructor will be shown for decorated + classes, not a signature of decorator + Deprecated ---------- +* The ``follow_wrapped`` argument of ``sphinx.util.inspect.signature()`` + Features added -------------- * #8119: autodoc: Allow to determine whether a member not included in ``__all__`` attribute of the module should be documented or not via :event:`autodoc-skip-member` event +* #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 +* #4606: autodoc: the location of the warning is incorrect for inherited method +* #8105: autodoc: the signature of class constructor is incorrect if the class + is decorated + 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 <https://bitbucket.org/birkenfeld/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 <https://github.com/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/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 2bb8aebfd..797648a1a 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -26,6 +26,12 @@ The following is a list of deprecated interfaces. - (will be) Removed - Alternatives + + * - The ``follow_wrapped`` argument of ``sphinx.util.inspect.signature()`` + - 3.4 + - 5.0 + - N/A + * - ``sphinx.builders.latex.LaTeXBuilder.usepackages`` - 3.3 - 5.0 diff --git a/doc/internals/contributing.rst b/doc/internals/contributing.rst index 1f4a31013..a52e6e72d 100644 --- a/doc/internals/contributing.rst +++ b/doc/internals/contributing.rst @@ -272,9 +272,9 @@ identifier and put ``sphinx.po`` in there. Don't forget to update the possible values for :confval:`language` in ``doc/usage/configuration.rst``. The Sphinx core messages can also be translated on `Transifex -<https://www.transifex.com/sphinx-doc/>`_. There ``tx`` client tool, which is -provided by the ``transifex_client`` Python package, can be used to pull -translations in ``.po`` format from Transifex. To do this, go to +<https://www.transifex.com/sphinx-doc/sphinx-1/>`_. There ``tx`` client tool, +which is provided by the ``transifex_client`` Python package, can be used to +pull translations in ``.po`` format from Transifex. To do this, go to ``sphinx/locale`` and then run ``tx pull -f -l LANG`` where ``LANG`` is an existing language identifier. It is good practice to run ``python setup.py update_catalog`` afterwards to make sure the ``.po`` file has the canonical @@ -203,6 +203,7 @@ setup( 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Framework :: Setuptools Plugin', diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index 39f67b54e..7ea468404 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -610,8 +610,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', @@ -1107,8 +1105,23 @@ class StandardDomain(Domain): RemovedInSphinx40Warning, stacklevel=2) +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 82a52d762..214654706 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -46,6 +46,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 696b06694..a0c5cf61f 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -569,9 +569,18 @@ class Documenter: yield from docstringlines def get_sourcename(self) -> str: + if (getattr(self.object, '__module__', None) and + getattr(self.object, '__qualname__', None)): + # Get the correct location of docstring from self.object + # to support inherited methods + fullname = '%s.%s' % (self.object.__module__, self.object.__qualname__) + else: + fullname = self.fullname + if self.analyzer: - return '%s:docstring of %s' % (self.analyzer.srcname, self.fullname) - return 'docstring of %s' % self.fullname + return '%s:docstring of %s' % (self.analyzer.srcname, fullname) + else: + return 'docstring of %s' % fullname def add_content(self, more_content: Any, no_docstring: bool = False) -> None: """Add content from docstrings, attribute documentation and user.""" @@ -1251,7 +1260,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ try: self.env.app.emit('autodoc-before-process-signature', self.object, False) - sig = inspect.signature(self.object, follow_wrapped=True, + sig = inspect.signature(self.object, type_aliases=self.env.config.autodoc_type_aliases) args = stringify_signature(sig, **kwargs) except TypeError as exc: @@ -1429,7 +1438,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__') @@ -1886,7 +1900,6 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: else: self.env.app.emit('autodoc-before-process-signature', self.object, True) sig = inspect.signature(self.object, bound_method=True, - follow_wrapped=True, type_aliases=self.env.config.autodoc_type_aliases) args = stringify_signature(sig, **kwargs) except TypeError as exc: 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/sphinx/util/inspect.py b/sphinx/util/inspect.py index f2cd8070b..27f478675 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -62,14 +62,6 @@ def getargspec(func: Callable) -> Any: methods.""" warnings.warn('sphinx.ext.inspect.getargspec() is deprecated', RemovedInSphinx50Warning, stacklevel=2) - # On 3.5+, signature(int) or similar raises ValueError. On 3.4, it - # succeeds with a bogus signature. We want a TypeError uniformly, to - # match historical behavior. - if (isinstance(func, type) and - is_builtin_class_method(func, "__new__") and - is_builtin_class_method(func, "__init__")): - raise TypeError( - "can't compute signature for built-in type {}".format(func)) sig = inspect.signature(func) @@ -439,14 +431,20 @@ def _should_unwrap(subject: Callable) -> bool: return False -def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False, +def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = None, type_aliases: Dict = {}) -> inspect.Signature: """Return a Signature object for the given *subject*. :param bound_method: Specify *subject* is a bound method or not :param follow_wrapped: Same as ``inspect.signature()``. - Defaults to ``False`` (get a signature of *subject*). """ + + if follow_wrapped is None: + follow_wrapped = True + else: + warnings.warn('The follow_wrapped argument of sphinx.util.inspect.signature() is ' + 'deprecated', RemovedInSphinx50Warning, stacklevel=2) + try: try: if _should_unwrap(subject): 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 --- /dev/null +++ b/tests/roots/test-domain-py-xref-warning/conf.py 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/roots/test-ext-autodoc/target/decorator.py b/tests/roots/test-ext-autodoc/target/decorator.py index 61398b324..faad3fff9 100644 --- a/tests/roots/test-ext-autodoc/target/decorator.py +++ b/tests/roots/test-ext-autodoc/target/decorator.py @@ -29,3 +29,25 @@ class Bar: @deco1 def meth(self, name=None, age=None): pass + + +class Baz: + @deco1 + def __init__(self, name=None, age=None): + pass + + +class Qux: + @deco1 + def __new__(self, name=None, age=None): + pass + + +class _Metaclass(type): + @deco1 + def __call__(self, name=None, age=None): + pass + + +class Quux(metaclass=_Metaclass): + pass diff --git a/tests/roots/test-ext-autodoc/target/typehints.py b/tests/roots/test-ext-autodoc/target/typehints.py index 1a70eca67..2c9039650 100644 --- a/tests/roots/test-ext-autodoc/target/typehints.py +++ b/tests/roots/test-ext-autodoc/target/typehints.py @@ -1,4 +1,4 @@ -from typing import Tuple, Union +from typing import Any, Tuple, Union def incr(a: int, b: int = 1) -> int: @@ -11,7 +11,7 @@ def decr(a, b = 1): class Math: - def __init__(self, s: str, o: object = None) -> None: + def __init__(self, s: str, o: Any = None) -> None: pass def incr(self, a: int, b: int = 1) -> int: diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index ceea51508..d81b406c2 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -859,3 +859,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..76f01f5b3 --- /dev/null +++ b/tests/test_ext_autodoc_autoclass.py @@ -0,0 +1,75 @@ +""" + 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', + '', + ] + + +def test_decorators(app): + actual = do_autodoc(app, 'class', 'target.decorator.Baz') + assert list(actual) == [ + '', + '.. py:class:: Baz(name=None, age=None)', + ' :module: target.decorator', + '', + ] + + actual = do_autodoc(app, 'class', 'target.decorator.Qux') + assert list(actual) == [ + '', + '.. py:class:: Qux(name=None, age=None)', + ' :module: target.decorator', + '', + ] + + actual = do_autodoc(app, 'class', 'target.decorator.Quux') + assert list(actual) == [ + '', + '.. py:class:: Quux(name=None, age=None)', + ' :module: target.decorator', + '', + ] 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/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index 8ebe12d40..a0eba20c8 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -490,7 +490,7 @@ def test_autodoc_typehints_signature(app): '.. py:module:: target.typehints', '', '', - '.. py:class:: Math(s: str, o: object = None)', + '.. py:class:: Math(s: str, o: Any = None)', ' :module: target.typehints', '', '', diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index c21eaaa16..204fa65b7 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -100,7 +100,7 @@ def test_signature_methods(): # wrapped bound method sig = inspect.signature(wrapped_bound_method) - assert stringify_signature(sig) == '(*args, **kwargs)' + assert stringify_signature(sig) == '(arg1, **kwargs)' def test_signature_partialmethod(): @@ -26,7 +26,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} |