ruff-sync 0.1.7.dev2__tar.gz → 0.1.9.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/DEPENDENCIES.md +2 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/args_refactor_plan.md +14 -8
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/cli_animation_plan.md +1 -3
- ruff_sync-0.1.9.dev0/.agents/decisions/0004-httpx2-respx-test-aliasing.md +51 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/decisions/README.md +1 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/formatters-architecture.md +7 -6
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/gitlab-reports.md +26 -16
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/issue-102-context.md +12 -2
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/plans/issue-116-config-validation.md +19 -20
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/dirty-equals/SKILL.md +7 -4
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/dirty-equals/references/common-matchers.md +7 -5
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/dirty-equals/references/toml-matching.md +8 -6
- {ruff_sync-0.1.7.dev2/.agents/skills/ruff-sync-usage → ruff_sync-0.1.9.dev0/.agents/skills/ruff-sync}/SKILL.md +1 -1
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/textual/SKILL.md +3 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/textual/references/events.md +3 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/textual/references/testing.md +1 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/textual/references/widgets.md +3 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/type-checking/references/advanced-narrowing.md +4 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/type-checking/references/error-code-lookup.md +10 -6
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/type-checking/references/generics.md +5 -1
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/type-checking/references/naming.md +2 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/type-checking/references/protocol-patterns.md +7 -2
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/type-checking/references/refactoring-patterns.md +4 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/warnings-control/SKILL.md +5 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/tui_design.md +3 -1
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/workflows/update-screenshots.md +1 -1
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.github/workflows/ci.yaml +7 -7
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.github/workflows/complexity.yaml +1 -1
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.pre-commit-config.yaml +1 -1
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/AGENTS.md +9 -7
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/CONTRIBUTING.md +3 -3
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/PKG-INFO +32 -6
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/README.md +28 -1
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/agent-skill.md +10 -6
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/contributing.md +1 -1
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/pyproject.toml +16 -14
- ruff_sync-0.1.9.dev0/src/ruff_sync/.agents/skills/ruff-sync/SKILL.md +147 -0
- ruff_sync-0.1.9.dev0/src/ruff_sync/.agents/skills/ruff-sync/references/ci-integration.md +233 -0
- ruff_sync-0.1.9.dev0/src/ruff_sync/.agents/skills/ruff-sync/references/configuration.md +164 -0
- ruff_sync-0.1.9.dev0/src/ruff_sync/.agents/skills/ruff-sync/references/troubleshooting.md +157 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/config_io.py +1 -1
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/constants.py +3 -3
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/core.py +17 -44
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/types_.py +1 -3
- ruff_sync-0.1.9.dev0/src/sitecustomize.py +20 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tasks.py +5 -5
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/conftest.py +5 -45
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/ruff.toml +2 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_basic.py +42 -19
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_benchmarks.py +1 -1
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_check.py +60 -49
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_config_validation.py +19 -18
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_deprecation.py +9 -8
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_e2e.py +11 -11
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_project.py +2 -2
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_scaffold.py +5 -5
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_serialization.py +1 -1
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_url_handling.py +9 -5
- ruff_sync-0.1.9.dev0/uv.lock +1988 -0
- ruff_sync-0.1.7.dev2/uv.lock +0 -2097
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/TESTING.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/decisions/0001-type-refactoring-strategy.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/decisions/0002-tui-node-ast.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/decisions/0003-argument-resolution-layers.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/doc-fix.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/docs_update_plan.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/plans/issue-100-roadmap-plan.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/ruff.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/adr/SKILL.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/dirty-equals/trigger_eval.json +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/gh-issues/SKILL.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/mike/SKILL.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/mike/references/commands.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/mkdocs-generation/SKILL.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/mkdocs-generation/examples.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/mkdocs-generation/templates/api-reference.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/mkdocs-generation/templates/getting-started.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/mkdocs-generation/templates/index.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/mkdocs-generation/templates/mkdocs.yml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/release-notes-generation/SKILL.md +0 -0
- {ruff_sync-0.1.7.dev2/.agents/skills/ruff-sync-usage → ruff_sync-0.1.9.dev0/.agents/skills/ruff-sync}/references/ci-integration.md +0 -0
- {ruff_sync-0.1.7.dev2/.agents/skills/ruff-sync-usage → ruff_sync-0.1.9.dev0/.agents/skills/ruff-sync}/references/configuration.md +0 -0
- {ruff_sync-0.1.7.dev2/.agents/skills/ruff-sync-usage → ruff_sync-0.1.9.dev0/.agents/skills/ruff-sync}/references/troubleshooting.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/skill-creator/SKILL.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/skill-creator/references/best-practices.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/skill-creator/references/evaluating-skills.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/skill-creator/references/optimizing-descriptions.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/skill-creator/references/quickstart.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/skill-creator/references/sources.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/skill-creator/references/using-scripts.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/skill-creator/scripts/scaffold_skill.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/textual/examples/basic_app.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/textual/examples/reactive_example.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/textual/references/styling.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/type-checking/SKILL.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/type-checking/evals.json +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/type-checking/references/quickstart.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/skills/type-checking/scripts/audit_types.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/tui_requirements.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/tui_rule_browsing.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/tui_rule_browsing_design.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/workflows/add-test-case.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.agents/workflows/update-recordings.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.git-blame-ignore-revs +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.github/dependabot.yml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.github/workflows/codspeed.yaml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.gitignore +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/.pre-commit-hooks.yaml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/LICENSE.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/codecov.yml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/configs/data-science-engineering/ruff.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/configs/fastapi/ruff.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/configs/kitchen-sink/ruff.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/favicon.png +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/github-job-summary.png +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/github-pr-annotation.png +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/inspect-main.png +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/inspect-search.png +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/logo.png +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/recordings/check_drift.gif +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/recordings/check_in_sync.gif +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/recordings/help_overview.gif +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/recordings/init_project.gif +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/recordings/pull_basic.gif +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/recordings/validate_strict.gif +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/ruff_sync_banner.png +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/screenshots/dashboard.svg +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/screenshots/legend_help.svg +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/screenshots/rule_details.svg +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/screenshots/screenshot_sample.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/assets/screenshots/search_omnibox.svg +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/best-practices.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/ci-integration.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/configuration.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/examples/advanced-config.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/examples/basic-config.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/gen_ref_pages.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/index.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/inspect.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/installation.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/overrides/main.html +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/overrides/partials/version_warning.html +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/pre-commit.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/pre-defined-configs.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/troubleshooting.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/url-resolution.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/docs/usage.md +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/mkdocs.yml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/scripts/check_dogfood.sh +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/scripts/generate_tui_screenshots.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/scripts/gitclone_dogfood.sh +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/scripts/pull_dogfood.sh +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/skills-lock.json +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/__init__.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/__main__.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/cli.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/dependencies.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/formatters.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/pre_commit.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/system.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/tui/__init__.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/tui/app.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/tui/constants.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/tui/screens.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/tui/themes.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/tui/types_.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/tui/widgets.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/src/ruff_sync/validation.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tapes/_common.tape +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tapes/check_drift.tape +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tapes/check_in_sync.tape +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tapes/help_overview.tape +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tapes/init_project.tape +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tapes/pull_basic.tape +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tapes/validate_strict.tape +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/__init__.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/multi_upstream_final.toml +1 -1
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/multi_upstream_initial.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/multi_upstream_up1.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/multi_upstream_up2.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/no_changes_final.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/no_changes_initial.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/no_changes_upstream.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/no_dotted_keys_final.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/no_dotted_keys_initial.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/no_dotted_keys_upstream.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/no_ruff_cfg_final.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/no_ruff_cfg_initial.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/no_ruff_cfg_upstream.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/readme_excludes_final.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/readme_excludes_initial.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/readme_excludes_upstream.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/standard_final.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/standard_initial.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/lifecycle_tomls/standard_upstream.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_ci_integration.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_ci_validation.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_config_io.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_constants.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_corner_cases.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_dependencies.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_formatters.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_git_fetch.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_minimal_imports.sh +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_pre_commit.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_rule_logic.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_system.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_toml_operations.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/test_whitespace.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/tui/__init__.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/tui/conftest.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/tui/test_themes.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/tui/test_tui.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/tui/test_tui_types.py +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/w_ruff_sync_cfg/pyproject.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/wo_ruff_cfg/pyproject.toml +0 -0
- {ruff_sync-0.1.7.dev2 → ruff_sync-0.1.9.dev0}/tests/wo_ruff_sync_cfg/pyproject.toml +0 -0
|
@@ -29,10 +29,12 @@ Never import optional dependencies at the top level of a module. All imports mus
|
|
|
29
29
|
def run_tui_feature():
|
|
30
30
|
# 1. First, check availability (fast, lightweight)
|
|
31
31
|
from ruff_sync.dependencies import require_dependency
|
|
32
|
+
|
|
32
33
|
require_dependency("textual", extra_name="tui")
|
|
33
34
|
|
|
34
35
|
# 2. Then, perform local import (delayed expensive cycle)
|
|
35
36
|
from textual.app import App
|
|
37
|
+
|
|
36
38
|
...
|
|
37
39
|
```
|
|
38
40
|
|
|
@@ -96,11 +96,11 @@ class ExecutionArgs(NamedTuple):
|
|
|
96
96
|
semantic: bool
|
|
97
97
|
diff: bool
|
|
98
98
|
init: bool
|
|
99
|
-
pre_commit: bool
|
|
99
|
+
pre_commit: bool # plain bool — MISSING resolved to default
|
|
100
100
|
save: bool | None
|
|
101
101
|
output_format: OutputFormat
|
|
102
|
-
validate: bool
|
|
103
|
-
strict: bool
|
|
102
|
+
validate: bool # plain bool — MISSING resolved to default
|
|
103
|
+
strict: bool # plain bool — MISSING resolved to default
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
> [!NOTE]
|
|
@@ -120,9 +120,9 @@ class Arguments(NamedTuple):
|
|
|
120
120
|
def resolve(self) -> ExecutionArgs:
|
|
121
121
|
"""Resolve all MISSING sentinels to their effective defaults for execution."""
|
|
122
122
|
_, _, _, eff_validate, eff_strict, eff_pre_commit = resolve_defaults(
|
|
123
|
-
MISSING,
|
|
124
|
-
MISSING,
|
|
125
|
-
MISSING,
|
|
123
|
+
MISSING, # branch — already resolved, pass MISSING to skip
|
|
124
|
+
MISSING, # path — already resolved, pass MISSING to skip
|
|
125
|
+
MISSING, # exclude — already resolved, pass MISSING to skip
|
|
126
126
|
self.validate,
|
|
127
127
|
self.strict,
|
|
128
128
|
self.pre_commit,
|
|
@@ -205,11 +205,14 @@ Replace the `resolve_defaults()` call in `Arguments.resolve()` with `resolve_boo
|
|
|
205
205
|
```python
|
|
206
206
|
from ruff_sync.constants import resolve_bool_flags
|
|
207
207
|
|
|
208
|
+
|
|
208
209
|
class Arguments(NamedTuple):
|
|
209
210
|
# ...
|
|
210
211
|
def resolve(self) -> ExecutionArgs:
|
|
211
212
|
eff_validate, eff_strict, eff_pre_commit = resolve_bool_flags(
|
|
212
|
-
self.validate,
|
|
213
|
+
self.validate,
|
|
214
|
+
self.strict,
|
|
215
|
+
self.pre_commit,
|
|
213
216
|
)
|
|
214
217
|
return ExecutionArgs(
|
|
215
218
|
command=self.command,
|
|
@@ -291,7 +294,7 @@ call is actually redundant. However, if the function is also used by code that p
|
|
|
291
294
|
async def _merge_multiple_upstreams(
|
|
292
295
|
target_doc: TOMLDocument,
|
|
293
296
|
is_target_ruff_toml: bool,
|
|
294
|
-
args: ExecutionArgs,
|
|
297
|
+
args: ExecutionArgs, # ← changed from Arguments
|
|
295
298
|
client: httpx.AsyncClient,
|
|
296
299
|
) -> TOMLDocument:
|
|
297
300
|
# No resolve_defaults() needed — args already has plain values
|
|
@@ -363,17 +366,20 @@ Add **new** tests for `resolve_bool_flags()`:
|
|
|
363
366
|
```python
|
|
364
367
|
from ruff_sync.constants import resolve_bool_flags
|
|
365
368
|
|
|
369
|
+
|
|
366
370
|
def test_resolve_bool_flags_all_missing():
|
|
367
371
|
validate, strict, pre_commit = resolve_bool_flags(MISSING, MISSING, MISSING)
|
|
368
372
|
assert validate is False
|
|
369
373
|
assert strict is False
|
|
370
374
|
assert pre_commit is True
|
|
371
375
|
|
|
376
|
+
|
|
372
377
|
def test_resolve_bool_flags_strict_implies_validate():
|
|
373
378
|
validate, strict, pre_commit = resolve_bool_flags(MISSING, True, MISSING)
|
|
374
379
|
assert validate is True
|
|
375
380
|
assert strict is True
|
|
376
381
|
|
|
382
|
+
|
|
377
383
|
def test_resolve_bool_flags_explicit_false():
|
|
378
384
|
validate, strict, pre_commit = resolve_bool_flags(False, False, False)
|
|
379
385
|
assert validate is False
|
|
@@ -441,9 +441,7 @@ def recordings(ctx, tape=None):
|
|
|
441
441
|
tape_files = [tape_file]
|
|
442
442
|
else:
|
|
443
443
|
# Process all tape files except _common.tape
|
|
444
|
-
tape_files = sorted(
|
|
445
|
-
f for f in tapes_dir.glob("*.tape") if not f.name.startswith("_")
|
|
446
|
-
)
|
|
444
|
+
tape_files = sorted(f for f in tapes_dir.glob("*.tape") if not f.name.startswith("_"))
|
|
447
445
|
|
|
448
446
|
if not tape_files:
|
|
449
447
|
print("⚠️ No tape files found in tapes/")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# ADR 0004: httpx2 Integration, Respx Mocking, and Test-Guarded Process Aliasing
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
status: accepted
|
|
5
|
+
date: 2026-07-25
|
|
6
|
+
decider: Kilo59 / Agent
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Context & Motivation for Switching to `httpx2`
|
|
10
|
+
|
|
11
|
+
`ruff-sync` relies on an asynchronous HTTP client to pull upstream Ruff linter configurations from GitHub and GitLab repositories.
|
|
12
|
+
|
|
13
|
+
We migrated from `httpx` to **`httpx2`** based on the official rationale provided by **Pydantic Services**:
|
|
14
|
+
- **Active Stewardship & Security Maintenance**: Due to a significant period of reduced maintenance activity and inactivity on the original `httpx` package, Pydantic Services created `httpx2` as an actively maintained successor to guarantee timely security updates, bug fixes, and continuous maintenance for a critical core library in the Python ecosystem.
|
|
15
|
+
- **Ecosystem Continuity**: The "2" in `httpx2` functions as a versioning marker for this new era of Pydantic Services maintenance rather than a breaking paradigm rewrite, preserving full compatibility while delivering active upstream support.
|
|
16
|
+
- **Strict Typing**: `httpx2` provides first-class typing out of the box, integrating cleanly with our `mypy` strict mode requirements.
|
|
17
|
+
|
|
18
|
+
Previously, HTTP test mocking relied on the third-party `pytest-httpx2` plugin. We needed to clean up our HTTP dependencies, eliminate `pytest-httpx2`, and migrate to standard `respx` (`>=0.23.1`) while ensuring zero side-effects on production execution.
|
|
19
|
+
|
|
20
|
+
## Decision
|
|
21
|
+
|
|
22
|
+
1. **Direct Application Imports**: Application runtime code (`src/ruff_sync/core.py`) imports `httpx2` directly (`import httpx2 as httpx`).
|
|
23
|
+
2. **Elimination of `pytest-httpx2`**: We completely removed `pytest-httpx2` from dependencies due to confusion and transport hook coupling.
|
|
24
|
+
3. **Test-Guarded Process Aliasing (`src/sitecustomize.py`)**: We place `httpx2.alias_httpx()` inside `src/sitecustomize.py` guarded by a runtime check (`any("pytest" in arg for arg in sys.argv) or "PYTEST_CURRENT_TEST" in os.environ`).
|
|
25
|
+
|
|
26
|
+
## Rationale & Technical Tradeoffs
|
|
27
|
+
|
|
28
|
+
### 1. Removal of `pytest-httpx2`
|
|
29
|
+
Beyond introducing transport hook conflicts and entrypoint ordering bugs, `pytest-httpx2` caused developer confusion:
|
|
30
|
+
- `pytest-httpx2` is a wrapper around `respx`, but its name frequently leads developers to the documentation for `pytest-httpx` / `httpx_pytest`.
|
|
31
|
+
- This created confusion around fixture names, routing syntax, and unexpected mocking behavior.
|
|
32
|
+
Eliminating `pytest-httpx2` removes this confusing wrapper layer and standardizes our test suite directly on `respx`.
|
|
33
|
+
|
|
34
|
+
### 2. No Production HTTP Plugin Dependencies
|
|
35
|
+
`ruff-sync` runtime code does **not** depend on third-party `httpcore` or `httpx` plugins in production.
|
|
36
|
+
If our production runtime relied on external plugins that hard-coded `import httpx` or `import httpcore`, we would require process-wide aliasing at production application startup. Because `ruff-sync` manages its HTTP transport directly via `httpx2`, aliasing is only necessary during test suite execution when `respx` is loaded by Pytest.
|
|
37
|
+
|
|
38
|
+
### 3. Packaging Exclusion
|
|
39
|
+
`src/sitecustomize.py` lives outside `src/ruff_sync/` and is strictly excluded from wheel packages by Hatchling (`packages = ["src/ruff_sync"]`). Published `.whl` and `sdist` distributions never include or ship `sitecustomize.py` to end users.
|
|
40
|
+
|
|
41
|
+
### 4. Why Disabling `respx` Autoloading (`-p no:respx`) Is a Gnarly Upstream Problem
|
|
42
|
+
We evaluated disabling `respx` entrypoint autoloading via `addopts = ["-p", "no:respx"]` and manually declaring `pytest_plugins = ["respx"]` in `conftest.py`. However, this reveals a gnarly entrypoint race condition:
|
|
43
|
+
- `respx` registers a `pytest11` entrypoint that imports `httpx`/`httpcore` at plugin discovery time.
|
|
44
|
+
- Disabling `respx` autoloading and re-enabling it via `pytest_plugins` causes `respx` to initialize its `MockRouter` hooks *after* Python module resolution has settled. In end-to-end CLI tests where `httpx2.AsyncClient` is instantiated across sub-threads or CLI entrypoints, `httpx2` creates fresh `httpcore2` transport instances that bypass `respx`'s delayed transport hooks, causing `RESPX: some routes were not called!` errors.
|
|
45
|
+
- **Upstream Resolution Needed**: This conflict needs to be resolved **upstream** — either in `respx` (by deferring transport binding until active router context rather than entrypoint import time) or in `httpx2` (by providing native `httpcore2` transport interceptors for `respx` without requiring `sys.modules` aliasing). Until fixed upstream, `sitecustomize.py` is the only mechanism that forces `alias_httpx()` to run at Python interpreter launch before Pytest's entrypoint scanner initializes `respx`.
|
|
46
|
+
|
|
47
|
+
## References
|
|
48
|
+
|
|
49
|
+
- [ADR Index](./README.md)
|
|
50
|
+
- [httpx2 Migration Guide](https://httpx2.pydantic.dev/migration/)
|
|
51
|
+
- [Issue #202](https://github.com/Kilo59/ruff-sync/issues/202) / [PR #203](https://github.com/Kilo59/ruff-sync/pull/203)
|
|
@@ -7,6 +7,7 @@ This is an internal record of architectural decisions for `ruff-sync`. These doc
|
|
|
7
7
|
| [0001](./0001-type-refactoring-strategy.md) | 2026-04-05 | Accepted | Type Refactoring Strategy |
|
|
8
8
|
| [0002](./0002-tui-node-ast.md) | 2026-04-05 | Accepted | TUI Node AST Architecture |
|
|
9
9
|
| [0003](./0003-argument-resolution-layers.md) | 2026-04-10 | Accepted | Two-Layer Argument Resolution (Transport vs Execution) |
|
|
10
|
+
| [0004](./0004-httpx2-respx-test-aliasing.md) | 2026-07-25 | Accepted | httpx2 Integration, Respx Mocking, and Test-Guarded Process Aliasing |
|
|
10
11
|
|
|
11
12
|
---
|
|
12
13
|
*For instructions on how to create or manage ADRs, see the [ADR Skill](../skills/adr/SKILL.md).*
|
|
@@ -58,8 +58,8 @@ def error(
|
|
|
58
58
|
message: str,
|
|
59
59
|
file_path: pathlib.Path | None = None,
|
|
60
60
|
logger: logging.Logger | None = None,
|
|
61
|
-
check_name: str = "ruff-sync/config-drift",
|
|
62
|
-
drift_key: str | None = None,
|
|
61
|
+
check_name: str = "ruff-sync/config-drift", # machine-readable rule ID
|
|
62
|
+
drift_key: str | None = None, # e.g. "lint.select"
|
|
63
63
|
) -> None: ...
|
|
64
64
|
```
|
|
65
65
|
|
|
@@ -107,11 +107,11 @@ this even when `UpstreamError` or another exception occurs.
|
|
|
107
107
|
1. **Add the format value** to `OutputFormat` in `src/ruff_sync/constants.py`:
|
|
108
108
|
```python
|
|
109
109
|
class OutputFormat(str, enum.Enum):
|
|
110
|
-
TEXT
|
|
111
|
-
JSON
|
|
110
|
+
TEXT = "text"
|
|
111
|
+
JSON = "json"
|
|
112
112
|
GITHUB = "github"
|
|
113
113
|
GITLAB = "gitlab"
|
|
114
|
-
SARIF
|
|
114
|
+
SARIF = "sarif" # new
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
2. **Implement the class** in `src/ruff_sync/formatters.py`. For a
|
|
@@ -151,7 +151,7 @@ fmt = get_formatter(args.output_format)
|
|
|
151
151
|
try:
|
|
152
152
|
...
|
|
153
153
|
finally:
|
|
154
|
-
fmt.finalize()
|
|
154
|
+
fmt.finalize() # no-op for streaming; flushes JSON for accumulating
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
`finalize()` is always called unconditionally — **do not** guard it with
|
|
@@ -168,6 +168,7 @@ is newly introduced or already resolved between branches.
|
|
|
168
168
|
```python
|
|
169
169
|
import hashlib
|
|
170
170
|
|
|
171
|
+
|
|
171
172
|
def _make_fingerprint(upstream_url: str, local_file: str, drift_key: str | None) -> str:
|
|
172
173
|
if drift_key:
|
|
173
174
|
raw = f"ruff-sync:drift:{upstream_url}:{local_file}:{drift_key}"
|
|
@@ -145,6 +145,7 @@ For a "config drift" issue per key (e.g., `lint.select`), a good stable fingerpr
|
|
|
145
145
|
```python
|
|
146
146
|
import hashlib
|
|
147
147
|
|
|
148
|
+
|
|
148
149
|
def make_fingerprint(upstream_url: str, local_file: str, drift_key: str) -> str:
|
|
149
150
|
"""Stable fingerprint for a config drift issue."""
|
|
150
151
|
raw = f"ruff-sync:drift:{upstream_url}:{local_file}:{drift_key}"
|
|
@@ -281,13 +282,15 @@ class GitlabFormatter:
|
|
|
281
282
|
fingerprint: str | None = None,
|
|
282
283
|
) -> None:
|
|
283
284
|
(logger or LOGGER).error(message)
|
|
284
|
-
self._issues.append(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
285
|
+
self._issues.append(
|
|
286
|
+
self._make_issue(
|
|
287
|
+
description=message,
|
|
288
|
+
check_name=check_name,
|
|
289
|
+
severity="major",
|
|
290
|
+
file_path=file_path,
|
|
291
|
+
fingerprint=fingerprint,
|
|
292
|
+
)
|
|
293
|
+
)
|
|
291
294
|
|
|
292
295
|
def warning(
|
|
293
296
|
self,
|
|
@@ -298,13 +301,15 @@ class GitlabFormatter:
|
|
|
298
301
|
fingerprint: str | None = None,
|
|
299
302
|
) -> None:
|
|
300
303
|
(logger or LOGGER).warning(message)
|
|
301
|
-
self._issues.append(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
304
|
+
self._issues.append(
|
|
305
|
+
self._make_issue(
|
|
306
|
+
description=message,
|
|
307
|
+
check_name=check_name,
|
|
308
|
+
severity="minor",
|
|
309
|
+
file_path=file_path,
|
|
310
|
+
fingerprint=fingerprint,
|
|
311
|
+
)
|
|
312
|
+
)
|
|
308
313
|
|
|
309
314
|
def _make_issue(
|
|
310
315
|
self,
|
|
@@ -327,6 +332,7 @@ class GitlabFormatter:
|
|
|
327
332
|
@staticmethod
|
|
328
333
|
def _auto_fingerprint(description: str, path: str) -> str:
|
|
329
334
|
import hashlib
|
|
335
|
+
|
|
330
336
|
raw = f"ruff-sync:drift:{path}:{description}"
|
|
331
337
|
return hashlib.md5(raw.encode()).hexdigest()
|
|
332
338
|
|
|
@@ -370,7 +376,7 @@ class OutputFormat(str, enum.Enum):
|
|
|
370
376
|
TEXT = "text"
|
|
371
377
|
JSON = "json"
|
|
372
378
|
GITHUB = "github"
|
|
373
|
-
GITLAB = "gitlab"
|
|
379
|
+
GITLAB = "gitlab" # NEW
|
|
374
380
|
```
|
|
375
381
|
|
|
376
382
|
Update `get_formatter` in `formatters.py`:
|
|
@@ -530,7 +536,7 @@ fmt = get_formatter(args.output_format)
|
|
|
530
536
|
try:
|
|
531
537
|
...
|
|
532
538
|
finally:
|
|
533
|
-
fmt.finalize()
|
|
539
|
+
fmt.finalize() # Writes the JSON array to stdout (piped to file by CI)
|
|
534
540
|
```
|
|
535
541
|
|
|
536
542
|
**Do not** guard the call with `isinstance` or `hasattr` checks.
|
|
@@ -562,6 +568,7 @@ from unittest.mock import patch
|
|
|
562
568
|
|
|
563
569
|
from ruff_sync.formatters import GitlabFormatter
|
|
564
570
|
|
|
571
|
+
|
|
565
572
|
def test_gitlab_formatter_empty_on_no_issues(capsys):
|
|
566
573
|
fmt = GitlabFormatter()
|
|
567
574
|
fmt.finalize()
|
|
@@ -569,6 +576,7 @@ def test_gitlab_formatter_empty_on_no_issues(capsys):
|
|
|
569
576
|
issues = json.loads(captured.out)
|
|
570
577
|
assert issues == []
|
|
571
578
|
|
|
579
|
+
|
|
572
580
|
def test_gitlab_formatter_error_produces_major_issue(capsys):
|
|
573
581
|
fmt = GitlabFormatter()
|
|
574
582
|
fmt.error("drift found", file_path=pathlib.Path("pyproject.toml"))
|
|
@@ -581,6 +589,7 @@ def test_gitlab_formatter_error_produces_major_issue(capsys):
|
|
|
581
589
|
assert issues[0]["location"]["lines"]["begin"] == 1
|
|
582
590
|
assert "fingerprint" in issues[0]
|
|
583
591
|
|
|
592
|
+
|
|
584
593
|
def test_gitlab_formatter_fingerprint_is_stable(capsys):
|
|
585
594
|
fmt1 = GitlabFormatter()
|
|
586
595
|
fmt2 = GitlabFormatter()
|
|
@@ -594,6 +603,7 @@ def test_gitlab_formatter_fingerprint_is_stable(capsys):
|
|
|
594
603
|
issues2 = json.loads(out2)
|
|
595
604
|
assert issues1[0]["fingerprint"] == issues2[0]["fingerprint"]
|
|
596
605
|
|
|
606
|
+
|
|
597
607
|
def test_gitlab_formatter_no_bom(capsys):
|
|
598
608
|
fmt = GitlabFormatter()
|
|
599
609
|
fmt.finalize()
|
|
@@ -17,8 +17,18 @@ class ResultFormatter(Protocol):
|
|
|
17
17
|
def note(self, message: str) -> None: ...
|
|
18
18
|
def info(self, message: str, logger: logging.Logger | None = None) -> None: ...
|
|
19
19
|
def success(self, message: str) -> None: ...
|
|
20
|
-
def error(
|
|
21
|
-
|
|
20
|
+
def error(
|
|
21
|
+
self,
|
|
22
|
+
message: str,
|
|
23
|
+
file_path: pathlib.Path | None = None,
|
|
24
|
+
logger: logging.Logger | None = None,
|
|
25
|
+
) -> None: ...
|
|
26
|
+
def warning(
|
|
27
|
+
self,
|
|
28
|
+
message: str,
|
|
29
|
+
file_path: pathlib.Path | None = None,
|
|
30
|
+
logger: logging.Logger | None = None,
|
|
31
|
+
) -> None: ...
|
|
22
32
|
def debug(self, message: str, logger: logging.Logger | None = None) -> None: ...
|
|
23
33
|
def diff(self, diff_text: str) -> None: ...
|
|
24
34
|
```
|
|
@@ -88,6 +88,7 @@ def validate_toml_syntax(doc: TOMLDocument) -> bool:
|
|
|
88
88
|
"""
|
|
89
89
|
try:
|
|
90
90
|
import tomlkit # already a dep
|
|
91
|
+
|
|
91
92
|
tomlkit.parse(doc.as_string())
|
|
92
93
|
return True
|
|
93
94
|
except Exception: # noqa: BLE001
|
|
@@ -146,9 +147,7 @@ def validate_ruff_accepts_config(doc: TOMLDocument, is_ruff_toml: bool = False)
|
|
|
146
147
|
)
|
|
147
148
|
return False
|
|
148
149
|
except FileNotFoundError:
|
|
149
|
-
LOGGER.warning(
|
|
150
|
-
"⚠️ `ruff` not found on PATH — skipping Ruff config validation."
|
|
151
|
-
)
|
|
150
|
+
LOGGER.warning("⚠️ `ruff` not found on PATH — skipping Ruff config validation.")
|
|
152
151
|
return True # Soft fail: don't block if ruff isn't installed
|
|
153
152
|
except subprocess.TimeoutExpired:
|
|
154
153
|
LOGGER.warning("⚠️ Ruff config validation timed out — skipping.")
|
|
@@ -184,7 +183,7 @@ Validation is **opt-in**. Before touching `core.py`, wire up the CLI flag.
|
|
|
184
183
|
class Arguments(NamedTuple):
|
|
185
184
|
...
|
|
186
185
|
validate: bool = False # run --validate checks before writing
|
|
187
|
-
strict: bool = False
|
|
186
|
+
strict: bool = False # treat warnings as errors (implies validate)
|
|
188
187
|
```
|
|
189
188
|
|
|
190
189
|
**In `common_parser`** (around line 236), add:
|
|
@@ -230,18 +229,17 @@ Open `core.py`. Find the `pull()` function (around line 1103). Look for this blo
|
|
|
230
229
|
**After** this block (and **before** `should_save = args.save ...`), insert:
|
|
231
230
|
|
|
232
231
|
```python
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return 1
|
|
232
|
+
# Validation is opt-in — only run if --validate (or --strict) was passed
|
|
233
|
+
if args.validate:
|
|
234
|
+
is_ruff_toml = is_ruff_toml_file(_source_toml_path.name)
|
|
235
|
+
from ruff_sync.validation import validate_merged_config # noqa: PLC0415
|
|
236
|
+
|
|
237
|
+
if not validate_merged_config(source_doc, is_ruff_toml=is_ruff_toml, strict=args.strict):
|
|
238
|
+
fmt.error(
|
|
239
|
+
"❌ Merged config failed validation. Local file left unchanged.",
|
|
240
|
+
logger=LOGGER,
|
|
241
|
+
)
|
|
242
|
+
return 1
|
|
245
243
|
```
|
|
246
244
|
|
|
247
245
|
> **Note**: The inline import avoids a circular import issue if `validation.py` ever needs to
|
|
@@ -437,6 +435,7 @@ Add to `tests/test_config_validation.py`:
|
|
|
437
435
|
```python
|
|
438
436
|
def test_version_consistency_warn_on_mismatch(caplog: pytest.LogCaptureFixture) -> None:
|
|
439
437
|
import logging
|
|
438
|
+
|
|
440
439
|
doc = tomlkit.parse(
|
|
441
440
|
'[project]\nrequires-python = ">=3.10"\n\n[tool.ruff]\ntarget-version = "py39"\n'
|
|
442
441
|
)
|
|
@@ -492,9 +491,7 @@ def _get_deprecated_rule_codes() -> frozenset[str]:
|
|
|
492
491
|
if result.returncode != 0:
|
|
493
492
|
return frozenset()
|
|
494
493
|
rules = json.loads(result.stdout)
|
|
495
|
-
return frozenset(
|
|
496
|
-
r["code"] for r in rules if r.get("deprecated") is True
|
|
497
|
-
)
|
|
494
|
+
return frozenset(r["code"] for r in rules if r.get("deprecated") is True)
|
|
498
495
|
except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError, KeyError):
|
|
499
496
|
return frozenset()
|
|
500
497
|
```
|
|
@@ -573,7 +570,9 @@ def check_deprecated_rules(
|
|
|
573
570
|
is_ruff_toml: bool = False,
|
|
574
571
|
_deprecated_codes: frozenset[str] | None = None,
|
|
575
572
|
) -> None:
|
|
576
|
-
deprecated_codes =
|
|
573
|
+
deprecated_codes = (
|
|
574
|
+
_deprecated_codes if _deprecated_codes is not None else _get_deprecated_rule_codes()
|
|
575
|
+
)
|
|
577
576
|
...
|
|
578
577
|
```
|
|
579
578
|
|
|
@@ -14,13 +14,16 @@ Instead of asserting on every field manually, compare against a "dirty" object t
|
|
|
14
14
|
```python
|
|
15
15
|
from dirty_equals import IsInt, IsPartialDict, IsStr
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
def test_config_logic():
|
|
18
19
|
result = {"status": "active", "version": 1, "extra": "data"}
|
|
19
20
|
# Declarative assertion
|
|
20
|
-
assert result == IsPartialDict(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
assert result == IsPartialDict(
|
|
22
|
+
{
|
|
23
|
+
"status": IsStr(regex="act.*"),
|
|
24
|
+
"version": IsInt(gt=0),
|
|
25
|
+
}
|
|
26
|
+
)
|
|
24
27
|
```
|
|
25
28
|
|
|
26
29
|
## Detailed reference
|
|
@@ -24,11 +24,13 @@ import httpx
|
|
|
24
24
|
import pathlib
|
|
25
25
|
|
|
26
26
|
# Match a partial dict with mixed types
|
|
27
|
-
assert response_data == IsPartialDict(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
assert response_data == IsPartialDict(
|
|
28
|
+
{
|
|
29
|
+
"url": IsInstance(httpx.URL),
|
|
30
|
+
"status": "success",
|
|
31
|
+
"retries": 0,
|
|
32
|
+
}
|
|
33
|
+
)
|
|
32
34
|
```
|
|
33
35
|
|
|
34
36
|
### String and Path Matching
|
|
@@ -13,7 +13,7 @@ from dirty_equals import IsPartialDict
|
|
|
13
13
|
import tomlkit
|
|
14
14
|
|
|
15
15
|
# Parse some TOML
|
|
16
|
-
doc = tomlkit.parse(
|
|
16
|
+
doc = tomlkit.parse("[tool.ruff]\nline-length = 80")
|
|
17
17
|
|
|
18
18
|
# Match the tool.ruff section
|
|
19
19
|
ruff_config = doc["tool"]["ruff"]
|
|
@@ -46,9 +46,11 @@ args = ruff_sync_cli.Arguments(
|
|
|
46
46
|
)
|
|
47
47
|
|
|
48
48
|
# Convert to dict and match specific fields
|
|
49
|
-
assert args._asdict() == IsPartialDict(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
assert args._asdict() == IsPartialDict(
|
|
50
|
+
{
|
|
51
|
+
"command": "pull",
|
|
52
|
+
"upstream": (IsInstance(httpx.URL),),
|
|
53
|
+
"to": IsInstance(pathlib.Path),
|
|
54
|
+
}
|
|
55
|
+
)
|
|
54
56
|
```
|
|
@@ -14,8 +14,10 @@ from __future__ import annotations
|
|
|
14
14
|
from textual.app import App, ComposeResult
|
|
15
15
|
from textual.widgets import Header, Footer, Static
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
class SimpleApp(App[None]):
|
|
18
19
|
"""A minimal Textual app."""
|
|
20
|
+
|
|
19
21
|
BINDINGS = [("q", "quit", "Quit")]
|
|
20
22
|
|
|
21
23
|
def compose(self) -> ComposeResult:
|
|
@@ -23,6 +25,7 @@ class SimpleApp(App[None]):
|
|
|
23
25
|
yield Static("Hello, [bold blue]Textual[/bold blue]!")
|
|
24
26
|
yield Footer()
|
|
25
27
|
|
|
28
|
+
|
|
26
29
|
if __name__ == "__main__":
|
|
27
30
|
SimpleApp().run()
|
|
28
31
|
```
|
|
@@ -33,6 +33,7 @@ Define `reactive` attributes to automatically trigger updates. Use `watch_<attri
|
|
|
33
33
|
```python
|
|
34
34
|
from textual.reactive import reactive
|
|
35
35
|
|
|
36
|
+
|
|
36
37
|
class Counter(Static):
|
|
37
38
|
# Reactive state: UI updates whenever 'count' is modified
|
|
38
39
|
count: reactive[int] = reactive(0)
|
|
@@ -52,6 +53,8 @@ Widgets can communicate with parents via custom messages:
|
|
|
52
53
|
1. **Define a Message**:
|
|
53
54
|
```python
|
|
54
55
|
from textual.message import Message
|
|
56
|
+
|
|
57
|
+
|
|
55
58
|
class DataLoaded(Message):
|
|
56
59
|
def __init__(self, data: list[str]) -> None:
|
|
57
60
|
self.data = data
|
|
@@ -82,6 +82,7 @@ Modals are screens with a transparent or dim background that overlay the main ap
|
|
|
82
82
|
from textual.screen import ModalScreen
|
|
83
83
|
from textual.app import App
|
|
84
84
|
|
|
85
|
+
|
|
85
86
|
class OmniboxScreen(ModalScreen[str]):
|
|
86
87
|
# A modal screen that returns a `str` when dismissed.
|
|
87
88
|
def compose(self) -> ComposeResult:
|
|
@@ -92,11 +93,13 @@ class OmniboxScreen(ModalScreen[str]):
|
|
|
92
93
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
93
94
|
self.dismiss(event.value)
|
|
94
95
|
|
|
96
|
+
|
|
95
97
|
# In the main App or Screen:
|
|
96
98
|
def on_key(self, event: events.Key) -> None:
|
|
97
99
|
if event.key == "ctrl+p":
|
|
98
100
|
self.push_screen(OmniboxScreen(), self.handle_omnibox_result)
|
|
99
101
|
|
|
102
|
+
|
|
100
103
|
def handle_omnibox_result(self, result: str | None) -> None:
|
|
101
104
|
if result:
|
|
102
105
|
print(f"Selected: {result}")
|
|
@@ -19,9 +19,11 @@ Use `TypeIs` when you want to definitively split a Union.
|
|
|
19
19
|
from typing import Union
|
|
20
20
|
from typing_extensions import TypeIs
|
|
21
21
|
|
|
22
|
+
|
|
22
23
|
def is_str(val: Union[str, int]) -> TypeIs[str]:
|
|
23
24
|
return isinstance(val, str)
|
|
24
25
|
|
|
26
|
+
|
|
25
27
|
def process(val: Union[str, int]) -> None:
|
|
26
28
|
if is_str(val):
|
|
27
29
|
# Mypy knows val is str
|
|
@@ -39,10 +41,12 @@ Use `@runtime_checkable Protocol` with `TypeIs` for structural subtyping.
|
|
|
39
41
|
from typing import Protocol, runtime_checkable
|
|
40
42
|
from typing_extensions import TypeIs
|
|
41
43
|
|
|
44
|
+
|
|
42
45
|
@runtime_checkable
|
|
43
46
|
class Reader(Protocol):
|
|
44
47
|
def read(self) -> str: ...
|
|
45
48
|
|
|
49
|
+
|
|
46
50
|
def is_reader(val: object) -> TypeIs[Reader]:
|
|
47
51
|
return isinstance(val, Reader)
|
|
48
52
|
```
|
|
@@ -17,8 +17,8 @@ Match the Mypy error code from your terminal to the project-safe resolution patt
|
|
|
17
17
|
|
|
18
18
|
**Bad (Cast)**:
|
|
19
19
|
```python
|
|
20
|
-
val = get_union()
|
|
21
|
-
val.upper()
|
|
20
|
+
val = get_union() # str | None
|
|
21
|
+
val.upper() # [union-attr] "None" has no attribute "upper"
|
|
22
22
|
# NO: val = cast(str, val)
|
|
23
23
|
```
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ val.upper() # [union-attr] "None" has no attribute "upper"
|
|
|
26
26
|
```python
|
|
27
27
|
val = get_union()
|
|
28
28
|
if val is not None:
|
|
29
|
-
val.upper()
|
|
29
|
+
val.upper() # FIXED
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
### Fixing `[assignment]` (Dealing with `tomlkit` or `Any`)
|
|
@@ -34,7 +34,7 @@ if val is not None:
|
|
|
34
34
|
**Bad (Cast)**:
|
|
35
35
|
```python
|
|
36
36
|
doc = tomlkit.parse(...)
|
|
37
|
-
tool: Table = doc["tool"]
|
|
37
|
+
tool: Table = doc["tool"] # [assignment] Incompatible types (Item vs Table)
|
|
38
38
|
# NO: tool = cast(Table, doc["tool"])
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -52,8 +52,10 @@ if not isinstance(tool, Table):
|
|
|
52
52
|
**Bad (Cast)**:
|
|
53
53
|
```python
|
|
54
54
|
def process_str(s: str): ...
|
|
55
|
+
|
|
56
|
+
|
|
55
57
|
data: Union[str, int] = ...
|
|
56
|
-
process_str(data)
|
|
58
|
+
process_str(data) # [arg-type] Argument 1 has incompatible type
|
|
57
59
|
# NO: process_str(cast(str, data))
|
|
58
60
|
```
|
|
59
61
|
|
|
@@ -61,9 +63,11 @@ process_str(data) # [arg-type] Argument 1 has incompatible type
|
|
|
61
63
|
```python
|
|
62
64
|
from typing_extensions import TypeIs
|
|
63
65
|
|
|
66
|
+
|
|
64
67
|
def is_str(v: object) -> TypeIs[str]:
|
|
65
68
|
return isinstance(v, str)
|
|
66
69
|
|
|
70
|
+
|
|
67
71
|
if is_str(data):
|
|
68
|
-
process_str(data)
|
|
72
|
+
process_str(data) # FIXED
|
|
69
73
|
```
|