mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
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:
1
.github/workflows/builddoc.yml
vendored
1
.github/workflows/builddoc.yml
vendored
@@ -44,4 +44,3 @@ jobs:
|
||||
--jobs=auto
|
||||
--show-traceback
|
||||
--fail-on-warning
|
||||
--keep-going
|
||||
|
||||
@@ -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
|
||||
----------
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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('')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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``.
|
||||
|
||||
@@ -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', ())
|
||||
|
||||
@@ -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:'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
2
tox.ini
2
tox.ini
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user