From f2d357ff9a36e3c65272c964f1e8599a93001577 Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Sat, 8 Nov 2025 06:50:41 +0000 Subject: [PATCH 1/9] When option -W is used, all warnings issued with the warnings module now makes pydoctor exit with code 3. As a drive-by-change stop emitting warnings when parsing a regex. Fixes #930 --- README.rst | 2 + pydoctor/_configparser.py | 4 +- pydoctor/driver.py | 4 +- pydoctor/epydoc/sre_parse36.py | 15 ++----- pydoctor/options.py | 6 +-- pydoctor/templatewriter/__init__.py | 10 +++-- pydoctor/templatewriter/util.py | 4 +- pydoctor/test/test_commandline.py | 69 +++++++++++++++++++++++++++++ pydoctor/utils.py | 22 +++++++++ setup.cfg | 1 + 10 files changed, 113 insertions(+), 24 deletions(-) diff --git a/README.rst b/README.rst index d489be1e5..344d2e9d6 100644 --- a/README.rst +++ b/README.rst @@ -73,6 +73,8 @@ What's New? in development ^^^^^^^^^^^^^^ +* When option -W is used, all warnings issued with the warnings module now makes pydoctor exit with code 3. + pydoctor 25.10.1 ^^^^^^^^^^^^^^^^ 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 a396ff3f5..5b280fb3c 100644 --- a/pydoctor/driver.py +++ b/pydoctor/driver.py @@ -8,7 +8,7 @@ from pathlib import Path from pydoctor.options import Options, BUILDTIME_FORMAT -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 @@ -184,7 +184,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 804e4d49b..2f2abb45e 100644 --- a/pydoctor/options.py +++ b/pydoctor/options.py @@ -287,9 +287,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 60acacb29..ca2d98a8b 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 pytest @@ -343,3 +344,71 @@ def test_html_ids_dont_look_like_python_names(tmp_path: Path) -> None: assert re.findall(r'id="[a-z]+"', text, re.IGNORECASE) == ['id="basic"'], text 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. + This is the test for the sh ort form of the CLI option. + + 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. + This is the test for the sh ort form of the CLI option. + + 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\n") + + with warnings.catch_warnings(record=True) as w: + exit_code = driver.main(args=[ + '-W', + '--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'"] diff --git a/pydoctor/utils.py b/pydoctor/utils.py index 6d80387c6..66557dcd0 100644 --- a/pydoctor/utils.py +++ b/pydoctor/utils.py @@ -103,3 +103,25 @@ 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 = False + +def warn(msg: str) -> None: + """ + Emit a pydoctor warning message. + """ + import warnings + warnings.warn(msg, category=PydoctorWarning) + global _warned + _warned = True + +def warned() -> bool: + """ + Return whether any pydoctor warning has been emitted. + """ + return _warned \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 964b1cbb0..22a395282 100644 --- a/setup.cfg +++ b/setup.cfg @@ -110,6 +110,7 @@ doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL xfail_strict = true filterwarnings = error + default::UserWarning [tool:pydoctor] intersphinx = From 452ff15a52fcbb38a4e28ddf57d0ed5094e23b55 Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Sat, 8 Nov 2025 17:21:39 +0000 Subject: [PATCH 2/9] remove unused import --- pydoctor/options.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pydoctor/options.py b/pydoctor/options.py index 2f2abb45e..24b5af802 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 From 0ce62d8a0710df4a8af50da3a92c516d79d83935 Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Sat, 8 Nov 2025 17:29:14 +0000 Subject: [PATCH 3/9] Use contextvars instread of global --- pydoctor/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pydoctor/utils.py b/pydoctor/utils.py index 66557dcd0..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: @@ -109,7 +110,7 @@ class PydoctorWarning(UserWarning): Base class for all warnings emitted by pydoctor thru the L{warnings} module. """ -_warned = False +_warned = contextvars.ContextVar('warned', default=False) def warn(msg: str) -> None: """ @@ -117,11 +118,10 @@ def warn(msg: str) -> None: """ import warnings warnings.warn(msg, category=PydoctorWarning) - global _warned - _warned = True + _warned.set(True) def warned() -> bool: """ Return whether any pydoctor warning has been emitted. """ - return _warned \ No newline at end of file + return _warned.get() \ No newline at end of file From 646f7d65639f6fa2580e04269146dd85bce85d74 Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:16:14 -0500 Subject: [PATCH 4/9] Apply suggestions from code review --- README.rst | 2 +- pydoctor/test/test_commandline.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 344d2e9d6..759cf2762 100644 --- a/README.rst +++ b/README.rst @@ -73,7 +73,7 @@ What's New? in development ^^^^^^^^^^^^^^ -* When option -W is used, all warnings issued with the warnings module now makes pydoctor exit with code 3. +* When option ``-W/--warnings-as-errors`` is used, all warnings issued with the warnings module now makes pydoctor exit with code 3. pydoctor 25.10.1 ^^^^^^^^^^^^^^^^ diff --git a/pydoctor/test/test_commandline.py b/pydoctor/test/test_commandline.py index ca2d98a8b..b29e66c1e 100644 --- a/pydoctor/test/test_commandline.py +++ b/pydoctor/test/test_commandline.py @@ -369,7 +369,6 @@ def test_no_such_option_exits_code0(tmp_path: Path) -> None: 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. - This is the test for the sh ort form of the CLI option. We demonstrate this using a non existing configuration keyword """ @@ -392,7 +391,6 @@ def test_warnings_as_errors_configured_from_config_file_no_such_option_exits_cod 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. - This is the test for the sh ort form of the CLI option. We demonstrate this using a non existing configuration keyword """ From 313a6c6cd5c915dbc8c377278b9b5714af8d9bc1 Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Wed, 12 Nov 2025 20:29:42 -0500 Subject: [PATCH 5/9] Simplify the test test_warnings_as_errors_configured_from_cli_option_no_such_option_exits_code3 --- pydoctor/test/test_commandline.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pydoctor/test/test_commandline.py b/pydoctor/test/test_commandline.py index b29e66c1e..c906cdaeb 100644 --- a/pydoctor/test/test_commandline.py +++ b/pydoctor/test/test_commandline.py @@ -392,18 +392,11 @@ def test_warnings_as_errors_configured_from_cli_option_no_such_option_exits_code """ When `-W` is used it returns 3 as exit code when there are warnings. - We demonstrate this using a non existing configuration keyword + We demonstrate this using deprecated option --enable-intersphinx-cache. """ - - 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=[ - '-W', - '--config', str(conf_file), + '-W', '--enable-intersphinx-cache', '--html-output', str(tmp_path / 'output'), 'pydoctor/test/testpackages/basic/' ]) From 442e201d9d070b0f1b7df448c28c313c1f7ee3af Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Thu, 13 Nov 2025 01:45:09 +0000 Subject: [PATCH 6/9] Fix test --- pydoctor/test/test_commandline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydoctor/test/test_commandline.py b/pydoctor/test/test_commandline.py index c906cdaeb..b8ca46ae9 100644 --- a/pydoctor/test/test_commandline.py +++ b/pydoctor/test/test_commandline.py @@ -402,4 +402,4 @@ def test_warnings_as_errors_configured_from_cli_option_no_such_option_exits_code ]) assert exit_code == 3 - assert [str(warn.message) for warn in w] == ["No such config option: 'no-such-option'"] + assert [str(warn.message) for warn in w] == ["The --enable-intersphinx-cache option is deprecated; the cache is now enabled by default."] From cf7a9044c61686fe59ce0da7703b50dd0a3fe3fd Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:57:32 -0500 Subject: [PATCH 7/9] Update setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 22a395282..403fa7f7e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -109,8 +109,8 @@ addopts = --doctest-glob='*.doctest' --doctest-modules --ignore-glob='*/testpack doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL xfail_strict = true filterwarnings = + default::pydoctor.utils.PydoctorWarning error - default::UserWarning [tool:pydoctor] intersphinx = From 0c316e7e16cf6f84bfe5295abcefcca85f0ec7af Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Sun, 16 Nov 2025 13:50:04 -0500 Subject: [PATCH 8/9] Update README.rst Co-authored-by: Tom Most --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 759cf2762..223032e00 100644 --- a/README.rst +++ b/README.rst @@ -73,7 +73,7 @@ What's New? in development ^^^^^^^^^^^^^^ -* When option ``-W/--warnings-as-errors`` is used, all warnings issued with the warnings module now makes pydoctor exit with code 3. +* When option ``-W/--warnings-as-errors`` is used, pydoctor exits with code 3 when it issues any warning. pydoctor 25.10.1 ^^^^^^^^^^^^^^^^ From 818cde56967f9ed602ef6121e56b2bbe80c9fd62 Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:24:06 -0500 Subject: [PATCH 9/9] Apply suggestions from code review --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 403fa7f7e..8e117d202 100644 --- a/setup.cfg +++ b/setup.cfg @@ -109,8 +109,8 @@ addopts = --doctest-glob='*.doctest' --doctest-modules --ignore-glob='*/testpack doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL xfail_strict = true filterwarnings = - default::pydoctor.utils.PydoctorWarning error + default::pydoctor.utils.PydoctorWarning [tool:pydoctor] intersphinx =