Stop exiting early with `--fail-on-warnings; add --exception-on-warning` (#12743)

Co-authored-by: Jeremy Maitin-Shepard <jbms@google.com>
This commit is contained in:
Adam Turner
2024-08-13 17:12:42 +01:00
committed by GitHub
parent 0b17bb1029
commit fadb6b10cb
20 changed files with 160 additions and 177 deletions

View File

@@ -44,4 +44,3 @@ jobs:
--jobs=auto
--show-traceback
--fail-on-warning
--keep-going

View File

@@ -30,6 +30,14 @@ Features added
output files.
* #12474: Support type-dependent search result highlighting via CSS.
Patch by Tim Hoffmann.
* #12743: No longer exit on the first warning when
:option:`--fail-on-warning <sphinx-build --fail-on-warning>` is used.
Instead, exit with a non-zero status if any warnings were generated
during the build.
Patch by Adam Turner.
* #12743: Add :option:`sphinx-build --exception-on-warning`,
to raise an exception when warnings are emitted during the build.
Patch by Adam Turner and Jeremy Maitin-Shepard.
Bugs fixed
----------

View File

@@ -263,7 +263,7 @@ To build the documentation, run the following command:
.. code-block:: shell
sphinx-build -M html ./doc ./build/sphinx --fail-on-warning --keep-going
sphinx-build -M html ./doc ./build/sphinx --fail-on-warning
This will parse the Sphinx documentation's source files and generate HTML for
you to preview in :file:`build/sphinx/html`.

View File

@@ -43,7 +43,7 @@ Options
the source and output directories, before any other options are passed.
For example::
sphinx-build -M html ./source ./build --fail-on-warning --keep-going
sphinx-build -M html ./source ./build --fail-on-warning
The *make-mode* provides the same build functionality as
a default :ref:`Makefile or Make.bat <makefile_options>`,
@@ -253,20 +253,35 @@ Options
.. option:: -W, --fail-on-warning
Turn warnings into errors. This means that the build stops at the first
warning and ``sphinx-build`` exits with exit status 1.
Turn warnings into errors.
This means that :program:`sphinx-build` exits with exit status 1
if any warnings are generated during the build.
.. versionchanged:: 7.3
Add ``--fail-on-warning`` long option.
.. versionchanged:: 8.1
:program:`sphinx-build` no longer exits on the first warning,
but instead runs the entire build and exits with exit status 1
if any warnings were generated.
This behaviour was previously enabled with :option:`--keep-going`.
.. option:: --keep-going
Only applicable whilst using :option:`--fail-on-warning`,
which by default exits :program:`sphinx-build` on the first warning.
From Sphinx 8.1, :option:`!--keep-going` is always enabled.
Previously, it was only applicable whilst using :option:`--fail-on-warning`,
which by default exited :program:`sphinx-build` on the first warning.
Using :option:`!--keep-going` runs :program:`!sphinx-build` to completion
and exits with exit status 1 if errors are encountered.
.. versionadded:: 1.8
.. versionchanged:: 8.1
:program:`sphinx-build` no longer exits on the first warning,
meaning that in effect :option:`!--fail-on-warning` is always enabled.
The option is retained for compatibility, but may be removed at some
later date.
.. xref RemovedInSphinx10Warning: deprecate this option in Sphinx 10
or no earlier than 2026-01-01.
.. option:: -T, --show-traceback
@@ -287,6 +302,13 @@ Options
.. versionchanged:: 7.3
Add ``--pdb`` long option.
.. option:: --exception-on-warning
Raise an exception when a warning is emitted during the build.
This can be useful in combination with :option:`--pdb` to debug warnings.
.. versionadded:: 8.1
.. option:: -h, --help, --version
Display usage summary or Sphinx version.

View File

@@ -770,6 +770,10 @@ There are also config values that you can set:
If ``False`` is given, autodoc forcedly suppresses the error if the imported
module emits warnings. By default, ``True``.
.. versionchanged:: 8.1
This option now has no effect as :option:`!--fail-on-warning`
no longer exits early.
.. confval:: autodoc_inherit_docstrings
This value controls the docstrings inheritance.

View File

@@ -41,6 +41,8 @@ from sphinx.util.osutil import ensuredir, relpath
from sphinx.util.tags import Tags
if TYPE_CHECKING:
from typing import Final
from docutils import nodes
from docutils.nodes import Element, Node
from docutils.parsers import Parser
@@ -134,7 +136,7 @@ class Sphinx:
:ivar outdir: Directory for storing build documents.
"""
warningiserror: bool
warningiserror: Final = False
_warncount: int
def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[str] | None,
@@ -144,7 +146,7 @@ class Sphinx:
freshenv: bool = False, warningiserror: bool = False,
tags: Sequence[str] = (),
verbosity: int = 0, parallel: int = 0, keep_going: bool = False,
pdb: bool = False) -> None:
pdb: bool = False, exception_on_warning: bool = False) -> None:
"""Initialize the Sphinx application.
:param srcdir: The path to the source directory.
@@ -163,8 +165,9 @@ class Sphinx:
:param verbosity: The verbosity level.
:param parallel: The maximum number of parallel jobs to use
when reading/writing documents.
:param keep_going: If true, continue processing when an error occurs.
:param keep_going: Unused.
:param pdb: If true, enable the Python debugger on an exception.
:param exception_on_warning: If true, raise an exception on warnings.
"""
self.phase = BuildPhase.INITIALIZATION
self.verbosity = verbosity
@@ -203,12 +206,10 @@ class Sphinx:
else:
self._warning = warning
self._warncount = 0
self.keep_going = warningiserror and keep_going
if self.keep_going:
self.warningiserror = False
else:
self.warningiserror = warningiserror
self.keep_going = bool(warningiserror) # Unused
self._fail_on_warnings = bool(warningiserror)
self.pdb = pdb
self._exception_on_warning = exception_on_warning
logging.setup(self, self._status, self._warning)
self.events = EventManager(self)
@@ -386,26 +387,31 @@ class Sphinx:
self.events.emit('build-finished', err)
raise
if self._warncount and self.keep_going:
self.statuscode = 1
status = (__('succeeded') if self.statuscode == 0
else __('finished with problems'))
if self._warncount:
if self.warningiserror:
if self._warncount == 1:
msg = __('build %s, %s warning (with warnings treated as errors).')
else:
msg = __('build %s, %s warnings (with warnings treated as errors).')
if self._warncount == 0:
if self.statuscode != 0:
logger.info(bold(__('build finished with problems.')))
else:
if self._warncount == 1:
msg = __('build %s, %s warning.')
else:
msg = __('build %s, %s warnings.')
logger.info(bold(msg), status, self._warncount)
logger.info(bold(__('build succeeded.')))
elif self._warncount == 1:
if self._fail_on_warnings:
self.statuscode = 1
msg = __('build finished with problems, 1 warning '
'(with warnings treated as errors).')
elif self.statuscode != 0:
msg = __('build finished with problems, 1 warning.')
else:
msg = __('build succeeded, 1 warning.')
logger.info(bold(msg))
else:
logger.info(bold(__('build %s.')), status)
if self._fail_on_warnings:
self.statuscode = 1
msg = __('build finished with problems, %s warnings '
'(with warnings treated as errors).')
elif self.statuscode != 0:
msg = __('build finished with problems, %s warnings.')
else:
msg = __('build succeeded, %s warnings.')
logger.info(bold(msg), self._warncount)
if self.statuscode == 0 and self.builder.epilog:
logger.info('')

View File

@@ -6,6 +6,7 @@ import codecs
import pickle
import re
import time
from contextlib import nullcontext
from os import path
from typing import TYPE_CHECKING, Any, Literal, final
@@ -327,7 +328,7 @@ class Builder:
logger.info(bold(__('building [%s]: ')) + summary, self.name)
# while reading, collect all warnings from docutils
with logging.pending_warnings():
with nullcontext() if self.app._exception_on_warning else logging.pending_warnings():
updated_docnames = set(self.read())
doccount = len(updated_docnames)
@@ -627,7 +628,7 @@ class Builder:
self._write_serial(sorted(docnames))
def _write_serial(self, docnames: Sequence[str]) -> None:
with logging.pending_warnings():
with nullcontext() if self.app._exception_on_warning else logging.pending_warnings():
for docname in status_iterator(docnames, __('writing output... '), "darkgreen",
len(docnames), self.app.verbosity):
self.app.phase = BuildPhase.RESOLVING

View File

@@ -106,7 +106,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
elif result.status == 'working':
logger.info(darkgreen('ok ') + result.uri + result.message)
elif result.status == 'timeout':
if self.app.quiet or self.app.warningiserror:
if self.app.quiet:
logger.warning('timeout ' + result.uri + result.message,
location=(result.docname, result.lineno))
else:
@@ -115,7 +115,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
result.uri + ': ' + result.message)
self.timed_out_hyperlinks += 1
elif result.status == 'broken':
if self.app.quiet or self.app.warningiserror:
if self.app.quiet:
logger.warning(__('broken link: %s (%s)'), result.uri, result.message,
location=(result.docname, result.lineno))
else:

View File

@@ -201,12 +201,14 @@ files can be built by specifying individual filenames.
help=__('write warnings (and errors) to given file'))
group.add_argument('--fail-on-warning', '-W', action='store_true', dest='warningiserror',
help=__('turn warnings into errors'))
group.add_argument('--keep-going', action='store_true', dest='keep_going',
help=__("with --fail-on-warning, keep going when getting warnings"))
group.add_argument('--keep-going', action='store_true', help=argparse.SUPPRESS)
group.add_argument('--show-traceback', '-T', action='store_true', dest='traceback',
help=__('show full traceback on exception'))
group.add_argument('--pdb', '-P', action='store_true', dest='pdb',
help=__('run Pdb on exception'))
group.add_argument('--exception-on-warning', action='store_true',
dest='exception_on_warning',
help=__('raise an exception on warnings'))
return parser
@@ -329,11 +331,16 @@ def build_main(argv: Sequence[str]) -> int:
try:
confdir = args.confdir or args.sourcedir
with patch_docutils(confdir), docutils_namespace():
app = Sphinx(args.sourcedir, args.confdir, args.outputdir,
args.doctreedir, args.builder, args.confoverrides, args.status,
args.warning, args.freshenv, args.warningiserror,
args.tags, args.verbosity, args.jobs, args.keep_going,
args.pdb)
app = Sphinx(
srcdir=args.sourcedir, confdir=args.confdir,
outdir=args.outputdir, doctreedir=args.doctreedir,
buildername=args.builder, confoverrides=args.confoverrides,
status=args.status, warning=args.warning,
freshenv=args.freshenv, warningiserror=args.warningiserror,
tags=args.tags,
verbosity=args.verbosity, parallel=args.jobs, keep_going=False,
pdb=args.pdb, exception_on_warning=args.exception_on_warning,
)
app.build(args.force_all, args.filenames)
return app.statuscode
except (Exception, KeyboardInterrupt) as exc:

View File

@@ -415,9 +415,10 @@ class Documenter:
"""
with mock(self.config.autodoc_mock_imports):
try:
ret = import_object(self.modname, self.objpath, self.objtype,
attrgetter=self.get_attr,
warningiserror=self.config.autodoc_warningiserror)
ret = import_object(
self.modname, self.objpath, self.objtype,
attrgetter=self.get_attr,
)
self.module, self.parent, self.object_name, self.object = ret
if ismock(self.object):
self.object = undecorate(self.object)
@@ -1960,7 +1961,7 @@ class UninitializedGlobalVariableMixin(DataDocumenterMixinBase):
# annotation only instance variable (PEP-526)
try:
with mock(self.config.autodoc_mock_imports):
parent = import_module(self.modname, self.config.autodoc_warningiserror)
parent = import_module(self.modname)
annotations = get_type_hints(parent, None,
self.config.autodoc_type_aliases,
include_extras=True)
@@ -2455,9 +2456,10 @@ class RuntimeInstanceAttributeMixin(DataDocumenterMixinBase):
except ImportError as exc:
try:
with mock(self.config.autodoc_mock_imports):
ret = import_object(self.modname, self.objpath[:-1], 'class',
attrgetter=self.get_attr, # type: ignore[attr-defined]
warningiserror=self.config.autodoc_warningiserror)
ret = import_object(
self.modname, self.objpath[:-1], 'class',
attrgetter=self.get_attr, # type: ignore[attr-defined]
)
parent = ret[3]
if self.is_runtime_instance_attribute(parent):
self.object = self.RUNTIME_INSTANCE_ATTRIBUTE
@@ -2509,9 +2511,10 @@ class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
return super().import_object(raiseerror=True) # type: ignore[misc]
except ImportError as exc:
try:
ret = import_object(self.modname, self.objpath[:-1], 'class',
attrgetter=self.get_attr, # type: ignore[attr-defined]
warningiserror=self.config.autodoc_warningiserror)
ret = import_object(
self.modname, self.objpath[:-1], 'class',
attrgetter=self.get_attr, # type: ignore[attr-defined]
)
parent = ret[3]
if self.is_uninitialized_instance_attribute(parent):
self.object = UNINITIALIZED_ATTR

View File

@@ -137,24 +137,22 @@ def unmangle(subject: Any, name: str) -> str | None:
return name
def import_module(modname: str, warningiserror: bool = False) -> Any:
def import_module(modname: str) -> Any:
"""Call importlib.import_module(modname), convert exceptions to ImportError."""
try:
with logging.skip_warningiserror(not warningiserror):
return importlib.import_module(modname)
return importlib.import_module(modname)
except BaseException as exc:
# Importing modules may cause any side effects, including
# SystemExit, so we need to catch all errors.
raise ImportError(exc, traceback.format_exc()) from exc
def _reload_module(module: ModuleType, warningiserror: bool = False) -> Any:
def _reload_module(module: ModuleType) -> Any:
"""
Call importlib.reload(module), convert exceptions to ImportError
"""
try:
with logging.skip_warningiserror(not warningiserror):
return importlib.reload(module)
return importlib.reload(module)
except BaseException as exc:
# Importing modules may cause any side effects, including
# SystemExit, so we need to catch all errors.
@@ -162,8 +160,7 @@ def _reload_module(module: ModuleType, warningiserror: bool = False) -> Any:
def import_object(modname: str, objpath: list[str], objtype: str = '',
attrgetter: Callable[[Any, str], Any] = safe_getattr,
warningiserror: bool = False) -> Any:
attrgetter: Callable[[Any, str], Any] = safe_getattr) -> Any:
if objpath:
logger.debug('[autodoc] from %s import %s', modname, '.'.join(objpath))
else:
@@ -176,7 +173,7 @@ def import_object(modname: str, objpath: list[str], objtype: str = '',
while module is None:
try:
original_module_names = frozenset(sys.modules)
module = import_module(modname, warningiserror=warningiserror)
module = import_module(modname)
if os.environ.get('SPHINX_AUTODOC_RELOAD_MODULES'):
new_modules = [m for m in sys.modules if m not in original_module_names]
# Try reloading modules with ``typing.TYPE_CHECKING == True``.

View File

@@ -71,7 +71,7 @@ class DummyApplication:
self.translator = translator
self.verbosity = 0
self._warncount = 0
self.warningiserror = False
self._exception_on_warning = False
self.config.add('autosummary_context', {}, 'env', ())
self.config.add('autosummary_filename_map', {}, 'env', ())

View File

@@ -241,7 +241,7 @@ class CoverageBuilder(Builder):
for typ, name in sorted(undoc):
op.write(' * %-50s [%9s]\n' % (name, typ))
if self.config.coverage_show_missing_items:
if self.app.quiet or self.app.warningiserror:
if self.app.quiet:
logger.warning(__('undocumented c api: %s [%s] in file %s'),
name, typ, filename)
else:
@@ -423,7 +423,7 @@ class CoverageBuilder(Builder):
op.write('Functions:\n')
op.writelines(' * %s\n' % x for x in undoc['funcs'])
if self.config.coverage_show_missing_items:
if self.app.quiet or self.app.warningiserror:
if self.app.quiet:
for func in undoc['funcs']:
logger.warning(
__('undocumented python function: %s :: %s'),
@@ -440,7 +440,7 @@ class CoverageBuilder(Builder):
if not methods:
op.write(' * %s\n' % class_name)
if self.config.coverage_show_missing_items:
if self.app.quiet or self.app.warningiserror:
if self.app.quiet:
logger.warning(
__('undocumented python class: %s :: %s'),
name, class_name)
@@ -452,7 +452,7 @@ class CoverageBuilder(Builder):
op.write(' * %s -- missing methods:\n\n' % class_name)
op.writelines(' - %s\n' % x for x in methods)
if self.config.coverage_show_missing_items:
if self.app.quiet or self.app.warningiserror:
if self.app.quiet:
for meth in methods:
logger.warning(
__('undocumented python method:'

View File

@@ -322,7 +322,7 @@ class DocTestBuilder(Builder):
self.outfile.write(text)
def _warn_out(self, text: str) -> None:
if self.app.quiet or self.app.warningiserror:
if self.app.quiet:
logger.warning(text)
else:
logger.info(text, nonl=True)

View File

@@ -28,7 +28,7 @@ DEFAULT_ENABLED_MARKERS = [
'testroot="root", srcdir=None, '
'confoverrides=None, freshenv=False, '
'warningiserror=False, tags=None, verbosity=0, parallel=0, '
'keep_going=False, builddir=None, docutils_conf=None'
'builddir=None, docutils_conf=None'
'): arguments to initialize the sphinx test application.'
),
'test_params(shared_result=...): test parameters.',

View File

@@ -117,8 +117,9 @@ class SphinxTestApp(sphinx.application.Sphinx):
parallel: int = 0,
# additional arguments at the end to keep the signature
verbosity: int = 0, # argument is not in the same order as in the superclass
keep_going: bool = False,
warningiserror: bool = False, # argument is not in the same order as in the superclass
pdb: bool = False,
exception_on_warning: bool = False,
# unknown keyword arguments
**extras: Any,
) -> None:
@@ -170,8 +171,8 @@ class SphinxTestApp(sphinx.application.Sphinx):
srcdir, confdir, outdir, doctreedir, buildername,
confoverrides=confoverrides, status=status, warning=warning,
freshenv=freshenv, warningiserror=warningiserror, tags=tags,
verbosity=verbosity, parallel=parallel, keep_going=keep_going,
pdb=False,
verbosity=verbosity, parallel=parallel,
pdb=pdb, exception_on_warning=exception_on_warning,
)
except Exception:
self.cleanup()

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import logging
import logging.handlers
from collections import defaultdict
from contextlib import contextmanager
from contextlib import contextmanager, nullcontext
from typing import IO, TYPE_CHECKING, Any
from docutils import nodes
@@ -17,6 +17,7 @@ from sphinx.util.osutil import abspath
if TYPE_CHECKING:
from collections.abc import Iterator, Sequence, Set
from typing import NoReturn
from docutils.nodes import Node
@@ -322,24 +323,7 @@ def pending_logging() -> Iterator[MemoryHandler]:
memhandler.flushTo(logger)
@contextmanager
def skip_warningiserror(skip: bool = True) -> Iterator[None]:
"""Context manager to skip WarningIsErrorFilter temporarily."""
logger = logging.getLogger(NAMESPACE)
if skip is False:
yield
else:
try:
disabler = DisableWarningIsErrorFilter()
for handler in logger.handlers:
# use internal method; filters.insert() directly to install disabler
# before WarningIsErrorFilter
handler.filters.insert(0, disabler)
yield
finally:
for handler in logger.handlers:
handler.removeFilter(disabler)
skip_warningiserror = nullcontext # Deprecate in Sphinx 10
@contextmanager
@@ -407,6 +391,21 @@ class InfoFilter(logging.Filter):
return record.levelno < logging.WARNING
class _RaiseOnWarningFilter(logging.Filter):
"""Raise exception if a warning is emitted."""
def filter(self, record: logging.LogRecord) -> NoReturn:
try:
message = record.msg % record.args
except (TypeError, ValueError):
message = record.msg # use record.msg itself
if location := getattr(record, 'location', ''):
message = f"{location}:{message}"
if record.exc_info is not None:
raise SphinxWarning(message) from record.exc_info[1]
raise SphinxWarning(message)
def is_suppressed_warning(
warning_type: str, sub_type: str, suppress_warnings: Set[str] | Sequence[str],
) -> bool:
@@ -445,44 +444,6 @@ class WarningSuppressor(logging.Filter):
return True
class WarningIsErrorFilter(logging.Filter):
"""Raise exception if warning emitted."""
def __init__(self, app: Sphinx) -> None:
self.app = app
super().__init__()
def filter(self, record: logging.LogRecord) -> bool:
if getattr(record, 'skip_warningsiserror', False):
# disabled by DisableWarningIsErrorFilter
return True
elif self.app.warningiserror:
location = getattr(record, 'location', '')
try:
message = record.msg % record.args
except (TypeError, ValueError):
message = record.msg # use record.msg itself
if location:
exc = SphinxWarning(location + ":" + str(message))
else:
exc = SphinxWarning(message)
if record.exc_info is not None:
raise exc from record.exc_info[1]
else:
raise exc
else:
return True
class DisableWarningIsErrorFilter(logging.Filter):
"""Disable WarningIsErrorFilter if this filter installed."""
def filter(self, record: logging.LogRecord) -> bool:
record.skip_warningsiserror = True
return True
class MessagePrefixFilter(logging.Filter):
"""Prepend prefix to all log records."""
@@ -653,9 +614,10 @@ def setup(app: Sphinx, status: IO, warning: IO) -> None:
info_handler.setFormatter(ColorizeFormatter())
warning_handler = WarningStreamHandler(SafeEncodingWriter(warning))
if app._exception_on_warning:
warning_handler.addFilter(_RaiseOnWarningFilter())
warning_handler.addFilter(WarningSuppressor(app))
warning_handler.addFilter(WarningLogRecordTranslator(app))
warning_handler.addFilter(WarningIsErrorFilter(app))
warning_handler.addFilter(OnceFilter())
warning_handler.setLevel(logging.WARNING)
warning_handler.setFormatter(ColorizeFormatter())

View File

@@ -1,9 +1,11 @@
import os
import re
import sys
import traceback
import pytest
from sphinx.errors import SphinxError
from sphinx.util.console import strip_colors
ENV_WARNINGS = """\
@@ -71,6 +73,22 @@ def test_html_warnings(app):
_check_warnings(warnings_exp, app.warning.getvalue())
@pytest.mark.sphinx(
'html',
testroot='warnings',
freshenv=True,
exception_on_warning=True,
)
def test_html_warnings_exception_on_warning(app):
try:
app.build(force_all=True)
pytest.fail('Expected an exception to be raised')
except SphinxError:
tb = traceback.format_exc()
assert 'unindent_warning' in tb
assert 'pending_warnings' not in tb
@pytest.mark.sphinx(
'latex',
testroot='warnings',

View File

@@ -7,7 +7,6 @@ import os.path
import pytest
from docutils import nodes
from sphinx.errors import SphinxWarning
from sphinx.util import logging, osutil
from sphinx.util.console import colorize, strip_colors
from sphinx.util.logging import is_suppressed_warning, prefixed_warnings
@@ -176,25 +175,6 @@ def test_suppress_warnings(app):
assert app._warncount == 8
@pytest.mark.sphinx('html', testroot='root')
def test_warningiserror(app):
logging.setup(app, app.status, app.warning)
logger = logging.getLogger(__name__)
# if False, warning is not error
app.warningiserror = False
logger.warning('message')
# if True, warning raises SphinxWarning exception
app.warningiserror = True
with pytest.raises(SphinxWarning):
logger.warning('message: %s', 'arg')
# message contains format string (refs: #4070)
with pytest.raises(SphinxWarning):
logger.warning('%s')
@pytest.mark.sphinx('html', testroot='root')
def test_info_location(app):
logging.setup(app, app.status, app.warning)
@@ -356,31 +336,6 @@ def test_output_with_unencodable_char(app):
assert app.status.getvalue() == 'unicode ?...\n'
@pytest.mark.sphinx('html', testroot='root')
def test_skip_warningiserror(app):
logging.setup(app, app.status, app.warning)
logger = logging.getLogger(__name__)
app.warningiserror = True
with logging.skip_warningiserror():
logger.warning('message')
# if False, warning raises SphinxWarning exception
with logging.skip_warningiserror(False): # NoQA: SIM117
with pytest.raises(SphinxWarning):
logger.warning('message')
# It also works during pending_warnings.
with logging.pending_warnings(): # NoQA: SIM117
with logging.skip_warningiserror():
logger.warning('message')
with pytest.raises(SphinxWarning): # NoQA: PT012,SIM117
with logging.pending_warnings():
with logging.skip_warningiserror(False):
logger.warning('message')
@pytest.mark.sphinx('html', testroot='root')
def test_prefixed_warnings(app):
logging.setup(app, app.status, app.warning)

View File

@@ -48,7 +48,7 @@ extras =
docs
commands =
python -c "import shutil; shutil.rmtree('./build/sphinx', ignore_errors=True) if '{env:CLEAN:}' else None"
sphinx-build -M {env:BUILDER:html} ./doc ./build/sphinx -nW --keep-going {posargs}
sphinx-build -M {env:BUILDER:html} ./doc ./build/sphinx --fail-on-warning {posargs}
[testenv:docs-live]
description =