From ca08f496deaf077fd66b779c42a9bcd87c7d808a Mon Sep 17 00:00:00 2001 From: Gabriel G Date: Fri, 3 Apr 2026 14:24:35 -0400 Subject: [PATCH 01/19] auto-detect format in ci --- .agents/skills/ruff-sync-usage/SKILL.md | 2 +- src/ruff_sync/cli.py | 51 +++++++++-- src/ruff_sync/constants.py | 6 +- src/ruff_sync/core.py | 16 ++-- tests/test_ci_validation.py | 111 +++++++++++++++++++++++- 5 files changed, 168 insertions(+), 18 deletions(-) diff --git a/.agents/skills/ruff-sync-usage/SKILL.md b/.agents/skills/ruff-sync-usage/SKILL.md index 882f4d0..db758da 100644 --- a/.agents/skills/ruff-sync-usage/SKILL.md +++ b/.agents/skills/ruff-sync-usage/SKILL.md @@ -117,7 +117,7 @@ ruff-sync git@github.com:my-org/standards.git # SSH (shallow clone) | Flag | Meaning | |------|---------| -| `--output-format` | `text` (default), `json`, `github` (PR annotations) | +| `--output-format` | `text` (default), `json`, `github`, `gitlab`, `sarif` (auto-detected in CI) | | `--semantic` | Ignore whitespace/comments in `check` | | `--pre-commit` | Sync `.pre-commit-config.yaml` hook version | | `--save` | Persist CLI args to `pyproject.toml` | diff --git a/src/ruff_sync/cli.py b/src/ruff_sync/cli.py index fc6c600..c7f9e09 100644 --- a/src/ruff_sync/cli.py +++ b/src/ruff_sync/cli.py @@ -131,6 +131,7 @@ class ResolvedArgs(NamedTuple): exclude: Iterable[str] | MissingType branch: str | MissingType path: str | MissingType + output_format: OutputFormat @lru_cache(maxsize=1) @@ -260,8 +261,8 @@ def _get_cli_parser() -> ArgumentParser: "--output-format", type=OutputFormat, choices=list(OutputFormat), - default=OutputFormat.TEXT, - help="Format for output. Default: text.", + default=None, + help="Format for output. Default: text (auto-detected in CI).", ) # Pull subcommand (the default action) @@ -398,6 +399,29 @@ def _resolve_path(args: Any, config: Mapping[str, Any]) -> str | MissingType: return MISSING +def _resolve_output_format(args: Any, config: Mapping[str, Any]) -> OutputFormat | MissingType: + """Resolve output format from CLI, config, or environment auto-detection.""" + if getattr(args, "output_format", None): + return cast("OutputFormat", args.output_format) + + if "output-format" in config: + fmt_str = config["output-format"] + try: + fmt = OutputFormat(fmt_str) + LOGGER.info(f"πŸ“Š Using output format from [tool.ruff-sync]: {fmt}") + return fmt + except ValueError: + LOGGER.warning(f"Unknown output format in config: {fmt_str}") + + # Auto-detection + provider = _detect_ci_provider() + if provider: + LOGGER.info(f"πŸ€– Auto-detected CI environment: {provider}") + return provider + + return MISSING + + def _resolve_to(args: Any, config: Mapping[str, Any], initial_to: pathlib.Path) -> pathlib.Path: """Resolve target path from CLI, config, or default.""" if args.source: @@ -422,7 +446,15 @@ def _resolve_args(args: Any, config: Mapping[str, Any], initial_to: pathlib.Path exclude = _resolve_exclude(args, config) branch = _resolve_branch(args, config) path = _resolve_path(args, config) - return ResolvedArgs(upstream, to, cast("Any", exclude), cast("Any", branch), cast("Any", path)) + output_format = _resolve_output_format(args, config) + return ResolvedArgs( + upstream, + to, + cast("Any", exclude), + cast("Any", branch), + cast("Any", path), + cast("Any", output_format), + ) def _detect_ci_provider() -> CIProvider | None: @@ -489,7 +521,7 @@ def main() -> int: initial_to = pathlib.Path(arg_to) if arg_to else pathlib.Path() config: Config = get_config(initial_to) - upstream, to_val, exclude, branch, path = _resolve_args(args, config, initial_to) + upstream, to_val, exclude, branch, path, output_format = _resolve_args(args, config, initial_to) pre_commit_arg = getattr(args, "pre_commit", None) if pre_commit_arg is not None: @@ -517,12 +549,17 @@ def main() -> int: init=getattr(args, "init", False), pre_commit=pre_commit_val, save=getattr(args, "save", None), - output_format=getattr(args, "output_format", OutputFormat.TEXT), + output_format=output_format, ) # Use the shared helper from constants so the MISSINGβ†’default logic for - # branch/path cannot diverge between cli.main and core._merge_multiple_upstreams. - res_branch, res_path, _exclude = resolve_defaults(branch, path, exclude) + # branch/path/output_format cannot diverge between cli.main and + # core._merge_multiple_upstreams. + res_branch, res_path, _exclude, res_output_format = resolve_defaults( + branch, path, exclude, output_format + ) + exec_args = exec_args._replace(output_format=res_output_format) + resolved_upstream = tuple( resolve_raw_url(u, branch=res_branch, path=res_path) for u in upstream ) diff --git a/src/ruff_sync/constants.py b/src/ruff_sync/constants.py index b05bbd4..28d3fde 100644 --- a/src/ruff_sync/constants.py +++ b/src/ruff_sync/constants.py @@ -65,7 +65,8 @@ def resolve_defaults( branch: str | MissingType, path: str | MissingType, exclude: Iterable[str] | MissingType, -) -> tuple[str, str | None, Iterable[str]]: + output_format: OutputFormat | MissingType = MISSING, +) -> tuple[str, str | None, Iterable[str], OutputFormat]: """Resolve MISSING sentinel values to their effective defaults. This is the single source of truth for MISSING β†’ default resolution across @@ -89,4 +90,5 @@ def resolve_defaults( # but explicit None is the canonical "root directory" sentinel. eff_path: str | None = raw_path or None eff_exclude = exclude if exclude is not MISSING else DEFAULT_EXCLUDE - return eff_branch, eff_path, eff_exclude + eff_output_format = output_format if output_format is not MISSING else OutputFormat.TEXT + return eff_branch, eff_path, eff_exclude, eff_output_format diff --git a/src/ruff_sync/core.py b/src/ruff_sync/core.py index 22804fe..2ed7e16 100644 --- a/src/ruff_sync/core.py +++ b/src/ruff_sync/core.py @@ -34,8 +34,7 @@ from typing_extensions import override from ruff_sync.constants import ( - DEFAULT_BRANCH, - MISSING, + OutputFormat, resolve_defaults, ) from ruff_sync.formatters import ResultFormatter, get_formatter @@ -141,6 +140,7 @@ class Config(TypedDict, total=False): diff: bool init: bool pre_commit_version_sync: bool + output_format: str def resolve_target_path( @@ -800,14 +800,14 @@ async def fetch_upstreams_concurrently( def _resolve_defaults( args: Arguments, -) -> tuple[str, str | None, Iterable[str]]: +) -> tuple[str, str | None, Iterable[str], OutputFormat]: """Resolve MISSING sentinel values in *args* to their effective defaults. Delegates to :func:`~ruff_sync.constants.resolve_defaults` so that the MISSING β†’ default logic is centralised in one place and shared with ``cli.main`` without coupling the two layers together. """ - return resolve_defaults(args.branch, args.path, args.exclude) + return resolve_defaults(args.branch, args.path, args.exclude, args.output_format) async def _merge_multiple_upstreams( @@ -821,7 +821,7 @@ async def _merge_multiple_upstreams( Downloads are performed concurrently via fetch_upstreams_concurrently, while merging remains sequential to preserve layering order. """ - branch, path, exclude = _resolve_defaults(args) + branch, path, exclude, _fmt = _resolve_defaults(args) fetch_results = await fetch_upstreams_concurrently( args.upstream, client, branch=branch, path=path @@ -1017,7 +1017,8 @@ async def check( ... ) >>> # asyncio.run(check(args)) """ - fmt = get_formatter(args.output_format) + branch, path, exclude, output_format = _resolve_defaults(args) + fmt = get_formatter(output_format) try: fmt.note("πŸ” Checking Ruff sync status...") @@ -1194,7 +1195,8 @@ async def pull( ... ) >>> # asyncio.run(pull(args)) """ - fmt = get_formatter(args.output_format) + branch, path, exclude, output_format = _resolve_defaults(args) + fmt = get_formatter(output_format) try: fmt.note("πŸ”„ Syncing Ruff...") _source_toml_path = resolve_target_path(args.to, args.upstream).resolve(strict=False) diff --git a/tests/test_ci_validation.py b/tests/test_ci_validation.py index df6b64d..c28c355 100644 --- a/tests/test_ci_validation.py +++ b/tests/test_ci_validation.py @@ -5,7 +5,7 @@ import pytest from dirty_equals import IsStr -from ruff_sync.cli import Arguments, OutputFormat, _validate_ci_output_format +from ruff_sync.cli import MISSING, Arguments, OutputFormat, _validate_ci_output_format if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture @@ -83,3 +83,112 @@ def test_validate_ci_output_format( assert any(record.message == expected_warning for record in caplog.records) else: assert not caplog.records + + +@pytest.mark.parametrize( + ("env_vars", "cli_args", "config", "expected"), + [ + # CLI takes precedence + ( + {"GITHUB_ACTIONS": "true"}, + {"output_format": OutputFormat.JSON}, + {}, + OutputFormat.JSON, + ), + # Config takes precedence over auto-detection + ( + {"GITHUB_ACTIONS": "true"}, + {}, + {"output-format": "gitlab"}, + OutputFormat.GITLAB, + ), + # Auto-detection: GitHub + ( + {"GITHUB_ACTIONS": "true"}, + {}, + {}, + OutputFormat.GITHUB, + ), + # Auto-detection: GitLab + ( + {"GITLAB_CI": "true"}, + {}, + {}, + OutputFormat.GITLAB, + ), + # Default fallback + ( + {}, + {}, + {}, + OutputFormat.TEXT, + ), + # Unknown config format falls back to auto-detect/default + ( + {"GITHUB_ACTIONS": "true"}, + {}, + {"output-format": "invalid"}, + OutputFormat.GITHUB, + ), + ], +) +def test_resolve_output_format( + monkeypatch: MonkeyPatch, + env_vars: dict[str, str], + cli_args: dict[str, Any], + config: dict[str, Any], + expected: OutputFormat, +) -> None: + """Test that output format is correctly resolved from CLI, config, and environment.""" + from ruff_sync.cli import _resolve_output_format + + # Mock environment + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + monkeypatch.delenv("GITLAB_CI", raising=False) + for k, v in env_vars.items(): + monkeypatch.setenv(k, v) + + # Mock CLI args + class MockArgs: + def __init__(self, **kwargs: Any) -> None: + for k, v in kwargs.items(): + setattr(self, k, v) + + args = MockArgs(**cli_args) + + assert _resolve_output_format(args, config) == ( + expected if expected != OutputFormat.TEXT else MISSING + ) + + +def test_main_resolves_output_format(monkeypatch: MonkeyPatch) -> None: + """End-to-end-ish test for output format resolution in main.""" + from ruff_sync.cli import main + + monkeypatch.setenv("GITHUB_ACTIONS", "true") + # minimal args to avoid errors before resolution + monkeypatch.setattr("sys.argv", ["ruff-sync", "check", "https://github.com/org/repo"]) + + # We don't actually want to run the whole thing, just see if it resolves correctly. + # But main calls asyncio.run(check(exec_args)). + # We can mock 'check' to verify the arguments it receives. + import ruff_sync.cli + + captured_args: Arguments | None = None + + async def mock_check(args: Arguments) -> int: + nonlocal captured_args + captured_args = args + return 0 + + monkeypatch.setattr(ruff_sync.cli, "check", mock_check) + # Mock pull as well just in case + monkeypatch.setattr(ruff_sync.cli, "pull", mock_check) + + # Mock get_config to return empty config + monkeypatch.setattr(ruff_sync.cli, "get_config", lambda _: {}) + + main() + + assert captured_args is not None + assert captured_args.output_format == OutputFormat.GITHUB From 4f17a4551fbc0f909a33a577e87dc02c3d06c31f Mon Sep 17 00:00:00 2001 From: Gabriel G Date: Fri, 3 Apr 2026 14:25:04 -0400 Subject: [PATCH 02/19] bump version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 441b07f..e55803d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ruff-sync" -version = "0.1.4" +version = "0.1.5.dev1" description = "Synchronize Ruff linter configuration across projects" keywords = ["ruff", "linter", "config", "synchronize", "python", "linting", "automation", "tomlkit", "pre-commit"] authors = [ diff --git a/uv.lock b/uv.lock index f375225..32fe790 100644 --- a/uv.lock +++ b/uv.lock @@ -1519,7 +1519,7 @@ wheels = [ [[package]] name = "ruff-sync" -version = "0.1.4" +version = "0.1.5.dev1" source = { editable = "." } dependencies = [ { name = "httpx" }, From 8ee0c6e64ea45d79bf27bb33b8670d2835e9f02a Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 3 Apr 2026 15:18:43 -0400 Subject: [PATCH 03/19] fix ci --- src/ruff_sync/cli.py | 5 +++-- src/ruff_sync/constants.py | 1 + src/ruff_sync/core.py | 6 ++++-- tests/test_ci_validation.py | 5 +++-- tests/test_constants.py | 17 +++++++++-------- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/ruff_sync/cli.py b/src/ruff_sync/cli.py index c7f9e09..c687518 100644 --- a/src/ruff_sync/cli.py +++ b/src/ruff_sync/cli.py @@ -408,10 +408,11 @@ def _resolve_output_format(args: Any, config: Mapping[str, Any]) -> OutputFormat fmt_str = config["output-format"] try: fmt = OutputFormat(fmt_str) - LOGGER.info(f"πŸ“Š Using output format from [tool.ruff-sync]: {fmt}") - return fmt except ValueError: LOGGER.warning(f"Unknown output format in config: {fmt_str}") + else: + LOGGER.info(f"πŸ“Š Using output format from [tool.ruff-sync]: {fmt}") + return fmt # Auto-detection provider = _detect_ci_provider() diff --git a/src/ruff_sync/constants.py b/src/ruff_sync/constants.py index 28d3fde..8b3c168 100644 --- a/src/ruff_sync/constants.py +++ b/src/ruff_sync/constants.py @@ -77,6 +77,7 @@ def resolve_defaults( branch: The resolved branch value or ``MISSING``. path: The resolved path value or ``MISSING``. exclude: The resolved exclude iterable or ``MISSING``. + output_format: The resolved output format or ``MISSING``. Returns: A ``(branch, path, exclude)`` tuple with defaults applied. diff --git a/src/ruff_sync/core.py b/src/ruff_sync/core.py index 2ed7e16..8664f1a 100644 --- a/src/ruff_sync/core.py +++ b/src/ruff_sync/core.py @@ -34,6 +34,8 @@ from typing_extensions import override from ruff_sync.constants import ( + DEFAULT_BRANCH, + MISSING, OutputFormat, resolve_defaults, ) @@ -1017,7 +1019,7 @@ async def check( ... ) >>> # asyncio.run(check(args)) """ - branch, path, exclude, output_format = _resolve_defaults(args) + _branch, _path, _exclude, output_format = _resolve_defaults(args) fmt = get_formatter(output_format) try: fmt.note("πŸ” Checking Ruff sync status...") @@ -1195,7 +1197,7 @@ async def pull( ... ) >>> # asyncio.run(pull(args)) """ - branch, path, exclude, output_format = _resolve_defaults(args) + _branch, _path, _exclude, output_format = _resolve_defaults(args) fmt = get_formatter(output_format) try: fmt.note("πŸ”„ Syncing Ruff...") diff --git a/tests/test_ci_validation.py b/tests/test_ci_validation.py index c28c355..3e02919 100644 --- a/tests/test_ci_validation.py +++ b/tests/test_ci_validation.py @@ -1,11 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import pytest from dirty_equals import IsStr -from ruff_sync.cli import MISSING, Arguments, OutputFormat, _validate_ci_output_format +from ruff_sync.cli import Arguments, OutputFormat, _validate_ci_output_format +from ruff_sync.constants import MISSING if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture diff --git a/tests/test_constants.py b/tests/test_constants.py index 2f14a09..3bd2955 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -10,16 +10,17 @@ def test_resolve_defaults_all_missing(): """Verify that MISSING for all args resolves to project defaults.""" - branch, path, exclude = resolve_defaults(MISSING, MISSING, MISSING) + branch, path, exclude, output_format = resolve_defaults(MISSING, MISSING, MISSING) assert branch == DEFAULT_BRANCH assert path is None # DEFAULT_PATH ("") is normalized to None assert exclude == DEFAULT_EXCLUDE + assert output_format == "text" # Default format def test_resolve_defaults_passthrough(): """Verify that non-MISSING values are passed through unchanged.""" - branch, path, exclude = resolve_defaults("develop", "src", ["rule1"]) + branch, path, exclude, _ = resolve_defaults("develop", "src", ["rule1"]) assert branch == "develop" assert path == "src" @@ -29,19 +30,19 @@ def test_resolve_defaults_passthrough(): def test_resolve_defaults_mixed(): """Verify mixed combinations of MISSING and explicit values.""" # Case: branch is MISSING, others are explicit - branch, path, exclude = resolve_defaults(MISSING, "subdir", ["exclude1"]) + branch, path, exclude, _ = resolve_defaults(MISSING, "subdir", ["exclude1"]) assert branch == DEFAULT_BRANCH assert path == "subdir" assert exclude == ["exclude1"] # Case: path is MISSING, others are explicit - branch, path, exclude = resolve_defaults("feat/branch", MISSING, ["exclude2"]) + branch, path, exclude, _ = resolve_defaults("feat/branch", MISSING, ["exclude2"]) assert branch == "feat/branch" assert path is None # MISSING normalized to None assert exclude == ["exclude2"] # Case: exclude is MISSING, others are explicit - branch, path, exclude = resolve_defaults("main", "root", MISSING) + branch, path, exclude, _ = resolve_defaults("main", "root", MISSING) assert branch == "main" assert path == "root" assert exclude == DEFAULT_EXCLUDE @@ -50,13 +51,13 @@ def test_resolve_defaults_mixed(): def test_resolve_defaults_path_normalization(): """Verify that empty string paths are normalized to None.""" # Explicit empty string - _, path, _ = resolve_defaults("main", "", MISSING) + _, path, _, _ = resolve_defaults("main", "", MISSING) assert path is None # MISSING (which is DEFAULT_PATH which is "") - _, path, _ = resolve_defaults("main", MISSING, MISSING) + _, path, _, _ = resolve_defaults("main", MISSING, MISSING) assert path is None # Non-empty string remains - _, path, _ = resolve_defaults("main", "backend", MISSING) + _, path, _, _ = resolve_defaults("main", "backend", MISSING) assert path == "backend" From 185287a1bebe67cd084e757946939df7a4a86e60 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 3 Apr 2026 18:09:10 -0400 Subject: [PATCH 04/19] more tests --- tests/test_ci_validation.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/test_ci_validation.py b/tests/test_ci_validation.py index 3e02919..68bdb2b 100644 --- a/tests/test_ci_validation.py +++ b/tests/test_ci_validation.py @@ -6,7 +6,7 @@ from dirty_equals import IsStr from ruff_sync.cli import Arguments, OutputFormat, _validate_ci_output_format -from ruff_sync.constants import MISSING +from ruff_sync.constants import MISSING, MissingType if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture @@ -117,20 +117,34 @@ def test_validate_ci_output_format( {}, OutputFormat.GITLAB, ), - # Default fallback + # Default fallback (returns MISSING) ( {}, {}, {}, - OutputFormat.TEXT, + MISSING, ), - # Unknown config format falls back to auto-detect/default + # Unknown config format falls back to auto-detect/default (GitHub in this env) ( {"GITHUB_ACTIONS": "true"}, {}, {"output-format": "invalid"}, OutputFormat.GITHUB, ), + # Explicit CLI TEXT (with CI env) + ( + {"GITHUB_ACTIONS": "true"}, + {"output_format": OutputFormat.TEXT}, + {}, + OutputFormat.TEXT, + ), + # Config TEXT (no CI env) + ( + {}, + {}, + {"output-format": "text"}, + OutputFormat.TEXT, + ), ], ) def test_resolve_output_format( @@ -138,7 +152,7 @@ def test_resolve_output_format( env_vars: dict[str, str], cli_args: dict[str, Any], config: dict[str, Any], - expected: OutputFormat, + expected: OutputFormat | MissingType, ) -> None: """Test that output format is correctly resolved from CLI, config, and environment.""" from ruff_sync.cli import _resolve_output_format @@ -157,9 +171,7 @@ def __init__(self, **kwargs: Any) -> None: args = MockArgs(**cli_args) - assert _resolve_output_format(args, config) == ( - expected if expected != OutputFormat.TEXT else MISSING - ) + assert _resolve_output_format(args, config) == expected def test_main_resolves_output_format(monkeypatch: MonkeyPatch) -> None: From af1606819891bac06483d6c1340968226fef1360 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 3 Apr 2026 18:09:32 -0400 Subject: [PATCH 05/19] reduce use of cast --- src/ruff_sync/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ruff_sync/cli.py b/src/ruff_sync/cli.py index c687518..874176c 100644 --- a/src/ruff_sync/cli.py +++ b/src/ruff_sync/cli.py @@ -363,9 +363,9 @@ def _resolve_exclude(args: Any, config: Mapping[str, Any]) -> Iterable[str] | Mi if args.exclude is not None: return cast("Iterable[str]", args.exclude) if "exclude" in config: - exclude = config["exclude"] + exclude: Iterable[str] = config["exclude"] LOGGER.info(f"🚫 Using exclude from [tool.ruff-sync]: {list(exclude)}") - return cast("Iterable[str]", exclude) + return exclude return MISSING @@ -378,7 +378,7 @@ def _resolve_branch(args: Any, config: Mapping[str, Any]) -> str | MissingType: if getattr(args, "branch", None): return cast("str", args.branch) if "branch" in config: - branch = cast("str", config["branch"]) + branch: str = config["branch"] LOGGER.info(f"🌿 Using branch from [tool.ruff-sync]: {branch}") return branch return MISSING @@ -393,7 +393,7 @@ def _resolve_path(args: Any, config: Mapping[str, Any]) -> str | MissingType: if getattr(args, "path", None): return cast("str", args.path) if "path" in config: - path = cast("str", config["path"]) + path: str = config["path"] LOGGER.info(f"πŸ“„ Using path from [tool.ruff-sync]: {path}") return path return MISSING From 2542f8a7b54ea14675b8e8ef7e0b18b646efd518 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 3 Apr 2026 19:02:16 -0400 Subject: [PATCH 06/19] better typing --- src/ruff_sync/cli.py | 61 +++++++++++++++++++++++++------------ tests/test_ci_validation.py | 16 +++++----- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/ruff_sync/cli.py b/src/ruff_sync/cli.py index 874176c..39e83e4 100644 --- a/src/ruff_sync/cli.py +++ b/src/ruff_sync/cli.py @@ -21,6 +21,7 @@ Final, Literal, NamedTuple, + Protocol, cast, ) @@ -134,6 +135,26 @@ class ResolvedArgs(NamedTuple): output_format: OutputFormat +class CLIArguments(Protocol): + """Protocol for parsed CLI arguments from ArgumentParser.""" + + command: str | None + upstream: list[URL] + to: str | None + source: str | None + exclude: list[str] | None + verbose: int + branch: str | None + path: str | None + pre_commit: bool | None + output_format: OutputFormat | None + # Subcommand specific + init: bool + save: bool | None + semantic: bool + diff: bool + + @lru_cache(maxsize=1) def get_config( source: pathlib.Path, @@ -311,10 +332,10 @@ def _get_cli_parser() -> ArgumentParser: PARSER: Final[ArgumentParser] = _get_cli_parser() -def _resolve_upstream(args: Any, config: Mapping[str, Any]) -> tuple[URL, ...]: +def _resolve_upstream(args: CLIArguments, config: Config) -> tuple[URL, ...]: """Resolve upstream URL(s) from CLI or config.""" if args.upstream: - upstreams = tuple(cast("Iterable[URL]", args.upstream)) + upstreams = tuple(args.upstream) # Log CLI upstreams for consistency with config sourcing summary = ( f"{upstreams[0]}... ({len(upstreams)} total)" @@ -352,7 +373,7 @@ def _resolve_upstream(args: Any, config: Mapping[str, Any]) -> tuple[URL, ...]: ) -def _resolve_exclude(args: Any, config: Mapping[str, Any]) -> Iterable[str] | MissingType: +def _resolve_exclude(args: CLIArguments, config: Config) -> Iterable[str] | MissingType: """Resolve exclude patterns from CLI or config. Returns the CLI value if provided, otherwise the value from @@ -361,7 +382,7 @@ def _resolve_exclude(args: Any, config: Mapping[str, Any]) -> Iterable[str] | Mi can apply the ``DEFAULT_EXCLUDE`` set downstream. """ if args.exclude is not None: - return cast("Iterable[str]", args.exclude) + return args.exclude if "exclude" in config: exclude: Iterable[str] = config["exclude"] LOGGER.info(f"🚫 Using exclude from [tool.ruff-sync]: {list(exclude)}") @@ -369,14 +390,14 @@ def _resolve_exclude(args: Any, config: Mapping[str, Any]) -> Iterable[str] | Mi return MISSING -def _resolve_branch(args: Any, config: Mapping[str, Any]) -> str | MissingType: +def _resolve_branch(args: CLIArguments, config: Config) -> str | MissingType: """Resolve branch name from CLI, config, or MISSING. An empty string (``--branch ""``) is treated as falsy and falls back to the config or default, preserving the original behaviour. """ - if getattr(args, "branch", None): - return cast("str", args.branch) + if args.branch: + return args.branch if "branch" in config: branch: str = config["branch"] LOGGER.info(f"🌿 Using branch from [tool.ruff-sync]: {branch}") @@ -384,14 +405,14 @@ def _resolve_branch(args: Any, config: Mapping[str, Any]) -> str | MissingType: return MISSING -def _resolve_path(args: Any, config: Mapping[str, Any]) -> str | MissingType: +def _resolve_path(args: CLIArguments, config: Config) -> str | MissingType: """Resolve path prefix from CLI, config, or MISSING. An empty string (``--path ""``) is treated as falsy and falls back to the config or default, preserving the original behaviour. """ - if getattr(args, "path", None): - return cast("str", args.path) + if args.path: + return args.path if "path" in config: path: str = config["path"] LOGGER.info(f"πŸ“„ Using path from [tool.ruff-sync]: {path}") @@ -399,13 +420,15 @@ def _resolve_path(args: Any, config: Mapping[str, Any]) -> str | MissingType: return MISSING -def _resolve_output_format(args: Any, config: Mapping[str, Any]) -> OutputFormat | MissingType: +def _resolve_output_format( + args: CLIArguments, config: Config +) -> OutputFormat | MissingType: """Resolve output format from CLI, config, or environment auto-detection.""" - if getattr(args, "output_format", None): - return cast("OutputFormat", args.output_format) + if args.output_format: + return args.output_format - if "output-format" in config: - fmt_str = config["output-format"] + if "output_format" in config: + fmt_str = config["output_format"] try: fmt = OutputFormat(fmt_str) except ValueError: @@ -423,7 +446,7 @@ def _resolve_output_format(args: Any, config: Mapping[str, Any]) -> OutputFormat return MISSING -def _resolve_to(args: Any, config: Mapping[str, Any], initial_to: pathlib.Path) -> pathlib.Path: +def _resolve_to(args: CLIArguments, config: Config, initial_to: pathlib.Path) -> pathlib.Path: """Resolve target path from CLI, config, or default.""" if args.source: LOGGER.warning("DeprecationWarning: --source is deprecated. Use --to instead.") @@ -440,7 +463,7 @@ def _resolve_to(args: Any, config: Mapping[str, Any], initial_to: pathlib.Path) return initial_to -def _resolve_args(args: Any, config: Mapping[str, Any], initial_to: pathlib.Path) -> ResolvedArgs: +def _resolve_args(args: CLIArguments, config: Config, initial_to: pathlib.Path) -> ResolvedArgs: """Resolve upstream, to, exclude, branch, and path from CLI and config.""" upstream = _resolve_upstream(args, config) to = _resolve_to(args, config, initial_to) @@ -524,11 +547,9 @@ def main() -> int: upstream, to_val, exclude, branch, path, output_format = _resolve_args(args, config, initial_to) - pre_commit_arg = getattr(args, "pre_commit", None) + pre_commit_arg = args.pre_commit if pre_commit_arg is not None: pre_commit_val: bool | MissingType = pre_commit_arg - elif "pre-commit-version-sync" in config: - pre_commit_val = cast("Any", config).get("pre-commit-version-sync") elif "pre_commit_version_sync" in config: pre_commit_val = config.get("pre_commit_version_sync", MISSING) else: diff --git a/tests/test_ci_validation.py b/tests/test_ci_validation.py index 68bdb2b..9cc2c4e 100644 --- a/tests/test_ci_validation.py +++ b/tests/test_ci_validation.py @@ -4,8 +4,8 @@ import pytest from dirty_equals import IsStr - -from ruff_sync.cli import Arguments, OutputFormat, _validate_ci_output_format +from ruff_sync.core import Config +from ruff_sync.cli import Arguments, OutputFormat, _validate_ci_output_format, CLIArguments from ruff_sync.constants import MISSING, MissingType if TYPE_CHECKING: @@ -100,7 +100,7 @@ def test_validate_ci_output_format( ( {"GITHUB_ACTIONS": "true"}, {}, - {"output-format": "gitlab"}, + {"output_format": "gitlab"}, OutputFormat.GITLAB, ), # Auto-detection: GitHub @@ -128,7 +128,7 @@ def test_validate_ci_output_format( ( {"GITHUB_ACTIONS": "true"}, {}, - {"output-format": "invalid"}, + {"output_format": "invalid"}, OutputFormat.GITHUB, ), # Explicit CLI TEXT (with CI env) @@ -142,7 +142,7 @@ def test_validate_ci_output_format( ( {}, {}, - {"output-format": "text"}, + {"output_format": "text"}, OutputFormat.TEXT, ), ], @@ -151,7 +151,7 @@ def test_resolve_output_format( monkeypatch: MonkeyPatch, env_vars: dict[str, str], cli_args: dict[str, Any], - config: dict[str, Any], + config: Config, expected: OutputFormat | MissingType, ) -> None: """Test that output format is correctly resolved from CLI, config, and environment.""" @@ -165,11 +165,13 @@ def test_resolve_output_format( # Mock CLI args class MockArgs: + output_format = None + def __init__(self, **kwargs: Any) -> None: for k, v in kwargs.items(): setattr(self, k, v) - args = MockArgs(**cli_args) + args: CLIArguments = MockArgs(**cli_args) # type: ignore[assignment] assert _resolve_output_format(args, config) == expected From 9d75e8b91aa03084507a67048a03e83b34793256 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 3 Apr 2026 19:15:28 -0400 Subject: [PATCH 07/19] bump mypy --- uv.lock | 80 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/uv.lock b/uv.lock index 32fe790..7563135 100644 --- a/uv.lock +++ b/uv.lock @@ -927,7 +927,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.1" +version = "1.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -936,39 +936,51 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, - { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, - { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/a2/a965c8c3fcd4fa8b84ba0d46606181b0d0a1d50f274c67877f3e9ed4882c/mypy-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d99f515f95fd03a90875fdb2cca12ff074aa04490db4d190905851bdf8a549a8", size = 14430138, upload-time = "2026-03-31T16:52:37.843Z" }, + { url = "https://files.pythonhosted.org/packages/53/6e/043477501deeb8eabbab7f1a2f6cac62cfb631806dc1d6862a04a7f5011b/mypy-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd0212976dc57a5bfeede7c219e7cd66568a32c05c9129686dd487c059c1b88a", size = 13311282, upload-time = "2026-03-31T16:55:11.021Z" }, + { url = "https://files.pythonhosted.org/packages/65/aa/bd89b247b83128197a214f29f0632ff3c14f54d4cd70d144d157bd7d7d6e/mypy-1.20.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8426d4d75d68714abc17a4292d922f6ba2cfb984b72c2278c437f6dae797865", size = 13750889, upload-time = "2026-03-31T16:52:02.909Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9d/2860be7355c45247ccc0be1501c91176318964c2a137bd4743f58ce6200e/mypy-1.20.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02cca0761c75b42a20a2757ae58713276605eb29a08dd8a6e092aa347c4115ca", size = 14619788, upload-time = "2026-03-31T16:50:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/75/7f/3ef3e360c91f3de120f205c8ce405e9caf9fc52ef14b65d37073e322c114/mypy-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3a49064504be59e59da664c5e149edc1f26c67c4f8e8456f6ba6aba55033018", size = 14918849, upload-time = "2026-03-31T16:51:10.478Z" }, + { url = "https://files.pythonhosted.org/packages/ae/72/af970dfe167ef788df7c5e6109d2ed0229f164432ce828bc9741a4250e64/mypy-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebea00201737ad4391142808ed16e875add5c17f676e0912b387739f84991e13", size = 10822007, upload-time = "2026-03-31T16:50:25.268Z" }, + { url = "https://files.pythonhosted.org/packages/93/94/ba9065c2ebe5421619aff684b793d953e438a8bfe31a320dd6d1e0706e81/mypy-1.20.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80cf77847d0d3e6e3111b7b25db32a7f8762fd4b9a3a72ce53fe16a2863b281", size = 9756158, upload-time = "2026-03-31T16:48:36.213Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1c/74cb1d9993236910286865679d1c616b136b2eae468493aa939431eda410/mypy-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4525e7010b1b38334516181c5b81e16180b8e149e6684cee5a727c78186b4e3b", size = 14343972, upload-time = "2026-03-31T16:49:04.887Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/01399515eca280386e308cf57901e68d3a52af18691941b773b3380c1df8/mypy-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a17c5d0bdcca61ce24a35beb828a2d0d323d3fcf387d7512206888c900193367", size = 13225007, upload-time = "2026-03-31T16:50:08.151Z" }, + { url = "https://files.pythonhosted.org/packages/56/ac/b4ba5094fb2d7fe9d2037cd8d18bbe02bcf68fd22ab9ff013f55e57ba095/mypy-1.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75ff57defcd0f1d6e006d721ccdec6c88d4f6a7816eb92f1c4890d979d9ee62", size = 13663752, upload-time = "2026-03-31T16:49:26.064Z" }, + { url = "https://files.pythonhosted.org/packages/db/a7/460678d3cf7da252d2288dad0c602294b6ec22a91932ec368cc11e44bb6e/mypy-1.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b503ab55a836136b619b5fc21c8803d810c5b87551af8600b72eecafb0059cb0", size = 14532265, upload-time = "2026-03-31T16:53:55.077Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/051cca8166cf0438ae3ea80e0e7c030d7a8ab98dffc93f80a1aa3f23c1a2/mypy-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1973868d2adbb4584a3835780b27436f06d1dc606af5be09f187aaa25be1070f", size = 14768476, upload-time = "2026-03-31T16:50:34.587Z" }, + { url = "https://files.pythonhosted.org/packages/be/66/8e02ec184f852ed5c4abb805583305db475930854e09964b55e107cdcbc4/mypy-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:2fcedb16d456106e545b2bfd7ef9d24e70b38ec252d2a629823a4d07ebcdb69e", size = 10818226, upload-time = "2026-03-31T16:53:15.624Z" }, + { url = "https://files.pythonhosted.org/packages/13/4b/383ad1924b28f41e4879a74151e7a5451123330d45652da359f9183bcd45/mypy-1.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:379edf079ce44ac8d2805bcf9b3dd7340d4f97aad3a5e0ebabbf9d125b84b442", size = 9750091, upload-time = "2026-03-31T16:54:12.162Z" }, + { url = "https://files.pythonhosted.org/packages/be/dd/3afa29b58c2e57c79116ed55d700721c3c3b15955e2b6251dd165d377c0e/mypy-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214", size = 14509525, upload-time = "2026-03-31T16:55:01.824Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/227b516ab8cad9f2a13c5e7a98d28cd6aa75e9c83e82776ae6c1c4c046c7/mypy-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e", size = 13326469, upload-time = "2026-03-31T16:51:41.23Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/1ddb799860c1b5ac6117ec307b965f65deeb47044395ff01ab793248a591/mypy-1.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651", size = 13705953, upload-time = "2026-03-31T16:48:55.69Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b7/54a720f565a87b893182a2a393370289ae7149e4715859e10e1c05e49154/mypy-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5", size = 14710363, upload-time = "2026-03-31T16:53:26.948Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2a/74810274848d061f8a8ea4ac23aaad43bd3d8c1882457999c2e568341c57/mypy-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78", size = 14947005, upload-time = "2026-03-31T16:50:17.591Z" }, + { url = "https://files.pythonhosted.org/packages/77/91/21b8ba75f958bcda75690951ce6fa6b7138b03471618959529d74b8544e2/mypy-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489", size = 10880616, upload-time = "2026-03-31T16:52:19.986Z" }, + { url = "https://files.pythonhosted.org/packages/8a/15/3d8198ef97c1ca03aea010cce4f1d4f3bc5d9849e8c0140111ca2ead9fdd/mypy-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33", size = 9813091, upload-time = "2026-03-31T16:53:44.385Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f64ea7bd592fa431cb597418b6dec4a47f7d0c36325fec7ac67bc8402b94/mypy-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b20c8b0fd5877abdf402e79a3af987053de07e6fb208c18df6659f708b535134", size = 14485344, upload-time = "2026-03-31T16:49:16.78Z" }, + { url = "https://files.pythonhosted.org/packages/bb/72/8927d84cfc90c6abea6e96663576e2e417589347eb538749a464c4c218a0/mypy-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:367e5c993ba34d5054d11937d0485ad6dfc60ba760fa326c01090fc256adf15c", size = 13327400, upload-time = "2026-03-31T16:53:08.02Z" }, + { url = "https://files.pythonhosted.org/packages/ab/4a/11ab99f9afa41aa350178d24a7d2da17043228ea10f6456523f64b5a6cf6/mypy-1.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f799d9db89fc00446f03281f84a221e50018fc40113a3ba9864b132895619ebe", size = 13706384, upload-time = "2026-03-31T16:52:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/42/79/694ca73979cfb3535ebfe78733844cd5aff2e63304f59bf90585110d975a/mypy-1.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555658c611099455b2da507582ea20d2043dfdfe7f5ad0add472b1c6238b433f", size = 14700378, upload-time = "2026-03-31T16:48:45.527Z" }, + { url = "https://files.pythonhosted.org/packages/84/24/a022ccab3a46e3d2cdf2e0e260648633640eb396c7e75d5a42818a8d3971/mypy-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:efe8d70949c3023698c3fca1e94527e7e790a361ab8116f90d11221421cd8726", size = 14932170, upload-time = "2026-03-31T16:49:36.038Z" }, + { url = "https://files.pythonhosted.org/packages/d8/9b/549228d88f574d04117e736f55958bd4908f980f9f5700a07aeb85df005b/mypy-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:f49590891d2c2f8a9de15614e32e459a794bcba84693c2394291a2038bbaaa69", size = 10888526, upload-time = "2026-03-31T16:50:59.827Z" }, + { url = "https://files.pythonhosted.org/packages/91/17/15095c0e54a8bc04d22d4ff06b2139d5f142c2e87520b4e39010c4862771/mypy-1.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:76a70bf840495729be47510856b978f1b0ec7d08f257ca38c9d932720bf6b43e", size = 9816456, upload-time = "2026-03-31T16:49:59.537Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/6ca4a84cbed9e62384bc0b2974c90395ece5ed672393e553996501625fc5/mypy-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f42dfaab7ec1baff3b383ad7af562ab0de573c5f6edb44b2dab016082b89948", size = 14483331, upload-time = "2026-03-31T16:52:57.999Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c5/5fe9d8a729dd9605064691816243ae6c49fde0bd28f6e5e17f6a24203c43/mypy-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:31b5dbb55293c1bd27c0fc813a0d2bb5ceef9d65ac5afa2e58f829dab7921fd5", size = 13342047, upload-time = "2026-03-31T16:54:21.555Z" }, + { url = "https://files.pythonhosted.org/packages/4c/33/e18bcfa338ca4e6b2771c85d4c5203e627d0c69d9de5c1a2cf2ba13320ba/mypy-1.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d11c6f573a5a08f77fad13faff2139f6d0730ebed2cfa9b3d2702671dd7188", size = 13719585, upload-time = "2026-03-31T16:51:53.89Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/93491ff7b79419edc7eabf95cb3b3f7490e2e574b2855c7c7e7394ff933f/mypy-1.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d3243c406773185144527f83be0e0aefc7bf4601b0b2b956665608bf7c98a83", size = 14685075, upload-time = "2026-03-31T16:54:04.464Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9d/d924b38a4923f8d164bf2b4ec98bf13beaf6e10a5348b4b137eadae40a6e/mypy-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a79c1eba7ac4209f2d850f0edd0a2f8bba88cbfdfefe6fb76a19e9d4fe5e71a2", size = 14919141, upload-time = "2026-03-31T16:54:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/59/98/1da9977016678c0b99d43afe52ed00bb3c1a0c4c995d3e6acca1a6ebb9b4/mypy-1.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:00e047c74d3ec6e71a2eb88e9ea551a2edb90c21f993aefa9e0d2a898e0bb732", size = 11050925, upload-time = "2026-03-31T16:51:30.758Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e3/ba0b7a3143e49a9c4f5967dde6ea4bf8e0b10ecbbcca69af84027160ee89/mypy-1.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:931a7630bba591593dcf6e97224a21ff80fb357e7982628d25e3c618e7f598ef", size = 10001089, upload-time = "2026-03-31T16:49:43.632Z" }, + { url = "https://files.pythonhosted.org/packages/12/28/e617e67b3be9d213cda7277913269c874eb26472489f95d09d89765ce2d8/mypy-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:26c8b52627b6552f47ff11adb4e1509605f094e29815323e487fc0053ebe93d1", size = 15534710, upload-time = "2026-03-31T16:52:12.506Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/3b5f2d3e45dc7169b811adce8451679d9430399d03b168f9b0489f43adaa/mypy-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39362cdb4ba5f916e7976fccecaab1ba3a83e35f60fa68b64e9a70e221bb2436", size = 14393013, upload-time = "2026-03-31T16:54:41.186Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/edc8b0aa145cc09c1c74f7ce2858eead9329931dcbbb26e2ad40906daa4e/mypy-1.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34506397dbf40c15dc567635d18a21d33827e9ab29014fb83d292a8f4f8953b6", size = 15047240, upload-time = "2026-03-31T16:54:31.955Z" }, + { url = "https://files.pythonhosted.org/packages/42/37/a946bb416e37a57fa752b3100fd5ede0e28df94f92366d1716555d47c454/mypy-1.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555493c44a4f5a1b58d611a43333e71a9981c6dbe26270377b6f8174126a0526", size = 15858565, upload-time = "2026-03-31T16:53:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/2f/99/7690b5b5b552db1bd4ff362e4c0eb3107b98d680835e65823fbe888c8b78/mypy-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2721f0ce49cb74a38f00c50da67cb7d36317b5eda38877a49614dc018e91c787", size = 16087874, upload-time = "2026-03-31T16:52:48.313Z" }, + { url = "https://files.pythonhosted.org/packages/aa/76/53e893a498138066acd28192b77495c9357e5a58cc4be753182846b43315/mypy-1.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:47781555a7aa5fedcc2d16bcd72e0dc83eb272c10dd657f9fb3f9cc08e2e6abb", size = 12572380, upload-time = "2026-03-31T16:49:52.454Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/6dbdae21f01b7aacddc2c0bbf3c5557aa547827fdf271770fe1e521e7093/mypy-1.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c70380fe5d64010f79fb863b9081c7004dd65225d2277333c219d93a10dad4dd", size = 10381174, upload-time = "2026-03-31T16:51:20.179Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4d734961ce167f0fd8380769b3b7c06dbdd6ff54c2190f3f2ecd22528158/mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", size = 2636365, upload-time = "2026-03-31T16:51:44.911Z" }, ] [[package]] From e5c347c16f2d18b832c7cd88f0a4c717184ef599 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 3 Apr 2026 19:16:23 -0400 Subject: [PATCH 08/19] bump linters --- .pre-commit-config.yaml | 2 +- tests/test_ci_validation.py | 5 +++-- uv.lock | 42 ++++++++++++++++++------------------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b9489fc..0daf971 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: no-commit-to-branch args: [--branch, develop, --branch, main] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.15.8" + rev: "v0.15.9" hooks: - id: ruff-check args: ["--fix"] diff --git a/tests/test_ci_validation.py b/tests/test_ci_validation.py index 9cc2c4e..d187684 100644 --- a/tests/test_ci_validation.py +++ b/tests/test_ci_validation.py @@ -4,9 +4,10 @@ import pytest from dirty_equals import IsStr -from ruff_sync.core import Config -from ruff_sync.cli import Arguments, OutputFormat, _validate_ci_output_format, CLIArguments + +from ruff_sync.cli import Arguments, CLIArguments, OutputFormat, _validate_ci_output_format from ruff_sync.constants import MISSING, MissingType +from ruff_sync.core import Config if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture diff --git a/uv.lock b/uv.lock index 7563135..7f8575f 100644 --- a/uv.lock +++ b/uv.lock @@ -1506,27 +1506,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, - { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, - { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, - { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, - { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, - { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, - { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, - { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, - { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, - { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, +version = "0.15.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, + { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, + { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, + { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, ] [[package]] From 6294388eb63d9ffd2608ed4092c718e4a7813bf2 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 3 Apr 2026 19:24:00 -0400 Subject: [PATCH 09/19] ban the use of cast --- pyproject.toml | 2 ++ src/ruff_sync/cli.py | 15 ++++++--------- tasks.py | 6 +++--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e55803d..8f51ad0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -206,6 +206,7 @@ ignore = [ "PLW1510", # subprocess check not needed in tests "PT013", # pytest import style, prevents importing param and or types in from style "TC002", # Move third-party import into type-checking block + "TID251", # allow cast in tests ] [tool.ruff.lint.pydocstyle] @@ -222,6 +223,7 @@ docstring-code-line-length = "dynamic" [tool.ruff.lint.flake8-tidy-imports.banned-api] "unittest.mock".msg = "Prefer dedicated libraries to mock at external dependencies, such as `httpx` or `pyfakefs`." +"typing.cast".msg = "Please fix the underlying type issues instead of using typing.cast." [tool.ruff.lint.flake8-import-conventions] # Declare the banned `from` imports. diff --git a/src/ruff_sync/cli.py b/src/ruff_sync/cli.py index 39e83e4..f48156a 100644 --- a/src/ruff_sync/cli.py +++ b/src/ruff_sync/cli.py @@ -22,7 +22,6 @@ Literal, NamedTuple, Protocol, - cast, ) import tomlkit @@ -194,7 +193,7 @@ def get_config( # Ensure 'to' is populated if 'source' was used if "source" in cfg_result and "to" not in cfg_result: cfg_result["to"] = cfg_result["source"] - return cast("Config", cfg_result) + return cfg_result # type: ignore[return-value] def _get_cli_parser() -> ArgumentParser: @@ -420,9 +419,7 @@ def _resolve_path(args: CLIArguments, config: Config) -> str | MissingType: return MISSING -def _resolve_output_format( - args: CLIArguments, config: Config -) -> OutputFormat | MissingType: +def _resolve_output_format(args: CLIArguments, config: Config) -> OutputFormat | MissingType: """Resolve output format from CLI, config, or environment auto-detection.""" if args.output_format: return args.output_format @@ -474,10 +471,10 @@ def _resolve_args(args: CLIArguments, config: Config, initial_to: pathlib.Path) return ResolvedArgs( upstream, to, - cast("Any", exclude), - cast("Any", branch), - cast("Any", path), - cast("Any", output_format), + exclude, + branch, + path, + output_format, # type: ignore[arg-type] ) diff --git a/tasks.py b/tasks.py index e0c8fd6..f0964bc 100644 --- a/tasks.py +++ b/tasks.py @@ -11,7 +11,7 @@ import logging import pathlib -from typing import TYPE_CHECKING, Any, Final, Literal, cast +from typing import TYPE_CHECKING, Final, Literal import httpx from invoke.exceptions import Exit @@ -132,14 +132,14 @@ def release( """Tag and create a GitHub release for the current project version.""" # Check if we are on the main branch branch_result = ctx.run("git branch --show-current", hide=True) - current_branch = cast("Any", branch_result).stdout.strip() + current_branch = branch_result.stdout.strip() # type: ignore[union-attr] if not dry_run and current_branch != "main": print(f"❌ Releases must be made from the 'main' branch (current: {current_branch}).") return # Check for dirty git state status_result = ctx.run("git status --porcelain", hide=True) - git_status = cast("Any", status_result).stdout.strip() + git_status = status_result.stdout.strip() # type: ignore[union-attr] if git_status: print("❌ Git repository has uncommitted changes. Please commit or stash them first.") return From bc6eb19f39fb5416d015d7c8f47c10fd0c656db1 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 3 Apr 2026 19:30:23 -0400 Subject: [PATCH 10/19] fix types --- src/ruff_sync/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ruff_sync/cli.py b/src/ruff_sync/cli.py index f48156a..591cb8d 100644 --- a/src/ruff_sync/cli.py +++ b/src/ruff_sync/cli.py @@ -419,7 +419,7 @@ def _resolve_path(args: CLIArguments, config: Config) -> str | MissingType: return MISSING -def _resolve_output_format(args: CLIArguments, config: Config) -> OutputFormat | MissingType: +def _resolve_output_format(args: CLIArguments, config: Config) -> OutputFormat: """Resolve output format from CLI, config, or environment auto-detection.""" if args.output_format: return args.output_format @@ -440,7 +440,7 @@ def _resolve_output_format(args: CLIArguments, config: Config) -> OutputFormat | LOGGER.info(f"πŸ€– Auto-detected CI environment: {provider}") return provider - return MISSING + return OutputFormat.TEXT def _resolve_to(args: CLIArguments, config: Config, initial_to: pathlib.Path) -> pathlib.Path: @@ -474,7 +474,7 @@ def _resolve_args(args: CLIArguments, config: Config, initial_to: pathlib.Path) exclude, branch, path, - output_format, # type: ignore[arg-type] + output_format, ) From 975f210a4e0c1d1343f25d660e59fb0cc501eabc Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 3 Apr 2026 19:32:25 -0400 Subject: [PATCH 11/19] ignore overlap --- tests/test_basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_basic.py b/tests/test_basic.py index 8ad8b0b..87cb444 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -670,9 +670,9 @@ def test_ruff_config_file_name_equality() -> None: """Test equality comparisons.""" # intentional non-overlapping comparison assert ruff_sync.RuffConfigFileName.PYPROJECT_TOML == "pyproject.toml" # type: ignore[comparison-overlap] - assert ruff_sync.RuffConfigFileName.RUFF_TOML == "ruff.toml" - assert ruff_sync.RuffConfigFileName.DOT_RUFF_TOML == ".ruff.toml" - assert ruff_sync.RuffConfigFileName.PYPROJECT_TOML != ruff_sync.RuffConfigFileName.RUFF_TOML + assert ruff_sync.RuffConfigFileName.RUFF_TOML == "ruff.toml" # type: ignore[comparison-overlap] + assert ruff_sync.RuffConfigFileName.DOT_RUFF_TOML == ".ruff.toml" # type: ignore[comparison-overlap] + assert ruff_sync.RuffConfigFileName.PYPROJECT_TOML != ruff_sync.RuffConfigFileName.RUFF_TOML # type: ignore[comparison-overlap] @pytest.mark.asyncio From ec5fe36d5a68b57caff5eeaa565b003a7f127e91 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 3 Apr 2026 19:35:49 -0400 Subject: [PATCH 12/19] better annotations --- src/ruff_sync/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ruff_sync/core.py b/src/ruff_sync/core.py index 8664f1a..d80b601 100644 --- a/src/ruff_sync/core.py +++ b/src/ruff_sync/core.py @@ -1020,7 +1020,7 @@ async def check( >>> # asyncio.run(check(args)) """ _branch, _path, _exclude, output_format = _resolve_defaults(args) - fmt = get_formatter(output_format) + fmt: ResultFormatter = get_formatter(output_format) try: fmt.note("πŸ” Checking Ruff sync status...") From 12b46d66c6519dc5e08b8e899507287dd674f719 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 3 Apr 2026 20:03:52 -0400 Subject: [PATCH 13/19] reorganize for typing --- src/ruff_sync/cli.py | 86 ++++++++++++++++--------------------- src/ruff_sync/core.py | 33 +++++--------- tests/test_basic.py | 18 +++----- tests/test_ci_validation.py | 7 ++- tests/test_formatters.py | 6 +-- tests/test_serialization.py | 59 +++++++++---------------- 6 files changed, 80 insertions(+), 129 deletions(-) diff --git a/src/ruff_sync/cli.py b/src/ruff_sync/cli.py index 591cb8d..17b2484 100644 --- a/src/ruff_sync/cli.py +++ b/src/ruff_sync/cli.py @@ -16,7 +16,6 @@ from importlib import metadata from typing import ( TYPE_CHECKING, - Any, ClassVar, Final, Literal, @@ -26,16 +25,13 @@ import tomlkit from httpx import URL -from typing_extensions import deprecated +from typing_extensions import deprecated, override from ruff_sync.constants import ( DEFAULT_BRANCH, DEFAULT_EXCLUDE, DEFAULT_PATH, - MISSING, - MissingType, OutputFormat, - resolve_defaults, ) from ruff_sync.core import ( Config, @@ -85,7 +81,8 @@ def __init__(self, fmt: str = "%(message)s") -> None: """Initialize the formatter with a format string.""" super().__init__(fmt) - def format(self, record: logging.LogRecord) -> str: # type: ignore[explicit-override] + @override + def format(self, record: logging.LogRecord) -> str: """Format the log record with colors if the output is a TTY.""" if sys.stderr.isatty(): color = self.COLORS.get(record.levelno, self.RESET) @@ -99,14 +96,14 @@ class Arguments(NamedTuple): command: str upstream: tuple[URL, ...] to: pathlib.Path - exclude: Iterable[str] | MissingType + exclude: Iterable[str] verbose: int - branch: str | MissingType = MISSING - path: str | MissingType = MISSING + branch: str = DEFAULT_BRANCH + path: str | None = None semantic: bool = False diff: bool = True init: bool = False - pre_commit: bool | MissingType = MISSING + pre_commit: bool = False save: bool | None = None output_format: OutputFormat = OutputFormat.TEXT @@ -128,9 +125,9 @@ class ResolvedArgs(NamedTuple): upstream: tuple[URL, ...] to: pathlib.Path - exclude: Iterable[str] | MissingType - branch: str | MissingType - path: str | MissingType + exclude: Iterable[str] + branch: str + path: str | None output_format: OutputFormat @@ -168,7 +165,7 @@ def get_config( """ local_toml = source / RuffConfigFileName.PYPROJECT_TOML # TODO: use pydantic to validate the toml file - cfg_result: dict[str, Any] = {} + cfg_result: Config = {} if local_toml.exists(): toml = tomlkit.parse(local_toml.read_text()) config = toml.get("tool", {}).get("ruff-sync") @@ -187,13 +184,13 @@ def get_config( "DeprecationWarning: [tool.ruff-sync] 'source' is deprecated. " "Use 'to' instead." ) - cfg_result[arg_norm] = value + cfg_result[arg_norm] = value # type: ignore[literal-required] else: LOGGER.warning(f"Unknown ruff-sync configuration: {arg}") # Ensure 'to' is populated if 'source' was used if "source" in cfg_result and "to" not in cfg_result: cfg_result["to"] = cfg_result["source"] - return cfg_result # type: ignore[return-value] + return cfg_result def _get_cli_parser() -> ArgumentParser: @@ -372,7 +369,7 @@ def _resolve_upstream(args: CLIArguments, config: Config) -> tuple[URL, ...]: ) -def _resolve_exclude(args: CLIArguments, config: Config) -> Iterable[str] | MissingType: +def _resolve_exclude(args: CLIArguments, config: Config) -> Iterable[str]: """Resolve exclude patterns from CLI or config. Returns the CLI value if provided, otherwise the value from @@ -386,10 +383,10 @@ def _resolve_exclude(args: CLIArguments, config: Config) -> Iterable[str] | Miss exclude: Iterable[str] = config["exclude"] LOGGER.info(f"🚫 Using exclude from [tool.ruff-sync]: {list(exclude)}") return exclude - return MISSING + return DEFAULT_EXCLUDE -def _resolve_branch(args: CLIArguments, config: Config) -> str | MissingType: +def _resolve_branch(args: CLIArguments, config: Config) -> str: """Resolve branch name from CLI, config, or MISSING. An empty string (``--branch ""``) is treated as falsy and falls back to @@ -401,10 +398,10 @@ def _resolve_branch(args: CLIArguments, config: Config) -> str | MissingType: branch: str = config["branch"] LOGGER.info(f"🌿 Using branch from [tool.ruff-sync]: {branch}") return branch - return MISSING + return DEFAULT_BRANCH -def _resolve_path(args: CLIArguments, config: Config) -> str | MissingType: +def _resolve_path(args: CLIArguments, config: Config) -> str: """Resolve path prefix from CLI, config, or MISSING. An empty string (``--path ""``) is treated as falsy and falls back to @@ -416,7 +413,7 @@ def _resolve_path(args: CLIArguments, config: Config) -> str | MissingType: path: str = config["path"] LOGGER.info(f"πŸ“„ Using path from [tool.ruff-sync]: {path}") return path - return MISSING + return DEFAULT_PATH def _resolve_output_format(args: CLIArguments, config: Config) -> OutputFormat: @@ -468,16 +465,30 @@ def _resolve_args(args: CLIArguments, config: Config, initial_to: pathlib.Path) branch = _resolve_branch(args, config) path = _resolve_path(args, config) output_format = _resolve_output_format(args, config) + + # Normalize path "" -> None to match core expectation + final_path: str | None = path or None + return ResolvedArgs( upstream, to, exclude, branch, - path, + final_path, output_format, ) +def _resolve_pre_commit(args: CLIArguments, config: Config) -> bool: + """Resolve pre-commit sync setting from CLI or config.""" + if args.pre_commit is not None: + return args.pre_commit + if "pre_commit_version_sync" in config: + val = config.get("pre_commit_version_sync") + return bool(val) + return False + + def _detect_ci_provider() -> CIProvider | None: """Return the current CI provider, or None if not in a CI environment.""" if os.environ.get("GITHUB_ACTIONS") == "true": @@ -543,21 +554,13 @@ def main() -> int: config: Config = get_config(initial_to) upstream, to_val, exclude, branch, path, output_format = _resolve_args(args, config, initial_to) + pre_commit_val = _resolve_pre_commit(args, config) + + resolved_upstream = tuple(resolve_raw_url(u, branch=branch, path=path) for u in upstream) - pre_commit_arg = args.pre_commit - if pre_commit_arg is not None: - pre_commit_val: bool | MissingType = pre_commit_arg - elif "pre_commit_version_sync" in config: - pre_commit_val = config.get("pre_commit_version_sync", MISSING) - else: - pre_commit_val = MISSING - - # Build Arguments first so _resolve_defaults can centralize MISSING β†’ default - # resolution for branch and path (keeps cli.main and core._merge_multiple_upstreams - # in sync with a single source of truth). exec_args = Arguments( command=args.command, - upstream=upstream, + upstream=resolved_upstream, to=to_val, exclude=exclude, verbose=args.verbose, @@ -571,19 +574,6 @@ def main() -> int: output_format=output_format, ) - # Use the shared helper from constants so the MISSINGβ†’default logic for - # branch/path/output_format cannot diverge between cli.main and - # core._merge_multiple_upstreams. - res_branch, res_path, _exclude, res_output_format = resolve_defaults( - branch, path, exclude, output_format - ) - exec_args = exec_args._replace(output_format=res_output_format) - - resolved_upstream = tuple( - resolve_raw_url(u, branch=res_branch, path=res_path) for u in upstream - ) - exec_args = exec_args._replace(upstream=resolved_upstream) - # Warn if the specified output format doesn't match the current CI environment _validate_ci_output_format(exec_args) diff --git a/src/ruff_sync/core.py b/src/ruff_sync/core.py index d80b601..ab28d28 100644 --- a/src/ruff_sync/core.py +++ b/src/ruff_sync/core.py @@ -35,9 +35,8 @@ from ruff_sync.constants import ( DEFAULT_BRANCH, - MISSING, - OutputFormat, - resolve_defaults, + DEFAULT_EXCLUDE, + DEFAULT_PATH, ) from ruff_sync.formatters import ResultFormatter, get_formatter from ruff_sync.pre_commit import sync_pre_commit @@ -800,18 +799,6 @@ async def fetch_upstreams_concurrently( return fetch_results -def _resolve_defaults( - args: Arguments, -) -> tuple[str, str | None, Iterable[str], OutputFormat]: - """Resolve MISSING sentinel values in *args* to their effective defaults. - - Delegates to :func:`~ruff_sync.constants.resolve_defaults` so that the - MISSING β†’ default logic is centralised in one place and shared with - ``cli.main`` without coupling the two layers together. - """ - return resolve_defaults(args.branch, args.path, args.exclude, args.output_format) - - async def _merge_multiple_upstreams( target_doc: TOMLDocument, is_target_ruff_toml: bool, @@ -823,7 +810,7 @@ async def _merge_multiple_upstreams( Downloads are performed concurrently via fetch_upstreams_concurrently, while merging remains sequential to preserve layering order. """ - branch, path, exclude, _fmt = _resolve_defaults(args) + branch, path, exclude = args.branch, args.path, args.exclude fetch_results = await fetch_upstreams_concurrently( args.upstream, client, branch=branch, path=path @@ -1019,7 +1006,7 @@ async def check( ... ) >>> # asyncio.run(check(args)) """ - _branch, _path, _exclude, output_format = _resolve_defaults(args) + output_format = args.output_format fmt: ResultFormatter = get_formatter(output_format) try: fmt.note("πŸ” Checking Ruff sync status...") @@ -1158,20 +1145,20 @@ def serialize_ruff_sync_config(doc: TOMLDocument, args: Arguments) -> None: # Normalize excludes and de-duplicate while preserving order. # Only compute and persist excludes when explicitly provided so that # DEFAULT_EXCLUDE remains an implicit default and is not serialized. - if args.exclude is not MISSING: + if args.exclude != DEFAULT_EXCLUDE: normalized_excludes = list(dict.fromkeys(args.exclude)) exclude_array = tomlkit.array() for ex in normalized_excludes: exclude_array.append(ex) ruff_sync_table["exclude"] = exclude_array - if args.branch is not MISSING: + if args.branch != DEFAULT_BRANCH: ruff_sync_table["branch"] = args.branch - if args.path is not MISSING: + if args.path != DEFAULT_PATH and args.path is not None: ruff_sync_table["path"] = args.path - if args.pre_commit is not MISSING: + if args.pre_commit: ruff_sync_table["pre-commit-version-sync"] = args.pre_commit @@ -1197,7 +1184,7 @@ async def pull( ... ) >>> # asyncio.run(pull(args)) """ - _branch, _path, _exclude, output_format = _resolve_defaults(args) + output_format = args.output_format fmt = get_formatter(output_format) try: fmt.note("πŸ”„ Syncing Ruff...") @@ -1251,7 +1238,7 @@ async def pull( rel_path = _source_toml_path.resolve() fmt.success(f"βœ… Updated {rel_path}") - if args.pre_commit is not MISSING and args.pre_commit: + if args.pre_commit: sync_pre_commit(pathlib.Path.cwd(), dry_run=False) return 0 diff --git a/tests/test_basic.py b/tests/test_basic.py index 87cb444..d9b58d8 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -24,7 +24,7 @@ import ruff_sync import ruff_sync.cli as ruff_sync_cli -from ruff_sync.constants import DEFAULT_EXCLUDE, MISSING +from ruff_sync.constants import DEFAULT_EXCLUDE from ruff_sync.core import ( UpstreamError, _merge_multiple_upstreams, @@ -401,12 +401,8 @@ def test_exclude_resolution_config_precedence(patch_cli: CLIPatch): assert patch_cli.captured_args[0].exclude == ["from-config"] -def test_exclude_resolution_default_is_missing(patch_cli: CLIPatch): - """Default exclude should be MISSING when neither CLI nor config provides it. - - The CLI leaves `exclude` as MISSING so that downstream resolution (in - `_merge_multiple_upstreams`) applies the correct DEFAULT_EXCLUDE set. - """ +def test_exclude_resolution_is_default(patch_cli: CLIPatch): + """Default exclude should be DEFAULT_EXCLUDE when neither CLI nor config provides it.""" sys.argv = ["ruff-sync", "http://example.com"] # Mock get_config to return a config without 'exclude' (use defaults) patch_cli.set_config({}) @@ -414,13 +410,9 @@ def test_exclude_resolution_default_is_missing(patch_cli: CLIPatch): ruff_sync.main() assert len(patch_cli.captured_args) == 1 - # The CLI should leave `exclude` as MISSING so that default resolution is applied later. + # The CLI now resolves defaults early exclude = patch_cli.captured_args[0].exclude - assert exclude is MISSING - - # Simulate downstream default resolution to ensure the default exclude set is used. - resolved_exclude = DEFAULT_EXCLUDE if exclude is MISSING else exclude - assert set(resolved_exclude) == DEFAULT_EXCLUDE + assert exclude == DEFAULT_EXCLUDE def test_main_default_to_resolution(patch_cli: CLIPatch): diff --git a/tests/test_ci_validation.py b/tests/test_ci_validation.py index d187684..b40fad5 100644 --- a/tests/test_ci_validation.py +++ b/tests/test_ci_validation.py @@ -6,7 +6,6 @@ from dirty_equals import IsStr from ruff_sync.cli import Arguments, CLIArguments, OutputFormat, _validate_ci_output_format -from ruff_sync.constants import MISSING, MissingType from ruff_sync.core import Config if TYPE_CHECKING: @@ -118,12 +117,12 @@ def test_validate_ci_output_format( {}, OutputFormat.GITLAB, ), - # Default fallback (returns MISSING) + # Default fallback (returns OutputFormat.TEXT) ( {}, {}, {}, - MISSING, + OutputFormat.TEXT, ), # Unknown config format falls back to auto-detect/default (GitHub in this env) ( @@ -153,7 +152,7 @@ def test_resolve_output_format( env_vars: dict[str, str], cli_args: dict[str, Any], config: Config, - expected: OutputFormat | MissingType, + expected: OutputFormat, ) -> None: """Test that output format is correctly resolved from CLI, config, and environment.""" from ruff_sync.cli import _resolve_output_format diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 41b06cf..33049f0 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -7,7 +7,7 @@ import pytest -from ruff_sync.constants import MISSING, OutputFormat +from ruff_sync.constants import DEFAULT_EXCLUDE, OutputFormat from ruff_sync.formatters import ( GithubFormatter, GitlabFormatter, @@ -574,7 +574,7 @@ async def _mock_merge(*args, **kwargs): command="check", upstream=(URL("https://example.com"),), to=pathlib.Path("pyproject.toml"), - exclude=MISSING, + exclude=DEFAULT_EXCLUDE, verbose=0, output_format=OutputFormat.TEXT, ) @@ -601,7 +601,7 @@ async def test_check_calls_finalize_on_error(self, monkeypatch: pytest.MonkeyPat command="check", upstream=(URL("https://example.com"),), to=pathlib.Path("nonexistent.toml"), - exclude=MISSING, + exclude=DEFAULT_EXCLUDE, verbose=0, output_format=OutputFormat.TEXT, ) diff --git a/tests/test_serialization.py b/tests/test_serialization.py index c60591f..2025963 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -8,7 +8,7 @@ from httpx import URL from ruff_sync.cli import Arguments -from ruff_sync.constants import MISSING, MissingType +from ruff_sync.constants import DEFAULT_BRANCH, DEFAULT_EXCLUDE from ruff_sync.core import pull, serialize_ruff_sync_config @@ -40,36 +40,19 @@ def test_serialize_ruff_sync_config_basic(): assert "pre-commit-version-sync = true" in s -def test_serialize_ruff_sync_config_pre_commit_false(): - """Explicitly passing pre_commit=False must serialize as false, not omit the key.""" +def test_serialize_ruff_sync_config_pre_commit_default_skipped(): + """When pre_commit is False (the default), the key must be absent from the serialized config.""" doc = tomlkit.document() args = Arguments( command="pull", upstream=(URL("https://example.com/repo/pyproject.toml"),), to=pathlib.Path(), - exclude=MISSING, + exclude=DEFAULT_EXCLUDE, verbose=0, pre_commit=False, ) serialize_ruff_sync_config(doc, args) - s = doc.as_string() - assert "pre-commit-version-sync = false" in s - - -def test_serialize_ruff_sync_config_pre_commit_missing(): - """When pre_commit is MISSING the key must be absent from the serialized config.""" - doc = tomlkit.document() - args = Arguments( - command="pull", - upstream=(URL("https://example.com/repo/pyproject.toml"),), - to=pathlib.Path(), - exclude=MISSING, - verbose=0, - pre_commit=MISSING, - ) - serialize_ruff_sync_config(doc, args) - s = doc.as_string() assert "pre-commit-version-sync" not in s @@ -119,7 +102,7 @@ def test_serialize_ruff_sync_config_preserves_existing(): command="pull", upstream=(URL("https://example.com/repo/pyproject.toml"),), to=pathlib.Path(), - exclude=MISSING, + exclude=DEFAULT_EXCLUDE, verbose=0, ) @@ -165,7 +148,7 @@ def test_get_or_create_ruff_sync_table_non_table_ruff_sync(): command="pull", upstream=(URL("https://example.com/repo/pyproject.toml"),), to=pathlib.Path(), - exclude=MISSING, + exclude=DEFAULT_EXCLUDE, verbose=0, ) from ruff_sync.core import serialize_ruff_sync_config @@ -181,11 +164,11 @@ def test_serialize_ruff_sync_config_omits_defaults(): command="pull", upstream=(URL("https://example.com/repo/pyproject.toml"),), to=pathlib.Path(), - exclude=MISSING, + exclude=DEFAULT_EXCLUDE, verbose=0, - branch=MISSING, - path=MISSING, - pre_commit=MISSING, + branch=DEFAULT_BRANCH, + path=None, + pre_commit=False, save=True, ) serialize_ruff_sync_config(doc, args) @@ -205,7 +188,7 @@ def test_serialize_ruff_sync_config_exclude_default_skipped(): command="pull", upstream=(URL("https://example.com/repo/pyproject.toml"),), to=pathlib.Path(), - exclude=MISSING, + exclude=DEFAULT_EXCLUDE, verbose=0, ) serialize_ruff_sync_config(doc, args) @@ -224,7 +207,7 @@ def test_serialize_ruff_sync_config_mixed_credentials(caplog: pytest.LogCaptureF URL("https://user:pass@example.com/repo/pyproject.toml"), ), to=pathlib.Path(), - exclude=MISSING, + exclude=DEFAULT_EXCLUDE, verbose=0, ) @@ -242,7 +225,7 @@ def test_serialize_ruff_sync_config_skip_credentials(caplog: pytest.LogCaptureFi command="pull", upstream=(URL("https://user:pass@example.com/repo/pyproject.toml"),), to=pathlib.Path(), - exclude=MISSING, + exclude=DEFAULT_EXCLUDE, verbose=0, branch="main", path="", @@ -267,7 +250,7 @@ def test_serialize_ruff_sync_config_multiple_upstreams(): URL("https://example.com/repo2/pyproject.toml"), ), to=pathlib.Path(), - exclude=MISSING, + exclude=DEFAULT_EXCLUDE, verbose=0, ) serialize_ruff_sync_config(doc, args) @@ -285,15 +268,15 @@ def test_serialize_ruff_sync_config_multiple_upstreams(): # baseline: init+save with explicit pre_commit=True triggers sync (True, None, True, True, True), # save without init still writes [tool.ruff-sync] but does not init hooks - (False, True, MISSING, False, True), - # when pre_commit is MISSING, sync_pre_commit is never called - (True, None, MISSING, False, True), - # when pre_commit is False, sync_pre_commit is not called + (False, True, False, False, True), + # neither init nor save is truthy: no [tool.ruff-sync] section should appear + (True, None, False, False, True), + # neither init nor save is truthy: no [tool.ruff-sync] section should appear (True, None, False, False, True), # init with explicit --no-save should not serialize [tool.ruff-sync] - (True, False, MISSING, False, False), + (True, False, False, False, False), # neither init nor save is truthy: no [tool.ruff-sync] section should appear - (False, None, MISSING, False, False), + (False, None, False, False, False), ], ) @pytest.mark.asyncio @@ -301,7 +284,7 @@ async def test_pull_logging_and_serialization_triggers( monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, tmp_path: pathlib.Path, - case: tuple[bool, bool | None, bool | MissingType, bool, bool], + case: tuple[bool, bool | None, bool, bool, bool], ) -> None: init, save, pre_commit, expect_sync_pre_commit, expect_save = case from ruff_sync import core From d76e1c116a8392280bda905d0568348af5170e52 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sat, 4 Apr 2026 12:04:08 -0400 Subject: [PATCH 14/19] remove output_format --- src/ruff_sync/cli.py | 5 ++++- src/ruff_sync/constants.py | 13 ++++--------- tests/test_ci_validation.py | 12 +++++++++++- tests/test_constants.py | 17 ++++++++--------- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/ruff_sync/cli.py b/src/ruff_sync/cli.py index 17b2484..820208e 100644 --- a/src/ruff_sync/cli.py +++ b/src/ruff_sync/cli.py @@ -426,7 +426,10 @@ def _resolve_output_format(args: CLIArguments, config: Config) -> OutputFormat: try: fmt = OutputFormat(fmt_str) except ValueError: - LOGGER.warning(f"Unknown output format in config: {fmt_str}") + valid_formats = ", ".join(f.value for f in OutputFormat) + LOGGER.warning( + f"Unknown output format in config: {fmt_str}. Valid values: {valid_formats}" + ) else: LOGGER.info(f"πŸ“Š Using output format from [tool.ruff-sync]: {fmt}") return fmt diff --git a/src/ruff_sync/constants.py b/src/ruff_sync/constants.py index 8b3c168..7db655f 100644 --- a/src/ruff_sync/constants.py +++ b/src/ruff_sync/constants.py @@ -65,25 +65,21 @@ def resolve_defaults( branch: str | MissingType, path: str | MissingType, exclude: Iterable[str] | MissingType, - output_format: OutputFormat | MissingType = MISSING, -) -> tuple[str, str | None, Iterable[str], OutputFormat]: +) -> tuple[str, str | None, Iterable[str]]: """Resolve MISSING sentinel values to their effective defaults. This is the single source of truth for MISSING β†’ default resolution across - both ``cli.main`` and ``core._merge_multiple_upstreams``, keeping the two - layers in sync without introducing a circular dependency between them. + the CLI and internal logic, keeping the layers in sync. Args: branch: The resolved branch value or ``MISSING``. path: The resolved path value or ``MISSING``. exclude: The resolved exclude iterable or ``MISSING``. - output_format: The resolved output format or ``MISSING``. Returns: A ``(branch, path, exclude)`` tuple with defaults applied. ``path`` is normalised to ``None`` (not ``""``) so callers can forward - it directly to :func:`~ruff_sync.core.resolve_raw_url` and - :func:`~ruff_sync.core.fetch_upstreams_concurrently`. + it directly to :func:`~ruff_sync.core.resolve_raw_url`. """ eff_branch = branch if branch is not MISSING else DEFAULT_BRANCH raw_path = path if path is not MISSING else DEFAULT_PATH @@ -91,5 +87,4 @@ def resolve_defaults( # but explicit None is the canonical "root directory" sentinel. eff_path: str | None = raw_path or None eff_exclude = exclude if exclude is not MISSING else DEFAULT_EXCLUDE - eff_output_format = output_format if output_format is not MISSING else OutputFormat.TEXT - return eff_branch, eff_path, eff_exclude, eff_output_format + return eff_branch, eff_path, eff_exclude diff --git a/tests/test_ci_validation.py b/tests/test_ci_validation.py index b40fad5..c659193 100644 --- a/tests/test_ci_validation.py +++ b/tests/test_ci_validation.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING, Any import pytest @@ -149,6 +150,7 @@ def test_validate_ci_output_format( ) def test_resolve_output_format( monkeypatch: MonkeyPatch, + caplog: pytest.LogCaptureFixture, env_vars: dict[str, str], cli_args: dict[str, Any], config: Config, @@ -173,7 +175,15 @@ def __init__(self, **kwargs: Any) -> None: args: CLIArguments = MockArgs(**cli_args) # type: ignore[assignment] - assert _resolve_output_format(args, config) == expected + with caplog.at_level(logging.WARNING): + assert _resolve_output_format(args, config) == expected + + # Only the invalid config case should produce the warning + if config.get("output_format") == "invalid": + assert "Unknown output format in config" in caplog.text + assert "Valid values:" in caplog.text + else: + assert "Unknown output format in config" not in caplog.text def test_main_resolves_output_format(monkeypatch: MonkeyPatch) -> None: diff --git a/tests/test_constants.py b/tests/test_constants.py index 3bd2955..2f14a09 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -10,17 +10,16 @@ def test_resolve_defaults_all_missing(): """Verify that MISSING for all args resolves to project defaults.""" - branch, path, exclude, output_format = resolve_defaults(MISSING, MISSING, MISSING) + branch, path, exclude = resolve_defaults(MISSING, MISSING, MISSING) assert branch == DEFAULT_BRANCH assert path is None # DEFAULT_PATH ("") is normalized to None assert exclude == DEFAULT_EXCLUDE - assert output_format == "text" # Default format def test_resolve_defaults_passthrough(): """Verify that non-MISSING values are passed through unchanged.""" - branch, path, exclude, _ = resolve_defaults("develop", "src", ["rule1"]) + branch, path, exclude = resolve_defaults("develop", "src", ["rule1"]) assert branch == "develop" assert path == "src" @@ -30,19 +29,19 @@ def test_resolve_defaults_passthrough(): def test_resolve_defaults_mixed(): """Verify mixed combinations of MISSING and explicit values.""" # Case: branch is MISSING, others are explicit - branch, path, exclude, _ = resolve_defaults(MISSING, "subdir", ["exclude1"]) + branch, path, exclude = resolve_defaults(MISSING, "subdir", ["exclude1"]) assert branch == DEFAULT_BRANCH assert path == "subdir" assert exclude == ["exclude1"] # Case: path is MISSING, others are explicit - branch, path, exclude, _ = resolve_defaults("feat/branch", MISSING, ["exclude2"]) + branch, path, exclude = resolve_defaults("feat/branch", MISSING, ["exclude2"]) assert branch == "feat/branch" assert path is None # MISSING normalized to None assert exclude == ["exclude2"] # Case: exclude is MISSING, others are explicit - branch, path, exclude, _ = resolve_defaults("main", "root", MISSING) + branch, path, exclude = resolve_defaults("main", "root", MISSING) assert branch == "main" assert path == "root" assert exclude == DEFAULT_EXCLUDE @@ -51,13 +50,13 @@ def test_resolve_defaults_mixed(): def test_resolve_defaults_path_normalization(): """Verify that empty string paths are normalized to None.""" # Explicit empty string - _, path, _, _ = resolve_defaults("main", "", MISSING) + _, path, _ = resolve_defaults("main", "", MISSING) assert path is None # MISSING (which is DEFAULT_PATH which is "") - _, path, _, _ = resolve_defaults("main", MISSING, MISSING) + _, path, _ = resolve_defaults("main", MISSING, MISSING) assert path is None # Non-empty string remains - _, path, _, _ = resolve_defaults("main", "backend", MISSING) + _, path, _ = resolve_defaults("main", "backend", MISSING) assert path == "backend" From cd7435db72b59d8cd27cd81c1f3261c970924052 Mon Sep 17 00:00:00 2001 From: Gabriel G Date: Sat, 4 Apr 2026 12:15:24 -0400 Subject: [PATCH 15/19] update project details --- .agents/TESTING.md | 2 +- AGENTS.md | 32 +++++++++++++++++++++++++------- CONTRIBUTING.md | 1 + 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/.agents/TESTING.md b/.agents/TESTING.md index 341100a..6f980d2 100644 --- a/.agents/TESTING.md +++ b/.agents/TESTING.md @@ -130,6 +130,6 @@ def test_my_edge_case(): ## 6. Code Coverage -We target **high coverage** for `ruff_sync.py`. +We target **high coverage** for `src/ruff_sync/`. - Run coverage locally: `uv run coverage run -m pytest -vv && uv run coverage report` - New features MUST include unit tests in `tests/test_basic.py` or specialized files like `tests/test_whitespace.py` if they involve formatting logic. diff --git a/AGENTS.md b/AGENTS.md index 77b6295..b235c55 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ gh label list # See available labels - **Python** β‰₯ 3.10 (target version `py310`) - **Package Manager**: [uv](https://docs.astral.sh/uv/) β€” Use `uv run ` for all executions to ensure the correct environment. -- **Linter / Formatter**: [Ruff](https://docs.astral.sh/ruff/) (`^0.15.0`) +- **Linter / Formatter**: [Ruff](https://docs.astral.sh/ruff/) (`>=0.15.0`) - **Type Checker**: [mypy](https://mypy-lang.org/) (strict mode) - **Test Framework**: [pytest](https://docs.pytest.org/) with `pytest-asyncio`, `respx`, `pyfakefs` (See [Testing Standards](.agents/TESTING.md)) - **Coverage**: `coverage` + Codecov @@ -67,18 +67,35 @@ gh label list # See available labels ci-integration.md src/ruff_sync/ # The application source __init__.py # Public API - cli.py # CLI, merging logic, HTTP - __main__.py # -m support -tasks.py # Invoke tasks: lint, fmt, type-check, deps, new-case + __main__.py # CLI entry point (`python -m ruff_sync`) + cli.py # CLI argparse definition and orchestration + constants.py # Project-wide constants and default values + core.py # Core logic for merging, syncing, and repository handling + formatters.py # Logic for CLI output formatting (GitHub, GitLab, etc.) + pre_commit.py # Support for pre-commit hook generation and validation +tasks.py # Invoke tasks: lint, fmt, type-check, deps, new-case, release pyproject.toml # Project config, dependencies, ruff/mypy settings tests/ + conftest.py # Shared pytest fixtures (mocking, temp dirs) ruff.toml # Test-specific ruff overrides (extends ../pyproject.toml) test_basic.py # Unit tests for core functions + test_check.py # Tests for --check mode and drift detection + test_ci_integration.py # CI-specific environment tests + test_ci_validation.py # Environment variable and CI output detection tests + test_config_validation.py # Validation of local configuration + test_constants.py # Tests for internal constants and sentinels test_corner_cases.py # Edge case tests for TOML merge logic - test_whitespace.py # Tests for whitespace/comment preservation during merge + test_deprecation.py # Tests for handling of deprecated flags/settings test_e2e.py # End-to-end tests using lifecycle TOML fixtures + test_formatters.py # Serialization and formatting tests + test_git_fetch.py # Mocked git repository fetching tests + test_pre_commit.py # Pre-commit hook generation and sync tests test_project.py # Tests that validate project config consistency - test_toml_operations.py # Tests for low-level TOML operations + test_scaffold.py # Tests for the new-case scaffold task + test_serialization.py # Tests for tomlkit serialization edge cases + test_toml_operations.py # Tests for low-level TOML operations + test_url_handling.py # Tests for GitHub and GitLab URL parsing + test_whitespace.py # Tests for whitespace/comment preservation during merge lifecycle_tomls/ # TOML fixture files (*_initial.toml, *_upstream.toml, *_final.toml) ``` @@ -113,7 +130,7 @@ Use this to make informed decisions rather than blindly suppressing rules. uv run ruff format . ``` -- Line length: **90** characters. +- Line length: **100** characters. - Docstring code formatting is enabled (`docstring-code-format = true`). - Preview formatting features are enabled. @@ -207,6 +224,7 @@ Defined in `tasks.py`. **ALWAYS** run these through uv: `uv run invoke ` | `deps` | `sync` | Sync dependencies with uv | | `new-case` | `new-lifecycle-tomls` | Scaffold lifecycle TOML fixtures | | `docs` | | Build or serve documentation | +| `release` | | Tag and create a GitHub release | ## CI diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec0383a..d714893 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,6 +51,7 @@ The project uses a `src` layout. All source code lives in `src/ruff_sync/`. Deve | `uv run invoke deps` | `sync` | Sync dependencies with uv | | `uv run invoke new-case` | `new-lifecycle-tomls` | Scaffold lifecycle TOML test fixtures | | `uv run invoke docs` | | Build or serve documentation | +| `uv run invoke release` | | Tag and create a GitHub release | --- From 61091b6da29595c5e8ddf2e715bfbda76ed58754 Mon Sep 17 00:00:00 2001 From: Gabriel G Date: Sat, 4 Apr 2026 12:22:52 -0400 Subject: [PATCH 16/19] ConfKey --- src/ruff_sync/cli.py | 26 +++++++++++++++----------- src/ruff_sync/constants.py | 26 ++++++++++++++++++++++++++ src/ruff_sync/core.py | 13 +++++++------ tests/test_config_validation.py | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/src/ruff_sync/cli.py b/src/ruff_sync/cli.py index 820208e..7b1514f 100644 --- a/src/ruff_sync/cli.py +++ b/src/ruff_sync/cli.py @@ -31,6 +31,7 @@ DEFAULT_BRANCH, DEFAULT_EXCLUDE, DEFAULT_PATH, + ConfKey, OutputFormat, ) from ruff_sync.core import ( @@ -175,21 +176,24 @@ def get_config( arg_norm = arg.replace("-", "_") # Handle aliases for pre-commit - if arg_norm in ("pre_commit_sync", "pre_commit"): - arg_norm = "pre_commit_version_sync" + if arg_norm in ( + ConfKey.PRE_COMMIT_SYNC_LEGACY, + ConfKey.PRE_COMMIT.replace("-", "_"), + ): + arg_norm = ConfKey.PRE_COMMIT_VERSION_SYNC.replace("-", "_") if arg_norm in allowed_keys: - if arg_norm == "source": + if arg_norm == ConfKey.SOURCE: LOGGER.warning( - "DeprecationWarning: [tool.ruff-sync] 'source' is deprecated. " - "Use 'to' instead." + f"DeprecationWarning: [tool.ruff-sync] '{ConfKey.SOURCE}' " + f"is deprecated. Use '{ConfKey.TO}' instead." ) cfg_result[arg_norm] = value # type: ignore[literal-required] else: LOGGER.warning(f"Unknown ruff-sync configuration: {arg}") # Ensure 'to' is populated if 'source' was used - if "source" in cfg_result and "to" not in cfg_result: - cfg_result["to"] = cfg_result["source"] + if ConfKey.SOURCE in cfg_result and ConfKey.TO not in cfg_result: + cfg_result[ConfKey.TO] = cfg_result[ConfKey.SOURCE] # type: ignore[literal-required] return cfg_result @@ -374,8 +378,7 @@ def _resolve_exclude(args: CLIArguments, config: Config) -> Iterable[str]: Returns the CLI value if provided, otherwise the value from ``[tool.ruff-sync].exclude`` in the config. If neither is set, - returns ``MISSING`` so that :func:`~ruff_sync.constants.resolve_defaults` - can apply the ``DEFAULT_EXCLUDE`` set downstream. + returns ``DEFAULT_EXCLUDE``. """ if args.exclude is not None: return args.exclude @@ -486,8 +489,9 @@ def _resolve_pre_commit(args: CLIArguments, config: Config) -> bool: """Resolve pre-commit sync setting from CLI or config.""" if args.pre_commit is not None: return args.pre_commit - if "pre_commit_version_sync" in config: - val = config.get("pre_commit_version_sync") + pre_commit_key = ConfKey.PRE_COMMIT_VERSION_SYNC.replace("-", "_") + if pre_commit_key in config: + val = config.get(pre_commit_key) return bool(val) return False diff --git a/src/ruff_sync/constants.py b/src/ruff_sync/constants.py index 7db655f..dc975f5 100644 --- a/src/ruff_sync/constants.py +++ b/src/ruff_sync/constants.py @@ -15,6 +15,7 @@ "DEFAULT_EXCLUDE", "DEFAULT_PATH", "MISSING", + "ConfKey", "MissingType", "OutputFormat", "resolve_defaults", @@ -61,6 +62,31 @@ def __str__(self) -> str: return self.value +class ConfKey: + """Centralized configuration keys for [tool.ruff-sync]. + + These are the canonical names used in the pyproject.toml configuration file. + """ + + UPSTREAM: Final[str] = "upstream" + TO: Final[str] = "to" + EXCLUDE: Final[str] = "exclude" + BRANCH: Final[str] = "branch" + PATH: Final[str] = "path" + PRE_COMMIT_VERSION_SYNC: Final[str] = "pre-commit-version-sync" + OUTPUT_FORMAT: Final[str] = "output-format" + SEMANTIC: Final[str] = "semantic" + DIFF: Final[str] = "diff" + INIT: Final[str] = "init" + SAVE: Final[str] = "save" + VERBOSE: Final[str] = "verbose" + + # Legacy / Alias Keys + SOURCE: Final[str] = "source" # Legacy for 'to' + PRE_COMMIT: Final[str] = "pre-commit" # Legacy for 'pre-commit-version-sync' + PRE_COMMIT_SYNC_LEGACY: Final[str] = "pre_commit_sync" # Legacy for 'pre-commit-version-sync' + + def resolve_defaults( branch: str | MissingType, path: str | MissingType, diff --git a/src/ruff_sync/core.py b/src/ruff_sync/core.py index ab28d28..22faa45 100644 --- a/src/ruff_sync/core.py +++ b/src/ruff_sync/core.py @@ -37,6 +37,7 @@ DEFAULT_BRANCH, DEFAULT_EXCLUDE, DEFAULT_PATH, + ConfKey, ) from ruff_sync.formatters import ResultFormatter, get_formatter from ruff_sync.pre_commit import sync_pre_commit @@ -1134,13 +1135,13 @@ def serialize_ruff_sync_config(doc: TOMLDocument, args: Arguments) -> None: # TODO: Consider only saving upstream if it differs from existing config if len(args.upstream) == 1: - ruff_sync_table["upstream"] = str(args.upstream[0]) + ruff_sync_table[ConfKey.UPSTREAM] = str(args.upstream[0]) else: urls_array = tomlkit.array() urls_array.multiline(True) for url in args.upstream: urls_array.append(str(url)) - ruff_sync_table["upstream"] = urls_array + ruff_sync_table[ConfKey.UPSTREAM] = urls_array # Normalize excludes and de-duplicate while preserving order. # Only compute and persist excludes when explicitly provided so that @@ -1150,16 +1151,16 @@ def serialize_ruff_sync_config(doc: TOMLDocument, args: Arguments) -> None: exclude_array = tomlkit.array() for ex in normalized_excludes: exclude_array.append(ex) - ruff_sync_table["exclude"] = exclude_array + ruff_sync_table[ConfKey.EXCLUDE] = exclude_array if args.branch != DEFAULT_BRANCH: - ruff_sync_table["branch"] = args.branch + ruff_sync_table[ConfKey.BRANCH] = args.branch if args.path != DEFAULT_PATH and args.path is not None: - ruff_sync_table["path"] = args.path + ruff_sync_table[ConfKey.PATH] = args.path if args.pre_commit: - ruff_sync_table["pre-commit-version-sync"] = args.pre_commit + ruff_sync_table[ConfKey.PRE_COMMIT_VERSION_SYNC] = args.pre_commit async def pull( diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py index 31f9199..7144fc6 100644 --- a/tests/test_config_validation.py +++ b/tests/test_config_validation.py @@ -88,5 +88,38 @@ def test_get_config_passes_allowed_keys( assert config["branch"] == "develop" +def test_get_config_key_normalization( + tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture, clean_config_cache: None +): + """Verify that both dashed and legacy keys are normalized correctly.""" + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + """ +[tool.ruff-sync] +# Canonical dashed key +pre-commit-version-sync = true +# Legacy underscored key (alias) +pre_commit_sync = false +# Another legacy alias +pre-commit = true +# Canonical with dashes +output-format = "github" +""" + ) + # Note: in a real TOML, the last value for the same normalized key WOULD win + # because they all map to 'pre_commit_version_sync'. + # But TOML itself doesn't allow duplicate keys if they have the same name. + # Here they have different names in TOML but map to the same name in Python. + + config = get_config(tmp_path) + + # 'pre-commit-version-sync' -> 'pre_commit_version_sync' + # 'pre_commit_sync' -> 'pre_commit_version_sync' + # 'pre-commit' -> 'pre_commit_version_sync' + # The last one in the file wins if they map to the same key. + assert config["pre_commit_version_sync"] is True + assert config["output_format"] == "github" + + if __name__ == "__main__": pytest.main([__file__, "-vv"]) From f1f277788e84171aa50489c8c539b069c51551c0 Mon Sep 17 00:00:00 2001 From: Gabriel G Date: Sat, 4 Apr 2026 12:30:51 -0400 Subject: [PATCH 17/19] make ConfKey an enum --- src/ruff_sync/cli.py | 51 ++++++++++++++------------- src/ruff_sync/constants.py | 70 +++++++++++++++++++++++++++++--------- 2 files changed, 82 insertions(+), 39 deletions(-) diff --git a/src/ruff_sync/cli.py b/src/ruff_sync/cli.py index 7b1514f..6c48853 100644 --- a/src/ruff_sync/cli.py +++ b/src/ruff_sync/cli.py @@ -172,28 +172,32 @@ def get_config( config = toml.get("tool", {}).get("ruff-sync") if config: allowed_keys = set(Config.__annotations__.keys()) - for arg, value in config.items(): - arg_norm = arg.replace("-", "_") - - # Handle aliases for pre-commit - if arg_norm in ( - ConfKey.PRE_COMMIT_SYNC_LEGACY, - ConfKey.PRE_COMMIT.replace("-", "_"), - ): - arg_norm = ConfKey.PRE_COMMIT_VERSION_SYNC.replace("-", "_") - - if arg_norm in allowed_keys: - if arg_norm == ConfKey.SOURCE: - LOGGER.warning( - f"DeprecationWarning: [tool.ruff-sync] '{ConfKey.SOURCE}' " - f"is deprecated. Use '{ConfKey.TO}' instead." - ) - cfg_result[arg_norm] = value # type: ignore[literal-required] + for raw_key, value in config.items(): + # Check for legacy 'source' key to emit deprecation warning + if raw_key.replace("-", "_") == ConfKey.to_attr(ConfKey.SOURCE): + LOGGER.warning( + f"DeprecationWarning: [tool.ruff-sync] '{raw_key}' " + f"is deprecated. Use '{ConfKey.TO}' instead." + ) + + # Map legacy names (source, pre_commit_sync) to canonical + # (to, pre-commit-version-sync) + canonical_key = ConfKey.get_canonical(raw_key) + + # Normalize TOML key (dashes) to internal Python attribute name (underscores) + # e.g. "pre-commit-version-sync" -> "pre_commit_version_sync" + arg_attr = ConfKey.to_attr(canonical_key) + + if arg_attr in allowed_keys: + cfg_result[arg_attr] = value # type: ignore[literal-required] else: - LOGGER.warning(f"Unknown ruff-sync configuration: {arg}") + LOGGER.warning(f"Unknown ruff-sync configuration: {raw_key}") + # Ensure 'to' is populated if 'source' was used - if ConfKey.SOURCE in cfg_result and ConfKey.TO not in cfg_result: - cfg_result[ConfKey.TO] = cfg_result[ConfKey.SOURCE] # type: ignore[literal-required] + to_attr = ConfKey.to_attr(ConfKey.TO) + source_attr = ConfKey.to_attr(ConfKey.SOURCE) + if source_attr in cfg_result and to_attr not in cfg_result: + cfg_result[to_attr] = cfg_result[source_attr] # type: ignore[literal-required] return cfg_result @@ -489,9 +493,10 @@ def _resolve_pre_commit(args: CLIArguments, config: Config) -> bool: """Resolve pre-commit sync setting from CLI or config.""" if args.pre_commit is not None: return args.pre_commit - pre_commit_key = ConfKey.PRE_COMMIT_VERSION_SYNC.replace("-", "_") - if pre_commit_key in config: - val = config.get(pre_commit_key) + + pre_commit_attr = ConfKey.to_attr(ConfKey.PRE_COMMIT_VERSION_SYNC) + if pre_commit_attr in config: + val = config.get(pre_commit_attr) return bool(val) return False diff --git a/src/ruff_sync/constants.py b/src/ruff_sync/constants.py index dc975f5..fa58c5f 100644 --- a/src/ruff_sync/constants.py +++ b/src/ruff_sync/constants.py @@ -62,29 +62,67 @@ def __str__(self) -> str: return self.value -class ConfKey: +@enum.unique +class ConfKey(str, enum.Enum): """Centralized configuration keys for [tool.ruff-sync]. These are the canonical names used in the pyproject.toml configuration file. """ - UPSTREAM: Final[str] = "upstream" - TO: Final[str] = "to" - EXCLUDE: Final[str] = "exclude" - BRANCH: Final[str] = "branch" - PATH: Final[str] = "path" - PRE_COMMIT_VERSION_SYNC: Final[str] = "pre-commit-version-sync" - OUTPUT_FORMAT: Final[str] = "output-format" - SEMANTIC: Final[str] = "semantic" - DIFF: Final[str] = "diff" - INIT: Final[str] = "init" - SAVE: Final[str] = "save" - VERBOSE: Final[str] = "verbose" + UPSTREAM = "upstream" + TO = "to" + EXCLUDE = "exclude" + BRANCH = "branch" + PATH = "path" + PRE_COMMIT_VERSION_SYNC = "pre-commit-version-sync" + OUTPUT_FORMAT = "output-format" + SEMANTIC = "semantic" + DIFF = "diff" + INIT = "init" + SAVE = "save" + VERBOSE = "verbose" # Legacy / Alias Keys - SOURCE: Final[str] = "source" # Legacy for 'to' - PRE_COMMIT: Final[str] = "pre-commit" # Legacy for 'pre-commit-version-sync' - PRE_COMMIT_SYNC_LEGACY: Final[str] = "pre_commit_sync" # Legacy for 'pre-commit-version-sync' + SOURCE = "source" # Legacy for 'to' + PRE_COMMIT = "pre-commit" # Legacy for 'pre-commit-version-sync' + PRE_COMMIT_SYNC_LEGACY = "pre_commit_sync" # Legacy for 'pre-commit-version-sync' + + @override + def __str__(self) -> str: + """Return the string value for TOML keys.""" + return self.value + + @classmethod + def to_attr(cls, key: str | ConfKey) -> str: + """Normalize a configuration key to its Python attribute name (underscore).""" + return str(key).replace("-", "_") + + @classmethod + def get_canonical(cls, key: str) -> str: + """Map legacy or aliased configuration keys to their canonical names. + + Args: + key: The raw key from the configuration file. + + Returns: + The canonical ConfKey name (still as a string for use in logic). + """ + # Normalize to underscores first for easy alias checking + norm_key = key.replace("-", "_") + + # Handle aliases for 'to' + if norm_key == cls.SOURCE.replace("-", "_"): + return cls.TO.value + + # Handle aliases for 'pre-commit-version-sync' + if norm_key in ( + cls.PRE_COMMIT_SYNC_LEGACY.value, + cls.PRE_COMMIT.replace("-", "_"), + ): + return cls.PRE_COMMIT_VERSION_SYNC.value + + # Return the original key (even if unknown, let 'allowed_keys' handle it) + return key def resolve_defaults( From f141198f18efa6ceff73388e35cea036cf0ee488 Mon Sep 17 00:00:00 2001 From: Gabriel G Date: Sat, 4 Apr 2026 12:34:39 -0400 Subject: [PATCH 18/19] more consistent norming --- src/ruff_sync/constants.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ruff_sync/constants.py b/src/ruff_sync/constants.py index fa58c5f..b7dfb19 100644 --- a/src/ruff_sync/constants.py +++ b/src/ruff_sync/constants.py @@ -85,7 +85,7 @@ class ConfKey(str, enum.Enum): # Legacy / Alias Keys SOURCE = "source" # Legacy for 'to' PRE_COMMIT = "pre-commit" # Legacy for 'pre-commit-version-sync' - PRE_COMMIT_SYNC_LEGACY = "pre_commit_sync" # Legacy for 'pre-commit-version-sync' + PRE_COMMIT_SYNC_LEGACY = "pre-commit-sync" # Legacy for 'pre-commit-version-sync' @override def __str__(self) -> str: @@ -107,17 +107,17 @@ def get_canonical(cls, key: str) -> str: Returns: The canonical ConfKey name (still as a string for use in logic). """ - # Normalize to underscores first for easy alias checking - norm_key = key.replace("-", "_") + # Normalize the input key to underscores for robust alias matching + norm_key = cls.to_attr(key) # Handle aliases for 'to' - if norm_key == cls.SOURCE.replace("-", "_"): + if norm_key == cls.to_attr(cls.SOURCE): return cls.TO.value # Handle aliases for 'pre-commit-version-sync' if norm_key in ( - cls.PRE_COMMIT_SYNC_LEGACY.value, - cls.PRE_COMMIT.replace("-", "_"), + cls.to_attr(cls.PRE_COMMIT_SYNC_LEGACY), + cls.to_attr(cls.PRE_COMMIT), ): return cls.PRE_COMMIT_VERSION_SYNC.value From 07652e85f9a548fe595e554f032c06a81714a1f6 Mon Sep 17 00:00:00 2001 From: Gabriel G Date: Sat, 4 Apr 2026 12:44:44 -0400 Subject: [PATCH 19/19] test cli precedence --- tests/test_ci_validation.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_ci_validation.py b/tests/test_ci_validation.py index c659193..1ce1022 100644 --- a/tests/test_ci_validation.py +++ b/tests/test_ci_validation.py @@ -90,13 +90,20 @@ def test_validate_ci_output_format( @pytest.mark.parametrize( ("env_vars", "cli_args", "config", "expected"), [ - # CLI takes precedence + # CLI takes precedence over auto-detection ( {"GITHUB_ACTIONS": "true"}, {"output_format": OutputFormat.JSON}, {}, OutputFormat.JSON, ), + # CLI takes precedence over config + ( + {"GITHUB_ACTIONS": "true"}, + {"output_format": OutputFormat.JSON}, + {"output_format": "gitlab"}, + OutputFormat.JSON, + ), # Config takes precedence over auto-detection ( {"GITHUB_ACTIONS": "true"},