diff --git a/README.rst b/README.rst index 687ba6713..7db26c884 100644 --- a/README.rst +++ b/README.rst @@ -73,6 +73,7 @@ What's New? in development ^^^^^^^^^^^^^^ +* When option ``-W/--warnings-as-errors`` is used, pydoctor exits with code 3 when it issues any warning. * Hide sidebar element title when all items under it are private. * Allow suppressing the footer's buildtime altogether with option ``--buildtime=no``. * Add project version on each HTML page. diff --git a/pydoctor/_configparser.py b/pydoctor/_configparser.py index e38d697ad..842475646 100644 --- a/pydoctor/_configparser.py +++ b/pydoctor/_configparser.py @@ -31,7 +31,6 @@ import functools import configparser from ast import literal_eval -import warnings from configargparse import ConfigFileParserException, ConfigFileParser, ArgumentParser @@ -435,7 +434,8 @@ def parse(self, stream:TextIO) -> Dict[str, Any]: action = known_config_keys.get(key) if not action: # Warn "no such config option" - warnings.warn(f"No such config option: {key!r}") + from pydoctor.utils import warn + warn(f"No such config option: {key!r}") # Remove option else: new_data[key] = value diff --git a/pydoctor/driver.py b/pydoctor/driver.py index a9a10c797..8d53393b3 100644 --- a/pydoctor/driver.py +++ b/pydoctor/driver.py @@ -8,7 +8,7 @@ from pathlib import Path from pydoctor.options import Options, BUILDTIME_FORMAT, FALSE_VALUES -from pydoctor.utils import error +from pydoctor.utils import error, warned from pydoctor import model from pydoctor.templatewriter import IWriter, TemplateLookup, TemplateError from pydoctor.sphinx import SphinxInventoryWriter, prepareCache @@ -188,7 +188,7 @@ def p(msg: str) -> None: elif any(system.parse_errors.values()): exitcode = 2 - if system.violations and options.warnings_as_errors: + if options.warnings_as_errors and (system.violations or warned()): # Update exit code if the run has produced warnings. exitcode = 3 diff --git a/pydoctor/epydoc/sre_parse36.py b/pydoctor/epydoc/sre_parse36.py index f3c7b81fb..c6f239a44 100644 --- a/pydoctor/epydoc/sre_parse36.py +++ b/pydoctor/epydoc/sre_parse36.py @@ -796,14 +796,8 @@ def _parse(source, state, verbose, nested, first=False): flags = _parse_flags(source, state, char) if flags is None: # global flags if not first or subpattern: - import warnings - warnings.warn( - 'Flags not at the start of the expression %r%s' % ( - source.string[:20], # truncate long regexes - ' (truncated)' if len(source.string) > 20 else '', - ), - DeprecationWarning, stacklevel=nested + 6 - ) + # changed: we don't trigger deprecated warning here + pass if (state.flags & SRE_FLAG_VERBOSE) and not verbose: raise Verbose continue @@ -1008,9 +1002,8 @@ def addgroup(index, pos): this = chr(ESCAPES[this][1]) except KeyError: if c in ASCIILETTERS: - import warnings - warnings.warn('bad escape %s' % this, - DeprecationWarning, stacklevel=4) + # changed: we don't trigger deprecated warning here + pass lappend(this) else: lappend(this) diff --git a/pydoctor/options.py b/pydoctor/options.py index a8db6d1ea..2cf0e1b7e 100644 --- a/pydoctor/options.py +++ b/pydoctor/options.py @@ -5,7 +5,6 @@ import re from typing import NamedTuple, Sequence, List, Optional, Type, Tuple, TYPE_CHECKING -import sys import functools from pathlib import Path from argparse import SUPPRESS, Namespace @@ -291,9 +290,9 @@ def _warn_deprecated_options(options: Namespace) -> None: Check the CLI options and warn on deprecated options. """ if options.enable_intersphinx_cache_deprecated: - print("The --enable-intersphinx-cache option is deprecated; " - "the cache is now enabled by default.", - file=sys.stderr, flush=True) + from pydoctor.utils import warn + warn("The --enable-intersphinx-cache option is deprecated; " + "the cache is now enabled by default.") # CONVERTERS diff --git a/pydoctor/templatewriter/__init__.py b/pydoctor/templatewriter/__init__.py index 4cdddb357..30d7882cd 100644 --- a/pydoctor/templatewriter/__init__.py +++ b/pydoctor/templatewriter/__init__.py @@ -10,7 +10,6 @@ def runtime_checkable(f): return f import abc from pathlib import Path, PurePath -import warnings from xml.dom import minidom # Newer APIs from importlib_resources should arrive to stdlib importlib.resources in Python 3.9. @@ -241,7 +240,8 @@ def _extract_version(dom: minidom.Document, template_name: str) -> int: meta.parentNode.removeChild(meta) if not meta.hasAttribute("content"): - warnings.warn(f"Could not read '{template_name}' template version: " + from pydoctor.utils import warn + warn(f"Could not read '{template_name}' template version: " f"the 'content' attribute is missing") continue @@ -250,7 +250,8 @@ def _extract_version(dom: minidom.Document, template_name: str) -> int: try: version = int(version_str) except ValueError: - warnings.warn(f"Could not read '{template_name}' template version: " + from pydoctor.utils import warn + warn(f"Could not read '{template_name}' template version: " "the 'content' attribute must be an integer") else: break @@ -295,7 +296,8 @@ def _add_overriding_html_template(self, template: HtmlTemplate, current_template template_version = template.version if default_version != -1 and template_version != -1: if template_version < default_version: - warnings.warn(f"Your custom template '{template.name}' is out of date, " + from pydoctor.utils import warn + warn(f"Your custom template '{template.name}' is out of date, " "information might be missing. " "Latest templates are available to download from our github." ) elif template_version > default_version: diff --git a/pydoctor/templatewriter/util.py b/pydoctor/templatewriter/util.py index 95699854c..635a13cf6 100644 --- a/pydoctor/templatewriter/util.py +++ b/pydoctor/templatewriter/util.py @@ -1,7 +1,6 @@ """Miscellaneous utilities for the HTML writer.""" from __future__ import annotations -import warnings from typing import (Any, Callable, Dict, Generic, Iterable, Iterator, List, Mapping, Optional, MutableMapping, Tuple, TypeVar, Union, Sequence, TYPE_CHECKING) from pydoctor import epydoc2stan @@ -164,7 +163,8 @@ def inherited_members(cls: model.Class) -> List[model.Documentable]: def templatefile(filename: str) -> None: """Deprecated: can be removed once Twisted stops patching this.""" - warnings.warn("pydoctor.templatewriter.util.templatefile() " + from pydoctor.utils import warn + warn("pydoctor.templatewriter.util.templatefile() " "is deprecated and returns None. It will be remove in future versions. " "Please use the templating system.") return None diff --git a/pydoctor/test/test_commandline.py b/pydoctor/test/test_commandline.py index dae6aa353..6606e548f 100644 --- a/pydoctor/test/test_commandline.py +++ b/pydoctor/test/test_commandline.py @@ -3,6 +3,7 @@ from pathlib import Path import re import sys +import warnings import logging import pytest @@ -346,6 +347,65 @@ def test_html_ids_dont_look_like_python_names(tmp_path: Path) -> None: else: assert re.findall(r'id="[a-z]+"', text, re.IGNORECASE) == [], text +def test_no_such_option_exits_code0(tmp_path: Path) -> None: + """ + When no such option is used in the config file it just ignores it and + continues normally, whith a warning message printed to stderr. + """ + + tmp_path.mkdir(parents=True, exist_ok=True) + conf_file = (tmp_path / "pydoctor_temp_conf") + with conf_file.open('w') as f: + f.write("[pydoctor]\nno-such-option = somevalue\n") + + with warnings.catch_warnings(record=True) as w: + exit_code = driver.main(args=[ + '--config', str(conf_file), + '--html-output', str(tmp_path / 'output'), + 'pydoctor/test/testpackages/basic/' + ]) + + assert exit_code == 0 + assert [str(warn.message) for warn in w] == ["No such config option: 'no-such-option'"] + +def test_warnings_as_errors_configured_from_config_file_no_such_option_exits_code3(tmp_path: Path) -> None: + """ + When `warnings-as-errors = true` is used it returns 3 as exit code when there are warnings. + + We demonstrate this using a non existing configuration keyword + """ + + tmp_path.mkdir(parents=True, exist_ok=True) + conf_file = (tmp_path / "pydoctor_temp_conf") + with conf_file.open('w') as f: + f.write("[pydoctor]\nno-such-option = somevalue\nwarnings-as-errors = true\n") + + with warnings.catch_warnings(record=True) as w: + exit_code = driver.main(args=[ + '--config', str(conf_file), + '--html-output', str(tmp_path / 'output'), + 'pydoctor/test/testpackages/basic/' + ]) + + assert exit_code == 3 + assert [str(warn.message) for warn in w] == ["No such config option: 'no-such-option'"] + +def test_warnings_as_errors_configured_from_cli_option_no_such_option_exits_code3(tmp_path: Path) -> None: + """ + When `-W` is used it returns 3 as exit code when there are warnings. + + We demonstrate this using deprecated option --enable-intersphinx-cache. + """ + with warnings.catch_warnings(record=True) as w: + exit_code = driver.main(args=[ + '-W', '--enable-intersphinx-cache', + '--html-output', str(tmp_path / 'output'), + 'pydoctor/test/testpackages/basic/' + ]) + + assert exit_code == 3 + assert [str(warn.message) for warn in w] == ["The --enable-intersphinx-cache option is deprecated; the cache is now enabled by default."] + def test_invalid_intersphinx_url_exits_code2(tmp_path: Path, capsys: CapSys, caplog: CapLog) -> None: caplog.set_level(logging.ERROR) # test for issue https://github.com/twisted/pydoctor/issues/751 diff --git a/pydoctor/utils.py b/pydoctor/utils.py index 6d80387c6..37ffff698 100644 --- a/pydoctor/utils.py +++ b/pydoctor/utils.py @@ -4,6 +4,7 @@ from pathlib import Path import sys import functools +import contextvars from typing import Any, Type, TypeVar, Tuple, Union, cast, TYPE_CHECKING if TYPE_CHECKING: @@ -103,3 +104,24 @@ class NewPartialCls(cls): __class__ = cls assert isinstance(NewPartialCls, type) return NewPartialCls + +class PydoctorWarning(UserWarning): + """ + Base class for all warnings emitted by pydoctor thru the L{warnings} module. + """ + +_warned = contextvars.ContextVar('warned', default=False) + +def warn(msg: str) -> None: + """ + Emit a pydoctor warning message. + """ + import warnings + warnings.warn(msg, category=PydoctorWarning) + _warned.set(True) + +def warned() -> bool: + """ + Return whether any pydoctor warning has been emitted. + """ + return _warned.get() \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 9114233e8..4a1de32f2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -111,6 +111,7 @@ xfail_strict = true filterwarnings = default::urllib3.exceptions.NotOpenSSLWarning error + default::pydoctor.utils.PydoctorWarning [tool:pydoctor] intersphinx =