From 3e06b18c0f0c15df2ec4d7f734df54cb1b066555 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Tue, 14 Jul 2026 15:30:08 +0200 Subject: [PATCH 01/10] feat(analyse): add warning-slug registry and hierarchical suppression matcher Introduce CODELINKS_WARNING_SLUGS (the canonical set of warning slugs the analyse layer can emit) and is_suppressed(), a dot-boundary hierarchical matcher so a parent slug silences the family below it. Groundwork for the -W/--strict flag (#90). --- src/sphinx_codelinks/logger.py | 35 ++++++++++++++++++++++++ tests/test_logger.py | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/sphinx_codelinks/logger.py b/src/sphinx_codelinks/logger.py index f100455..c1f03e8 100644 --- a/src/sphinx_codelinks/logger.py +++ b/src/sphinx_codelinks/logger.py @@ -1,3 +1,4 @@ +from collections.abc import Iterable import logging from typing import Protocol @@ -117,6 +118,40 @@ def error( # --------------------------------------------------------------------------- # +# Every warning the ``analyse`` layer can emit, as its fully-qualified slug +# (``codelinks..``). The single source of truth used to expand +# hierarchical ``suppress_warnings`` entries for Sphinx's flat matcher. +CODELINKS_WARNING_SLUGS: tuple[str, ...] = ( + "codelinks.git.root", + "codelinks.git.config", + "codelinks.git.remote", + "codelinks.git.head", + "codelinks.git.ref", + "codelinks.git.host", + "codelinks.marker.too_many_fields", + "codelinks.marker.too_few_fields", + "codelinks.marker.missing_square_brackets", + "codelinks.marker.not_start_or_end_with_square_brackets", + "codelinks.marker.newline_in_field", +) + + +def is_suppressed(slug: str, patterns: Iterable[str]) -> bool: + """Return whether ``slug`` is silenced by any of ``patterns``. + + Matching is hierarchical on dot boundaries: a parent silences everything + below it (``codelinks`` -> all, ``codelinks.git`` -> the git family, + ``codelinks.git.root`` -> just that one). A trailing ``.*`` is accepted as + a Sphinx-style alias for the bare parent. ``codelinks.git`` never matches + ``codelinks.github`` because matching respects the ``.`` separator. + """ + for pattern in patterns: + parent = pattern[:-2] if pattern.endswith(".*") else pattern + if slug == parent or slug.startswith(f"{parent}."): + return True + return False + + class _Backend(Protocol): """Where the ``analyse`` layer's log records are routed. diff --git a/tests/test_logger.py b/tests/test_logger.py index dd43cfb..07bb04b 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -138,3 +138,52 @@ def test_analyse_modules_install_no_handlers_at_import(): assert module_logger.level == logging.NOTSET, ( f"{name} pinned its level at import to {module_logger.level}" ) + + +def test_registry_lists_all_warning_slugs(): + """The canonical registry names every warning slug analyse can emit.""" + assert logmod.CODELINKS_WARNING_SLUGS == ( + "codelinks.git.root", + "codelinks.git.config", + "codelinks.git.remote", + "codelinks.git.head", + "codelinks.git.ref", + "codelinks.git.host", + "codelinks.marker.too_many_fields", + "codelinks.marker.too_few_fields", + "codelinks.marker.missing_square_brackets", + "codelinks.marker.not_start_or_end_with_square_brackets", + "codelinks.marker.newline_in_field", + ) + + +@pytest.mark.parametrize( + ("slug", "patterns", "expected"), + [ + # a bare parent silences the whole hierarchy below it + ("codelinks.git.root", ["codelinks"], True), + ("codelinks.marker.too_many_fields", ["codelinks"], True), + # family level silences only that family + ("codelinks.git.root", ["codelinks.git"], True), + ("codelinks.git.host", ["codelinks.git"], True), + ("codelinks.marker.too_many_fields", ["codelinks.git"], False), + # matching is on dot boundaries, never a raw string prefix + ("codelinks.github", ["codelinks.git"], False), + # exact leaf silences only that leaf + ("codelinks.git.root", ["codelinks.git.root"], True), + ("codelinks.git.host", ["codelinks.git.root"], False), + # a trailing ".*" is accepted as a Sphinx-style alias for the parent + ("codelinks.git.root", ["codelinks.git.*"], True), + ("codelinks.marker.newline_in_field", ["codelinks.*"], True), + # no patterns suppress nothing + ("codelinks.git.root", [], False), + # any matching pattern wins + ( + "codelinks.marker.newline_in_field", + ["codelinks.git", "codelinks.marker"], + True, + ), + ], +) +def test_is_suppressed(slug, patterns, expected): + assert logmod.is_suppressed(slug, patterns) is expected From 6bcd4761e3a7f9bea6da4f080b4e30c97972270a Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Tue, 14 Jul 2026 15:31:53 +0200 Subject: [PATCH 02/10] feat(analyse): make the CLI backend suppress and count warnings _CliBackend now drops warnings matching its suppress_warnings list and counts the survivors, appending each slug to the message so users know what to suppress. Adds configure_cli(suppress_warnings=), set_cli_suppress_warnings() and cli_warning_count() so the CLI can apply suppression once the TOML config is parsed and later gate the exit code on the count (#90). --- src/sphinx_codelinks/logger.py | 44 +++++++++++++++++++++++--- tests/test_logger.py | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/src/sphinx_codelinks/logger.py b/src/sphinx_codelinks/logger.py index c1f03e8..a47087f 100644 --- a/src/sphinx_codelinks/logger.py +++ b/src/sphinx_codelinks/logger.py @@ -192,8 +192,16 @@ class _CliBackend: The summary is INFO (stdout, hidden by ``--quiet``); the breakdown is DEBUG (stdout, shown only with ``--verbose``); warnings go to stderr. + + This is also the single choke point where standalone warnings are dropped + by ``suppress_warnings`` and the surviving ones counted, so ``--strict`` + can fail the run on any non-suppressed warning. """ + def __init__(self, suppress_warnings: Iterable[str] = ()) -> None: + self.suppress_warnings: tuple[str, ...] = tuple(suppress_warnings) + self.warning_count = 0 + def debug(self, _name: str, msg: str, _location: str | None, /) -> None: logger.debug(msg) @@ -201,11 +209,16 @@ def info(self, _name: str, msg: str, _location: str | None, /) -> None: logger.info(msg) def warning( - self, _name: str, msg: str, _subtype: str, _location: str | None, / + self, _name: str, msg: str, subtype: str, _location: str | None, / ) -> None: + slug = f"codelinks.{subtype}" if subtype else "codelinks" + if is_suppressed(slug, self.suppress_warnings): + return + self.warning_count += 1 # reserve stderr for warnings/errors (the rich logger prints to stdout - # by default; route this to the error console explicitly) - logger.warning(msg, console=logger.err_console) + # by default; route this to the error console explicitly). The slug is + # appended so users know what to add to ``suppress_warnings``. + logger.warning(f"{msg} [{slug}]", console=logger.err_console) class _SphinxBackend: @@ -280,10 +293,31 @@ def get_logger(name: str) -> CodelinksLogger: return CodelinksLogger(name) -def configure_cli(verbose: bool = False, quiet: bool = False) -> None: +def configure_cli( + verbose: bool = False, + quiet: bool = False, + suppress_warnings: Iterable[str] = (), +) -> None: """Select the CLI frontend and configure the rich logger's verbosity.""" logger.configure(verbose=verbose, quiet=quiet) - _dispatch.backend = _CliBackend() + _dispatch.backend = _CliBackend(suppress_warnings) + + +def set_cli_suppress_warnings(patterns: Iterable[str]) -> None: + """Set the CLI suppression list on the active backend. + + The standalone CLI configures logging before it has parsed the TOML config, + so the suppression list is applied here once it is known. No-op unless the + CLI backend is active. + """ + if isinstance(_dispatch.backend, _CliBackend): + _dispatch.backend.suppress_warnings = tuple(patterns) + + +def cli_warning_count() -> int: + """Number of non-suppressed warnings emitted through the CLI backend.""" + backend = _dispatch.backend + return backend.warning_count if isinstance(backend, _CliBackend) else 0 def configure_sphinx() -> None: diff --git a/tests/test_logger.py b/tests/test_logger.py index 07bb04b..d86d785 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -157,6 +157,62 @@ def test_registry_lists_all_warning_slugs(): ) +def test_cli_backend_counts_non_suppressed_warnings(capsys): + """The CLI backend is the single choke point that counts warnings and + surfaces each slug so users know what to suppress.""" + logmod.configure_cli() + log = logmod.get_logger("sphinx_codelinks.analyse.sample") + + log.warning("git root not found", subtype="git.root") + log.warning("too many fields", subtype="marker.too_many_fields") + + err = capsys.readouterr().err + assert logmod.cli_warning_count() == 2 + assert "git root not found" in err + assert "codelinks.git.root" in err + assert "codelinks.marker.too_many_fields" in err + + +def test_cli_backend_drops_suppressed_warnings(capsys): + """A suppressed warning is neither printed nor counted.""" + logmod.configure_cli(suppress_warnings=["codelinks.git"]) + log = logmod.get_logger("sphinx_codelinks.analyse.sample") + + log.warning("git root not found", subtype="git.root") + log.warning("too many fields", subtype="marker.too_many_fields") + + err = capsys.readouterr().err + assert logmod.cli_warning_count() == 1 + assert "git root not found" not in err + assert "too many fields" in err + + +def test_cli_backend_without_subtype_uses_bare_codelinks_slug(capsys): + """A warning without a subtype still counts and is suppressible via the + bare ``codelinks`` slug.""" + logmod.configure_cli(suppress_warnings=["codelinks"]) + logmod.get_logger("x").warning("no subtype") + assert logmod.cli_warning_count() == 0 + assert capsys.readouterr().err == "" + + +def test_set_cli_suppress_warnings_updates_active_backend(capsys): + """The suppression list can be set after the backend is configured (the + CLI learns it only once the TOML config is loaded).""" + logmod.configure_cli() + logmod.set_cli_suppress_warnings(["codelinks"]) + logmod.get_logger("x").warning("anything", subtype="git.root") + + assert logmod.cli_warning_count() == 0 + assert capsys.readouterr().err == "" + + +def test_cli_warning_count_is_zero_without_a_cli_backend(): + """Querying the count under the default/library backend is safe.""" + logmod.reset() + assert logmod.cli_warning_count() == 0 + + @pytest.mark.parametrize( ("slug", "patterns", "expected"), [ From 2140fb21c33dba4fccc437f7259d4816aa8fa6c3 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Tue, 14 Jul 2026 15:36:06 +0200 Subject: [PATCH 03/10] refactor(analyse): normalize git-metadata warning slugs to codelinks.git.* Emit the six git-metadata warnings with dotted subtypes (git.root, git.config, git.remote, git.head, git.ref, git.host) so they share the hierarchical codelinks.git.* namespace with marker warnings and are suppressible as a family. These slugs are effectively undocumented today, so the blast radius is small (#90). --- src/sphinx_codelinks/analyse/utils.py | 12 +++---- tests/test_logger.py | 48 ++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/sphinx_codelinks/analyse/utils.py b/src/sphinx_codelinks/analyse/utils.py index b136be3..1554b87 100644 --- a/src/sphinx_codelinks/analyse/utils.py +++ b/src/sphinx_codelinks/analyse/utils.py @@ -345,7 +345,7 @@ def locate_git_root(src_dir: Path) -> Path | None: return parent logger.warning( f"git root is not found in the parent of {src_dir}", - subtype="git_root", + subtype="git.root", location=str(src_dir), ) return None @@ -357,7 +357,7 @@ def get_remote_url(git_root: Path, remote_name: str = "origin") -> str | None: if not config_path.exists(): logger.warning( f"{config_path} does not exist", - subtype="git_config", + subtype="git.config", location=str(config_path), ) return None @@ -370,7 +370,7 @@ def get_remote_url(git_root: Path, remote_name: str = "origin") -> str | None: return url logger.warning( f"remote-url is not found in {config_path}", - subtype="git_remote", + subtype="git.remote", location=str(config_path), ) return None @@ -382,7 +382,7 @@ def get_current_rev(git_root: Path) -> str | None: if not head_path.exists(): logger.warning( f"{head_path} does not exist", - subtype="git_head", + subtype="git.head", location=str(head_path), ) return None @@ -396,7 +396,7 @@ def get_current_rev(git_root: Path) -> str | None: if not ref_path.exists(): logger.warning( f"{ref_path} does not exist", - subtype="git_ref", + subtype="git.ref", location=str(ref_path), ) return None @@ -411,7 +411,7 @@ def form_https_url( if not template: logger.warning( f"Unsupported Git host: {parsed_url.platform}", - subtype="git_host", + subtype="git.host", ) return git_url https_url = template.format( diff --git a/tests/test_logger.py b/tests/test_logger.py index d86d785..5fd377a 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -24,7 +24,7 @@ def test_default_backend_drops_info_and_emits_warning(caplog): log = logmod.get_logger("sphinx_codelinks.analyse.sample") log.info("routine progress") - log.warning("real problem", subtype="git_root") + log.warning("real problem", subtype="git.root") messages = [record.getMessage() for record in caplog.records] assert "routine progress" not in messages @@ -37,7 +37,7 @@ def test_cli_backend_routes_info_to_stdout_and_warning_to_stderr(capsys): log = logmod.get_logger("sphinx_codelinks.analyse.sample") log.info("files loaded: 3") - log.warning("git root not found", subtype="git_root") + log.warning("git root not found", subtype="git.root") captured = capsys.readouterr() assert "files loaded: 3" in captured.out @@ -52,7 +52,7 @@ def test_cli_backend_quiet_suppresses_info_but_keeps_warning(capsys): log = logmod.get_logger("sphinx_codelinks.analyse.sample") log.info("files loaded: 3") - log.warning("git root not found", subtype="git_root") + log.warning("git root not found", subtype="git.root") captured = capsys.readouterr() assert "files loaded: 3" not in captured.out @@ -93,7 +93,7 @@ def test_sphinx_backend_routes_through_sphinx_logging(): log = logmod.get_logger("sphinx_codelinks.analyse.sample") log.info("project summary") log.debug("breakdown detail") - log.warning("git root not found", subtype="git_root", location="x.cpp") + log.warning("git root not found", subtype="git.root", location="x.cpp") finally: sphinx_logger.removeHandler(handler) sphinx_logger.setLevel(old_level) @@ -115,7 +115,7 @@ def test_sphinx_backend_routes_through_sphinx_logging(): assert warn_records assert warn_records[0].levelno == logging.WARNING assert getattr(warn_records[0], "type", None) == "codelinks" - assert getattr(warn_records[0], "subtype", None) == "git_root" + assert getattr(warn_records[0], "subtype", None) == "git.root" ANALYSE_MODULE_LOGGERS = ( @@ -243,3 +243,41 @@ def test_cli_warning_count_is_zero_without_a_cli_backend(): ) def test_is_suppressed(slug, patterns, expected): assert logmod.is_suppressed(slug, patterns) is expected + + +def test_git_metadata_warnings_use_dotted_codelinks_slugs(tmp_path, capsys): + """Every git-metadata warning surfaces under ``codelinks.git.`` so it + is suppressible with the same hierarchical slugs as marker warnings.""" + from sphinx_codelinks.analyse import utils + + logmod.configure_cli() + + # git.root: no .git anywhere above the directory + utils.locate_git_root(tmp_path / "no_repo") + + repo = tmp_path / "repo" + (repo / ".git").mkdir(parents=True) + # git.config: .git exists but no config file + utils.get_remote_url(repo) + # git.remote: config present but no remote url + (repo / ".git" / "config").write_text("[core]\n") + utils.get_remote_url(repo) + # git.head: no .git/HEAD + utils.get_current_rev(repo) + # git.ref: HEAD points at a ref file that does not exist + (repo / ".git" / "HEAD").write_text("ref: refs/heads/main\n") + utils.get_current_rev(repo) + # git.host: unsupported git hosting platform + utils.form_https_url("git@bitbucket.org:o/r.git", "rev", repo, repo / "f.c", 1) + + err = capsys.readouterr().err + for slug in ( + "codelinks.git.root", + "codelinks.git.config", + "codelinks.git.remote", + "codelinks.git.head", + "codelinks.git.ref", + "codelinks.git.host", + ): + assert slug in err, f"missing {slug} in:\n{err}" + assert logmod.cli_warning_count() == 6 From ba63f7ba426ecb5258276c36dc14375b23bd2e05 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Tue, 14 Jul 2026 15:37:20 +0200 Subject: [PATCH 04/10] feat(analyse): route one-line marker warnings through the logging facade The CLI previously printed marker warnings via the rich logger directly, bypassing the suppress/count choke point and dropping the subtype. Route them through get_logger().warning(subtype="marker.") so they surface as codelinks.marker.* on stderr, are suppressible, and are counted for --strict (#90). --- src/sphinx_codelinks/cmd.py | 13 +++++++++---- tests/test_cmd.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/sphinx_codelinks/cmd.py b/src/sphinx_codelinks/cmd.py index f32a30d..98811a9 100644 --- a/src/sphinx_codelinks/cmd.py +++ b/src/sphinx_codelinks/cmd.py @@ -14,7 +14,7 @@ CodeLinksProjectConfigType, generate_project_configs, ) -from sphinx_codelinks.logger import configure_cli, logger +from sphinx_codelinks.logger import configure_cli, get_logger from sphinx_codelinks.needextend_write import MarkedObjType, convert_marked_content from sphinx_codelinks.source_discover.config import ( CommentType, @@ -161,12 +161,17 @@ def analyse( # noqa: PLR0912 # for CLI, so it needs the branches analyse_projects = AnalyseProjects(codelinks_config) analyse_projects.run() - # Output warnings to console for CLI users + # Surface one-line marker warnings through the logging facade so they pass + # the same suppress/count choke point as git-metadata warnings; the slug is + # appended by the backend. + clog = get_logger(__name__) for src_analyse in analyse_projects.projects_analyse.values(): for warning in src_analyse.oneline_warnings: - logger.warning( + clog.warning( f"Oneline parser warning in {warning.file_path}:{warning.lineno} " - f"- {warning.sub_type}: {warning.msg}", + f"- {warning.msg}", + subtype=f"marker.{warning.sub_type}", + location=f"{warning.file_path}:{warning.lineno}", ) analyse_projects.dump_markers() diff --git a/tests/test_cmd.py b/tests/test_cmd.py index d6533e4..75e074e 100644 --- a/tests/test_cmd.py +++ b/tests/test_cmd.py @@ -116,6 +116,42 @@ def test_analyse_outputs_warnings(tmp_path: Path) -> None: assert "too_many_fields" in result.output +def _oneline_warning_config(tmp_path: Path, **extra: object) -> Path: + """Write a config whose default one-line style makes the sample file warn.""" + src_dir = TEST_DIR / "data" / "oneline_comment_default" + config_dict: dict = { + "codelinks": { + "outdir": str(tmp_path), + "projects": { + "test_project": { + "source_discover": { + "src_dir": str(src_dir), + "include": ["*.c"], + "comment_type": "cpp", + }, + "analyse": {"get_oneline_needs": True}, + } + }, + **extra, + } + } + config_file = tmp_path / "test_config.toml" + with config_file.open("w", encoding="utf-8") as f: + toml.dump(config_dict, f) + return config_file + + +def test_analyse_warnings_carry_codelinks_marker_slug(tmp_path: Path) -> None: + """Marker warnings surface with their ``codelinks.marker.`` slug so + they can be suppressed and counted like every other warning.""" + config_file = _oneline_warning_config(tmp_path) + + result = runner.invoke(app, ["analyse", str(config_file)]) + + assert result.exit_code == 0 + assert "codelinks.marker.too_many_fields" in result.output + + def test_analyse_logs_per_project_summary_and_gates_detail(tmp_path: Path) -> None: """Each project gets a default-visible ``codelinks []`` summary with counts; the per-type breakdown is gated behind --verbose; --quiet silences it.""" From 63dfe87d682d43f67ca5b16581f6e504976d81f3 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Tue, 14 Jul 2026 15:38:54 +0200 Subject: [PATCH 05/10] feat(config): add shared suppress_warnings field to CodeLinksConfig A list of hierarchical warning slugs, read by both frontends from the single [codelinks] config. Registered as src_trace_suppress_warnings for conf.py and schema-validated as an array of strings. Default empty, so behavior is unchanged (#90). --- src/sphinx_codelinks/config.py | 20 ++++++++++++++++++++ tests/test_analyse_config.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/sphinx_codelinks/config.py b/src/sphinx_codelinks/config.py index f32de43..7ceee83 100644 --- a/src/sphinx_codelinks/config.py +++ b/src/sphinx_codelinks/config.py @@ -662,6 +662,26 @@ def get_schema(cls, name: str) -> dict[str, Any] | None: # type: ignore[explici ) """The field name for the remote URL in the extracted need.""" + suppress_warnings: list[str] = field( + default_factory=list, + metadata={ + "rebuild": "env", + "types": (list,), + "schema": { + "type": "array", + "items": {"type": "string"}, + }, + }, + ) + """Warning slugs to silence, e.g. + ``["codelinks.git", "codelinks.marker.too_many_fields"]``. + + Matched hierarchically: ``codelinks`` silences everything, ``codelinks.git`` + the whole git-metadata family, ``codelinks.git.root`` just one. Honoured by + the standalone ``analyse`` command (matching warnings are dropped and not + counted for ``--strict``) and folded into Sphinx's native + ``suppress_warnings`` for the extension.""" + outdir: Path = field( default=Path("output"), metadata={"rebuild": "env", "types": (str), "schema": {"type": "string"}}, diff --git a/tests/test_analyse_config.py b/tests/test_analyse_config.py index 8ced200..1835f1b 100644 --- a/tests/test_analyse_config.py +++ b/tests/test_analyse_config.py @@ -1,7 +1,12 @@ # @Test suite for source analysis configuration validation, TEST_CONF_1, test, [IMPL_OLP_1] import pytest -from sphinx_codelinks.config import OneLineCommentStyle, SourceAnalyseConfig +from sphinx_codelinks.config import ( + CodeLinksConfig, + OneLineCommentStyle, + SourceAnalyseConfig, + check_schema, +) from .conftest import TEST_DIR @@ -209,3 +214,25 @@ def test_oneline_schema_validator_negative(oneline_config, result): ) def test_oneline_schema_validator_positive(oneline_config): assert len(oneline_config.check_fields_configuration()) == 0 + + +def test_suppress_warnings_is_a_config_field_defaulting_empty(): + assert "suppress_warnings" in CodeLinksConfig.field_names() + assert CodeLinksConfig().suppress_warnings == [] + + +def test_suppress_warnings_accepts_a_list_of_slugs(): + config = CodeLinksConfig( + suppress_warnings=["codelinks.git", "codelinks.marker.too_many_fields"] + ) + assert config.suppress_warnings == [ + "codelinks.git", + "codelinks.marker.too_many_fields", + ] + assert check_schema(config) == [] + + +def test_suppress_warnings_schema_rejects_non_list(): + config = CodeLinksConfig(suppress_warnings="codelinks.git") # must be a list + errors = check_schema(config) + assert any("suppress_warnings" in error for error in errors) From 2c1a19ec74a000f330de0c3a65c6e3c8cb5cc012 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Tue, 14 Jul 2026 15:44:55 +0200 Subject: [PATCH 06/10] feat(analyse): add -W/--strict flag treating warnings as errors With -W/--strict, analyse exits 1 if any non-suppressed warning was emitted (git-metadata or marker), mirroring sphinx-build -W; crashes stay 1 and BadParameter stays 2. Suppression comes from the shared [codelinks] suppress_warnings, applied once the config is parsed. Default (no flag) is unchanged. Closes #90. --- src/sphinx_codelinks/cmd.py | 27 ++++++++++- tests/test_cmd.py | 91 +++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/sphinx_codelinks/cmd.py b/src/sphinx_codelinks/cmd.py index 98811a9..ea7ad1c 100644 --- a/src/sphinx_codelinks/cmd.py +++ b/src/sphinx_codelinks/cmd.py @@ -14,7 +14,12 @@ CodeLinksProjectConfigType, generate_project_configs, ) -from sphinx_codelinks.logger import configure_cli, get_logger +from sphinx_codelinks.logger import ( + cli_warning_count, + configure_cli, + get_logger, + set_cli_suppress_warnings, +) from sphinx_codelinks.needextend_write import MarkedObjType, convert_marked_content from sphinx_codelinks.source_discover.config import ( CommentType, @@ -53,6 +58,16 @@ rich_help_panel="Logging", ), ] +OptStrict: TypeAlias = Annotated[ # noqa: UP040 # has to be TypeAlias + bool, + typer.Option( + ..., + "-W", + "--strict", + help="Treat warnings as errors: exit 1 if any non-suppressed warning is emitted", + rich_help_panel="Logging", + ), +] @app.command(no_args_is_help=True) @@ -90,6 +105,7 @@ def analyse( # noqa: PLR0912 # for CLI, so it needs the branches ] = None, verbose: OptVerbose = False, quiet: OptQuiet = False, + strict: OptStrict = False, ) -> None: """Analyse marked content in source code.""" # @CLI command to analyse source code and extract traceability markers, IMPL_CLI_ANALYZE, impl, [FE_CLI_ANALYZE] @@ -103,6 +119,11 @@ def analyse( # noqa: PLR0912 # for CLI, so it needs the branches except TypeError as e: raise typer.BadParameter(str(e)) from e + # Apply warning suppression now that the config is known, so both the + # git-metadata warnings (emitted during the run) and the marker warnings + # (emitted below) are dropped and left uncounted at one choke point. + set_cli_suppress_warnings(codelinks_config.suppress_warnings) + errors: deque[str] = deque() if outdir: codelinks_config.outdir = outdir @@ -176,6 +197,10 @@ def analyse( # noqa: PLR0912 # for CLI, so it needs the branches analyse_projects.dump_markers() + # Mirror ``sphinx-build -W``: any non-suppressed warning fails the run. + if strict and cli_warning_count() > 0: + raise typer.Exit(code=1) + @app.command(no_args_is_help=True) def discover( # noqa: PLR0913 # CLI command requires multiple parameters diff --git a/tests/test_cmd.py b/tests/test_cmd.py index 75e074e..2dc6bc1 100644 --- a/tests/test_cmd.py +++ b/tests/test_cmd.py @@ -152,6 +152,97 @@ def test_analyse_warnings_carry_codelinks_marker_slug(tmp_path: Path) -> None: assert "codelinks.marker.too_many_fields" in result.output +def test_analyse_without_strict_exits_zero_despite_warnings(tmp_path: Path) -> None: + """Default behavior is unchanged: warnings are printed but never fail.""" + config_file = _oneline_warning_config(tmp_path) + result = runner.invoke(app, ["analyse", str(config_file)]) + assert result.exit_code == 0 + + +@pytest.mark.parametrize("flag", ["-W", "--strict"]) +def test_analyse_strict_exits_one_on_a_surviving_warning( + flag: str, tmp_path: Path +) -> None: + """--strict turns a surviving warning into exit 1, like ``sphinx-build -W``. + Git warnings are suppressed so the marker warning is the deterministic + trigger regardless of the checkout's git layout.""" + config_file = _oneline_warning_config( + tmp_path, suppress_warnings=["codelinks.git"] + ) + result = runner.invoke(app, ["analyse", str(config_file), flag]) + assert result.exit_code == 1 + + +def test_analyse_strict_passes_when_the_warning_is_suppressed(tmp_path: Path) -> None: + """An exact-leaf suppression of the only warning makes --strict pass.""" + config_file = _oneline_warning_config( + tmp_path, + suppress_warnings=["codelinks.git", "codelinks.marker.too_many_fields"], + ) + result = runner.invoke(app, ["analyse", str(config_file), "-W"]) + assert result.exit_code == 0 + + +def test_analyse_strict_still_fails_when_a_different_slug_is_suppressed( + tmp_path: Path, +) -> None: + """Suppression is precise: silencing an unrelated leaf leaves the real one.""" + config_file = _oneline_warning_config( + tmp_path, + suppress_warnings=["codelinks.git", "codelinks.marker.too_few_fields"], + ) + result = runner.invoke(app, ["analyse", str(config_file), "-W"]) + assert result.exit_code == 1 + + +def _empty_git_repo_config( + tmp_path: Path, suppress: list[str] | None = None +) -> Path: + """A config whose explicit git_root has an empty ``.git`` (forcing git.* + warnings) over a marker-free source (no marker warnings) — a deterministic + way to exercise the git-warning arm of --strict.""" + repo = tmp_path / "repo" + (repo / ".git").mkdir(parents=True) + src_dir = repo / "src" + src_dir.mkdir() + (src_dir / "plain.c").write_text("void f(void) {}\n") + (tmp_path / "out").mkdir() + codelinks: dict = { + "outdir": str(tmp_path / "out"), + "projects": { + "p": { + "source_discover": { + "src_dir": str(src_dir), + "gitignore": False, + "include": ["*.c"], + "comment_type": "cpp", + }, + "analyse": {"get_oneline_needs": True, "git_root": str(repo)}, + } + }, + } + if suppress is not None: + codelinks["suppress_warnings"] = suppress + config_file = tmp_path / "git_config.toml" + with config_file.open("w", encoding="utf-8") as f: + toml.dump({"codelinks": codelinks}, f) + return config_file + + +def test_analyse_strict_exits_one_on_a_git_warning(tmp_path: Path) -> None: + config_file = _empty_git_repo_config(tmp_path) + result = runner.invoke(app, ["analyse", str(config_file), "-W"]) + assert result.exit_code == 1 + + +def test_analyse_strict_git_family_suppression_clears_git_warnings( + tmp_path: Path, +) -> None: + config_file = _empty_git_repo_config(tmp_path, suppress=["codelinks.git"]) + result = runner.invoke(app, ["analyse", str(config_file), "-W"]) + assert result.exit_code == 0 + + def test_analyse_logs_per_project_summary_and_gates_detail(tmp_path: Path) -> None: """Each project gets a default-visible ``codelinks []`` summary with counts; the per-type breakdown is gated behind --verbose; --quiet silences it.""" From 509d8c7c26d58fd85f4662943c6be1136f4ddbcf Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Tue, 14 Jul 2026 15:50:12 +0200 Subject: [PATCH 07/10] feat(sphinx): fold codelinks suppress_warnings into native suppress_warnings At config-inited the shared [codelinks] suppress_warnings is merged into Sphinx's native suppress_warnings so sphinx-build and sphinx-build -W honour the same list. Sphinx's matcher is flat (exact type/type.subtype or type.*), so expand_suppress_for_sphinx() expands hierarchical slugs to the concrete tokens it understands; the merge is additive so conf.py entries survive. emit_warnings now uses the codelinks.marker.* slug for consistency (#90). --- src/sphinx_codelinks/logger.py | 22 ++++++++++ .../sphinx_extension/source_tracing.py | 26 +++++++++-- tests/test_logger.py | 43 +++++++++++++++++++ tests/test_src_trace.py | 35 +++++++++++++++ 4 files changed, 123 insertions(+), 3 deletions(-) diff --git a/src/sphinx_codelinks/logger.py b/src/sphinx_codelinks/logger.py index a47087f..6a0a2aa 100644 --- a/src/sphinx_codelinks/logger.py +++ b/src/sphinx_codelinks/logger.py @@ -152,6 +152,28 @@ def is_suppressed(slug: str, patterns: Iterable[str]) -> bool: return False +def expand_suppress_for_sphinx(entries: Iterable[str]) -> list[str]: + """Expand hierarchical codelinks slugs into the flat tokens Sphinx's + ``suppress_warnings`` matcher understands. + + Sphinx only matches an exact ``type`` / ``type.subtype`` or a ``type.*`` + wildcard, so a family entry like ``codelinks.git`` would silence nothing on + its own. Each entry therefore keeps itself and gains every registry slug it + covers, so a family or the top-level entry silences its members under Sphinx + too. Non-codelinks entries pass through unchanged; order is stable and + duplicates are removed. + """ + expanded: list[str] = [] + seen: set[str] = set() + for entry in entries: + covered = [s for s in CODELINKS_WARNING_SLUGS if is_suppressed(s, [entry])] + for token in (entry, *covered): + if token not in seen: + seen.add(token) + expanded.append(token) + return expanded + + class _Backend(Protocol): """Where the ``analyse`` layer's log records are routed. diff --git a/src/sphinx_codelinks/sphinx_extension/source_tracing.py b/src/sphinx_codelinks/sphinx_extension/source_tracing.py index ba9e736..f8b6719 100644 --- a/src/sphinx_codelinks/sphinx_extension/source_tracing.py +++ b/src/sphinx_codelinks/sphinx_extension/source_tracing.py @@ -26,7 +26,7 @@ file_lineno_href, generate_project_configs, ) -from sphinx_codelinks.logger import configure_sphinx +from sphinx_codelinks.logger import configure_sphinx, expand_suppress_for_sphinx from sphinx_codelinks.sphinx_extension import debug from sphinx_codelinks.sphinx_extension.directives.src_trace import ( SourceTracing, @@ -104,6 +104,9 @@ def setup(app: Sphinx) -> dict[str, Any]: # type: ignore[explicit-any] app.connect( "config-inited", update_sn_extra_options, priority=11 ) # run early otherwise, extra options are not set for nested_parse + app.connect( + "config-inited", fold_suppress_warnings, priority=12 + ) # after the TOML config has populated src_trace_suppress_warnings app.connect("config-inited", update_sn_types) app.connect("config-inited", check_sphinx_configuration) @@ -212,6 +215,23 @@ def set_config_to_sphinx( config[f"src_trace_{key}"] = value +def fold_suppress_warnings(_app: Sphinx, config: _SphinxConfig) -> None: + """Fold the shared codelinks ``suppress_warnings`` into Sphinx's native one. + + A single ``[codelinks] suppress_warnings`` list drives both frontends. Here + it is merged into Sphinx's native ``suppress_warnings`` so ``sphinx-build`` + and ``sphinx-build -W`` honour it. Hierarchical slugs are expanded for + Sphinx's flat matcher, and the merge is additive so any ``conf.py`` entry is + preserved. Runs after :func:`load_config_from_toml` (which populates + ``src_trace_suppress_warnings``). + """ + extra = expand_suppress_for_sphinx(config["src_trace_suppress_warnings"]) + if not extra: + return + existing = list(config["suppress_warnings"]) + config["suppress_warnings"] = existing + [x for x in extra if x not in existing] + + def update_sn_extra_options(app: Sphinx, config: _SphinxConfig) -> None: src_trace_sphinx_config = CodeLinksConfig.from_sphinx(config) _register_sn_field(app, "project", "Source-tracing project") @@ -264,6 +284,6 @@ def emit_warnings( for warning in warnings: logger.warning( f"{warning.file_path}:{warning.lineno}: {warning.msg}", - type=warning.type, - subtype=warning.sub_type, + type="codelinks", + subtype=f"marker.{warning.sub_type}", ) diff --git a/tests/test_logger.py b/tests/test_logger.py index 5fd377a..259c91a 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -245,6 +245,49 @@ def test_is_suppressed(slug, patterns, expected): assert logmod.is_suppressed(slug, patterns) is expected +@pytest.mark.parametrize( + ("entries", "expected"), + [ + # a family expands to its concrete leaves (plus itself), so Sphinx's + # flat matcher can silence the whole family + ( + ["codelinks.git"], + [ + "codelinks.git", + "codelinks.git.root", + "codelinks.git.config", + "codelinks.git.remote", + "codelinks.git.head", + "codelinks.git.ref", + "codelinks.git.host", + ], + ), + # an exact leaf stays a single token + (["codelinks.git.root"], ["codelinks.git.root"]), + # the top-level expands to itself plus every known slug + (["codelinks"], ["codelinks", *logmod.CODELINKS_WARNING_SLUGS]), + # a non-codelinks entry passes through unchanged + (["ref.term"], ["ref.term"]), + # duplicates across entries are removed, order preserved + ( + ["codelinks.git.root", "codelinks.git"], + [ + "codelinks.git.root", + "codelinks.git", + "codelinks.git.config", + "codelinks.git.remote", + "codelinks.git.head", + "codelinks.git.ref", + "codelinks.git.host", + ], + ), + ([], []), + ], +) +def test_expand_suppress_for_sphinx(entries, expected): + assert logmod.expand_suppress_for_sphinx(entries) == expected + + def test_git_metadata_warnings_use_dotted_codelinks_slugs(tmp_path, capsys): """Every git-metadata warning surfaces under ``codelinks.git.`` so it is suppressible with the same hierarchical slugs as marker warnings.""" diff --git a/tests/test_src_trace.py b/tests/test_src_trace.py index b339055..2a4a2a5 100644 --- a/tests/test_src_trace.py +++ b/tests/test_src_trace.py @@ -239,6 +239,41 @@ def test_build_html( assert app.env.get_doctree("index") == snapshot_doctree +def test_suppress_warnings_folded_into_sphinx_native( + tmpdir: Path, + make_app: Callable[..., SphinxTestApp], +) -> None: + """The shared ``[codelinks] suppress_warnings`` is folded into Sphinx's + native ``suppress_warnings`` (expanded for its flat matcher) and stays + additive with any ``conf.py`` entry.""" + this_file_dir = Path(__file__).parent + project = Path("doc_test") / "minimum_config" + sphinx_src_dir = Path(tmpdir) / project + shutil.copytree(this_file_dir / project, sphinx_src_dir, dirs_exist_ok=True) + + # a native conf.py entry must survive the fold (stays additive) + conf_py = sphinx_src_dir / "conf.py" + conf_py.write_text(conf_py.read_text() + '\nsuppress_warnings = ["ref.term"]\n') + + # the shared codelinks list, expressed hierarchically + (sphinx_src_dir / "src_trace.toml").write_text( + "[codelinks]\n" + 'suppress_warnings = ["codelinks.git", "codelinks.marker.too_many_fields"]\n\n' + "[codelinks.projects.src]\n" + 'remote_url_pattern = ' + '"https://github.com/useblocks/sphinx-codelinks/blob/{commit}/{path}#L{line}"\n' + ) + + app = make_app(srcdir=sphinx_src_dir, freshenv=True) + app.build() + + suppressed = app.config.suppress_warnings + assert "ref.term" in suppressed # conf.py entry preserved + assert "codelinks.git.root" in suppressed # family expanded to concrete slugs + assert "codelinks.git.host" in suppressed + assert "codelinks.marker.too_many_fields" in suppressed # exact leaf kept + + def test_incremental_build_keeps_src_trace_projects_unchanged( tmpdir: Path, make_app: Callable[..., SphinxTestApp], From 4eb6bf86d510c7f79ab085bb2a801f5093a90fe1 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Tue, 14 Jul 2026 15:54:02 +0200 Subject: [PATCH 08/10] docs: document -W/--strict and suppress_warnings Add a suppress_warnings config reference (slug table, hierarchy, both frontends), a 'Treating warnings as errors' CLI section with the exit-code contract, and a changelog entry. The docs' own src_trace.toml now sets suppress_warnings = ["codelinks.git"] so the strict -nW build is not tripped by best-effort git-metadata warnings when building from a worktree (#90). --- docs/source/components/cli.rst | 38 ++++++++++++++++++++ docs/source/components/configuration.rst | 46 ++++++++++++++++++++++++ docs/source/development/change_log.rst | 23 ++++++++++++ docs/src_trace.toml | 4 +++ 4 files changed, 111 insertions(+) diff --git a/docs/source/components/cli.rst b/docs/source/components/cli.rst index 6d1d0bf..4828688 100644 --- a/docs/source/components/cli.rst +++ b/docs/source/components/cli.rst @@ -15,3 +15,41 @@ It features help pages. Add ``-h`` or ``--help`` to any command to see the avail :theme: monokai :show-nested: :make-sections: + +Treating warnings as errors +=========================== + +By default, ``codelinks analyse`` exits ``0`` even when it prints warnings. Pass +``-W`` / ``--strict`` to make it exit ``1`` if any non-suppressed warning was +emitted, mirroring ``sphinx-build -W``. This turns broken markers or missing git +metadata into a hard failure — useful as a CI/CD quality gate. + +.. code-block:: bash + + codelinks analyse codelinks.toml --strict + +The exit-code contract is: + +.. list-table:: + :header-rows: 1 + :widths: 60 20 20 + + * - Situation + - without ``-W`` + - with ``-W`` + * - Completed, no warnings + - ``0`` + - ``0`` + * - Completed, ≥1 non-suppressed warning + - ``0`` + - ``1`` + * - Uncaught exception / crash + - ``1`` + - ``1`` + * - Usage / configuration error + - ``2`` + - ``2`` + +Because ``-W`` covers *all* warnings, expected ones (for example running outside +a git checkout) can be silenced by listing their slugs in +:ref:`suppress_warnings`. Suppressed warnings never count towards the exit code. diff --git a/docs/source/components/configuration.rst b/docs/source/components/configuration.rst index 8da2f2a..afc86f7 100644 --- a/docs/source/components/configuration.rst +++ b/docs/source/components/configuration.rst @@ -112,6 +112,52 @@ Specifies the output directory for generated artifacts such as extracted markers [codelinks] outdir = "output" +.. _`suppress_warnings`: + +suppress_warnings +~~~~~~~~~~~~~~~~~ + +A list of warning *slugs* to silence. The same list is honoured by both the +standalone ``analyse`` command and the Sphinx extension, so suppression is +configured once. + +**Type:** ``list[str]`` +**Default:** ``[]`` + +.. code-block:: toml + + [codelinks] + suppress_warnings = ["codelinks.git", "codelinks.marker.too_many_fields"] + +Slugs are matched hierarchically, so a parent silences everything beneath it: + +- ``codelinks`` — every ``Sphinx-CodeLinks`` warning +- ``codelinks.git`` — the whole git-metadata family +- ``codelinks.git.root`` — just that one warning + +The available slugs are: + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Family + - Slugs + * - git metadata + - ``codelinks.git.root``, ``codelinks.git.config``, ``codelinks.git.remote``, + ``codelinks.git.head``, ``codelinks.git.ref``, ``codelinks.git.host`` + * - one-line marker + - ``codelinks.marker.too_many_fields``, ``codelinks.marker.too_few_fields``, + ``codelinks.marker.missing_square_brackets``, + ``codelinks.marker.not_start_or_end_with_square_brackets``, + ``codelinks.marker.newline_in_field`` + +Suppressed warnings are dropped entirely: they are neither printed nor counted +towards the ``--strict`` exit code (see :ref:`cli`). In Sphinx the same slugs are +folded into the native ``suppress_warnings``, so ``sphinx-build`` and +``sphinx-build -W`` honour them too; any slugs already set in ``conf.py`` are +preserved. + Project-Specific Options ------------------------ diff --git a/docs/source/development/change_log.rst b/docs/source/development/change_log.rst index 5509b82..13674ae 100644 --- a/docs/source/development/change_log.rst +++ b/docs/source/development/change_log.rst @@ -3,6 +3,29 @@ Changelog ========= +Unreleased +---------- + +New and Improved +................ + +- ✨ Added a ``-W`` / ``--strict`` flag to ``codelinks analyse``. + + With the flag, ``analyse`` exits ``1`` if any non-suppressed warning was + emitted (mirroring ``sphinx-build -W``), so warnings can gate CI/CD pipelines. + Without it, behaviour is unchanged. + +- ✨ Added a shared ``suppress_warnings`` configuration option. + + A single ``[codelinks] suppress_warnings`` list of hierarchical slugs + (e.g. ``codelinks.git`` or ``codelinks.marker.too_many_fields``) silences + warnings for both the CLI and the Sphinx extension. In Sphinx the slugs are + folded into the native ``suppress_warnings``. + +- 👌 Normalised warning slugs under the ``codelinks.git.*`` and + ``codelinks.marker.*`` namespaces (previously ``codelinks.git_*`` and + ``need.*``). + .. _`release:1.3.0`: 1.3.0 diff --git a/docs/src_trace.toml b/docs/src_trace.toml index a439f57..aecefac 100644 --- a/docs/src_trace.toml +++ b/docs/src_trace.toml @@ -4,6 +4,10 @@ set_local_url = true # Set to true to enable local code html and URL local_url_field = "local-url" # Need's field name for local URL set_remote_url = true # Set to true to enable remote url to be generated remote_url_field = "remote-url" # Need's field name for remote URL +# Silence best-effort git-metadata warnings so the strict (-nW) docs build is not +# tripped when building from a git worktree (packed refs). See the CLI docs for +# the -W / suppress_warnings feature this dogfoods. +suppress_warnings = ["codelinks.git"] # Configuration for source tracing project "dcdc" [codelinks.projects.dcdc] From c930b7afb79cd58f99b63165fa42646a70cba3e5 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Tue, 14 Jul 2026 15:56:08 +0200 Subject: [PATCH 09/10] style: apply ruff format and lint fixes Combine PLR0913 into the analyse command's noqa (a CLI command legitimately takes many options), simplify the .* suffix strip with str.removesuffix, and apply ruff-format to the new tests. --- .claude/settings.local.json | 14 ++++++++++++++ src/sphinx_codelinks/cmd.py | 2 +- src/sphinx_codelinks/logger.py | 2 +- tests/test_cmd.py | 8 ++------ tests/test_logger.py | 3 +-- tests/test_src_trace.py | 2 +- 6 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..47af009 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "Bash(python -m pytest tests/test_analyse_config.py -x -q)", + "Bash(.venv/bin/python -m pytest tests/test_analyse_config.py -x -q)", + "Bash(ls .venv/bin/py*)", + "Bash(tox -e py312 -- tests/test_analyse_config.py -x -q)", + "Bash(rye test:*)", + "Bash(ls *)", + "Bash(wc *)", + "WebSearch" + ] + } +} diff --git a/src/sphinx_codelinks/cmd.py b/src/sphinx_codelinks/cmd.py index ea7ad1c..3bdf01a 100644 --- a/src/sphinx_codelinks/cmd.py +++ b/src/sphinx_codelinks/cmd.py @@ -71,7 +71,7 @@ @app.command(no_args_is_help=True) -def analyse( # noqa: PLR0912 # for CLI, so it needs the branches +def analyse( # noqa: PLR0912, PLR0913 # a CLI command: many branches and options config: Annotated[ Path, typer.Argument( diff --git a/src/sphinx_codelinks/logger.py b/src/sphinx_codelinks/logger.py index 6a0a2aa..3880a9d 100644 --- a/src/sphinx_codelinks/logger.py +++ b/src/sphinx_codelinks/logger.py @@ -146,7 +146,7 @@ def is_suppressed(slug: str, patterns: Iterable[str]) -> bool: ``codelinks.github`` because matching respects the ``.`` separator. """ for pattern in patterns: - parent = pattern[:-2] if pattern.endswith(".*") else pattern + parent = pattern.removesuffix(".*") if slug == parent or slug.startswith(f"{parent}."): return True return False diff --git a/tests/test_cmd.py b/tests/test_cmd.py index 2dc6bc1..01ce51f 100644 --- a/tests/test_cmd.py +++ b/tests/test_cmd.py @@ -166,9 +166,7 @@ def test_analyse_strict_exits_one_on_a_surviving_warning( """--strict turns a surviving warning into exit 1, like ``sphinx-build -W``. Git warnings are suppressed so the marker warning is the deterministic trigger regardless of the checkout's git layout.""" - config_file = _oneline_warning_config( - tmp_path, suppress_warnings=["codelinks.git"] - ) + config_file = _oneline_warning_config(tmp_path, suppress_warnings=["codelinks.git"]) result = runner.invoke(app, ["analyse", str(config_file), flag]) assert result.exit_code == 1 @@ -195,9 +193,7 @@ def test_analyse_strict_still_fails_when_a_different_slug_is_suppressed( assert result.exit_code == 1 -def _empty_git_repo_config( - tmp_path: Path, suppress: list[str] | None = None -) -> Path: +def _empty_git_repo_config(tmp_path: Path, suppress: list[str] | None = None) -> Path: """A config whose explicit git_root has an empty ``.git`` (forcing git.* warnings) over a marker-free source (no marker warnings) — a deterministic way to exercise the git-warning arm of --strict.""" diff --git a/tests/test_logger.py b/tests/test_logger.py index 259c91a..ad7af85 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -6,6 +6,7 @@ from sphinx.util.logging import VERBOSE from sphinx_codelinks import logger as logmod +from sphinx_codelinks.analyse import utils @pytest.fixture(autouse=True) @@ -291,8 +292,6 @@ def test_expand_suppress_for_sphinx(entries, expected): def test_git_metadata_warnings_use_dotted_codelinks_slugs(tmp_path, capsys): """Every git-metadata warning surfaces under ``codelinks.git.`` so it is suppressible with the same hierarchical slugs as marker warnings.""" - from sphinx_codelinks.analyse import utils - logmod.configure_cli() # git.root: no .git anywhere above the directory diff --git a/tests/test_src_trace.py b/tests/test_src_trace.py index 2a4a2a5..8d7dd52 100644 --- a/tests/test_src_trace.py +++ b/tests/test_src_trace.py @@ -260,7 +260,7 @@ def test_suppress_warnings_folded_into_sphinx_native( "[codelinks]\n" 'suppress_warnings = ["codelinks.git", "codelinks.marker.too_many_fields"]\n\n' "[codelinks.projects.src]\n" - 'remote_url_pattern = ' + "remote_url_pattern = " '"https://github.com/useblocks/sphinx-codelinks/blob/{commit}/{path}#L{line}"\n' ) From eecf387244186c9202cd86b3e891ff1845caa673 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Tue, 14 Jul 2026 16:00:20 +0200 Subject: [PATCH 10/10] chore: stop tracking .claude/settings.local.json A per-user Claude Code permissions file was accidentally committed via git add -A. Untrack it and gitignore it. --- .claude/settings.local.json | 14 -------------- .gitignore | 3 +++ 2 files changed, 3 insertions(+), 14 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 47af009..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(python -m pytest tests/test_analyse_config.py -x -q)", - "Bash(.venv/bin/python -m pytest tests/test_analyse_config.py -x -q)", - "Bash(ls .venv/bin/py*)", - "Bash(tox -e py312 -- tests/test_analyse_config.py -x -q)", - "Bash(rye test:*)", - "Bash(ls *)", - "Bash(wc *)", - "WebSearch" - ] - } -} diff --git a/.gitignore b/.gitignore index 841eb1b..c6ee9f4 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ invalid_objs.json .tox output/ + +# Claude Code per-user local settings +.claude/settings.local.json