Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 13 commits into from
Aug 13, 2024
1 change: 0 additions & 1 deletion .github/workflows/builddoc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,3 @@ jobs:
--jobs=auto
--show-traceback
--fail-on-warning
--keep-going
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand Down
2 changes: 1 addition & 1 deletion doc/internals/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
32 changes: 27 additions & 5 deletions doc/man/sphinx-build.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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>`,
Expand Down Expand Up @@ -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

Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions doc/usage/extensions/autodoc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
58 changes: 32 additions & 26 deletions sphinx/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
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
Expand Down Expand Up @@ -134,7 +136,7 @@ class Sphinx:
:ivar outdir: Directory for storing build documents.
"""

warningiserror: bool
warningiserror: Final = False
picnixz marked this conversation as resolved.
Show resolved Hide resolved
_warncount: int

def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[str] | None,
Expand All @@ -144,7 +146,7 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st
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.
Expand All @@ -163,8 +165,9 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st
: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
Expand Down Expand Up @@ -203,12 +206,10 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st
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)
Expand Down Expand Up @@ -386,26 +387,31 @@ def build(self, force_all: bool = False, filenames: list[str] | None = None) ->
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))
AA-Turner marked this conversation as resolved.
Show resolved Hide resolved
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('')
Expand Down
5 changes: 3 additions & 2 deletions sphinx/builders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pickle
import re
import time
from contextlib import nullcontext
from os import path
from typing import TYPE_CHECKING, Any, Literal, final

Expand Down Expand Up @@ -327,7 +328,7 @@ def build(
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)
Expand Down Expand Up @@ -627,7 +628,7 @@ def write(
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
Expand Down
4 changes: 2 additions & 2 deletions sphinx/builders/linkcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def process_result(self, result: CheckResult) -> None:
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:
Expand All @@ -115,7 +115,7 @@ def process_result(self, result: CheckResult) -> None:
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:
Expand Down
21 changes: 14 additions & 7 deletions sphinx/cmd/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,14 @@ def get_parser() -> argparse.ArgumentParser:
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

Expand Down Expand Up @@ -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:
Expand Down
23 changes: 13 additions & 10 deletions sphinx/ext/autodoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,9 +415,10 @@ def import_object(self, raiseerror: bool = False) -> bool:
"""
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)
Expand Down Expand Up @@ -1960,7 +1961,7 @@ def import_object(self, raiseerror: bool = False) -> bool:
# 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)
Expand Down Expand Up @@ -2455,9 +2456,10 @@ def import_object(self, raiseerror: bool = False) -> bool:
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
Expand Down Expand Up @@ -2509,9 +2511,10 @@ def import_object(self, raiseerror: bool = False) -> bool:
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
Expand Down
15 changes: 6 additions & 9 deletions sphinx/ext/autodoc/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,33 +137,30 @@ 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.
raise ImportError(exc, traceback.format_exc()) from exc


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:
Expand All @@ -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``.
Expand Down
2 changes: 1 addition & 1 deletion sphinx/ext/autosummary/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(self, translator: NullTranslations) -> None:
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', ())
Expand Down
Loading