ruff-sync 0.0.5.dev1__tar.gz → 0.0.5.dev2__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.
Files changed (56) hide show
  1. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/.github/workflows/complexity.yaml +8 -12
  2. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/PKG-INFO +12 -10
  3. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/README.md +10 -9
  4. ruff_sync-0.0.5.dev2/configs/fastapi/ruff.toml +150 -0
  5. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/pyproject.toml +9 -2
  6. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/ruff_sync.py +88 -39
  7. ruff_sync-0.0.5.dev2/tests/conftest.py +12 -0
  8. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/test_basic.py +36 -21
  9. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/test_check.py +6 -6
  10. ruff_sync-0.0.5.dev2/tests/test_deprecation.py +213 -0
  11. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/test_e2e.py +6 -6
  12. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/test_scaffold.py +6 -6
  13. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/uv.lock +3 -1
  14. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/.agents/TESTING.md +0 -0
  15. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/.agents/workflows/add-test-case.md +0 -0
  16. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/.git-blame-ignore-revs +0 -0
  17. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/.github/dependabot.yml +0 -0
  18. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/.github/workflows/ci.yaml +0 -0
  19. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/.gitignore +0 -0
  20. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/.pre-commit-config.yaml +0 -0
  21. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/AGENTS.md +0 -0
  22. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/LICENSE.md +0 -0
  23. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/codecov.yml +0 -0
  24. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/configs/kitchen-sink/ruff.toml +0 -0
  25. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/ruff_sync_banner.png +0 -0
  26. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/scripts/check_dogfood.sh +0 -0
  27. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/scripts/gitclone_dogfood.sh +0 -0
  28. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/scripts/pull_dogfood.sh +0 -0
  29. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tasks.py +0 -0
  30. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/__init__.py +0 -0
  31. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/no_changes_final.toml +0 -0
  32. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/no_changes_initial.toml +0 -0
  33. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/no_changes_upstream.toml +0 -0
  34. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/no_dotted_keys_final.toml +0 -0
  35. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/no_dotted_keys_initial.toml +0 -0
  36. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/no_dotted_keys_upstream.toml +0 -0
  37. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/no_ruff_cfg_final.toml +0 -0
  38. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/no_ruff_cfg_initial.toml +0 -0
  39. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/no_ruff_cfg_upstream.toml +0 -0
  40. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/readme_excludes_final.toml +0 -0
  41. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/readme_excludes_initial.toml +0 -0
  42. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/readme_excludes_upstream.toml +0 -0
  43. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/standard_final.toml +0 -0
  44. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/standard_initial.toml +0 -0
  45. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/lifecycle_tomls/standard_upstream.toml +0 -0
  46. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/ruff.toml +0 -0
  47. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/test_config_validation.py +0 -0
  48. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/test_corner_cases.py +0 -0
  49. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/test_git_fetch.py +0 -0
  50. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/test_project.py +0 -0
  51. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/test_toml_operations.py +0 -0
  52. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/test_url_handling.py +0 -0
  53. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/test_whitespace.py +0 -0
  54. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/w_ruff_sync_cfg/pyproject.toml +0 -0
  55. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/wo_ruff_cfg/pyproject.toml +0 -0
  56. {ruff_sync-0.0.5.dev1 → ruff_sync-0.0.5.dev2}/tests/wo_ruff_sync_cfg/pyproject.toml +0 -0
@@ -12,14 +12,13 @@ jobs:
12
12
 
13
13
  steps:
14
14
  - name: Checkout repository
15
- uses: actions/checkout@v2
15
+ uses: actions/checkout@v4
16
16
  with:
17
17
  fetch-depth: 0
18
- ref: ${{ github.event.pull_request.head.ref }}
19
18
  - name: Set up Python
20
- uses: actions/setup-python@v2
19
+ uses: actions/setup-python@v5
21
20
  with:
22
- python-version: 3.9
21
+ python-version: "3.10"
23
22
  - name: Install Wily
24
23
  run: pip install 'wily>=1.25.0'
25
24
  - name: Build cache and diff
@@ -33,26 +32,23 @@ jobs:
33
32
  DIFF="${DIFF//'%'/'%25'}"
34
33
  DIFF="${DIFF//$'\n'/'%0A'}"
35
34
  DIFF="${DIFF//$'\r'/'%0D'}"
36
- echo "::set-output name=diff::$DIFF"
37
- - name: Find current PR
38
- uses: jwalton/gh-find-current-pr@v1
39
- id: findPr
35
+ echo "diff=$DIFF" >> "$GITHUB_OUTPUT"
40
36
  - name: Add Wily PR Comment
41
37
  uses: marocchino/sticky-pull-request-comment@v2
42
- if: steps.findPr.outputs.number && steps.wily.outputs.diff != ''
38
+ if: github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.number && steps.wily.outputs.diff != ''
43
39
  with:
44
40
  recreate: true
45
- number: ${{ steps.findPr.outputs.number }}
41
+ number: ${{ github.event.pull_request.number }}
46
42
  message: |
47
43
  ```
48
44
  ${{ steps.wily.outputs.diff }}
49
45
  ```
50
46
  - name: Add Wily PR Comment
51
47
  uses: marocchino/sticky-pull-request-comment@v2
52
- if: steps.findPr.outputs.number && steps.wily.outputs.diff == ''
48
+ if: github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.number && steps.wily.outputs.diff == ''
53
49
  with:
54
50
  recreate: true
55
- number: ${{ steps.findPr.outputs.number }}
51
+ number: ${{ github.event.pull_request.number }}
56
52
  message: |
57
53
  ```
58
54
  Wily: No changes in complexity detected.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ruff-sync
3
- Version: 0.0.5.dev1
3
+ Version: 0.0.5.dev2
4
4
  Summary: Synchronize Ruff linter configuration across projects
5
5
  Project-URL: Homepage, https://github.com/Kilo59/ruff-sync
6
6
  Project-URL: Documentation, https://github.com/Kilo59/ruff-sync#readme
@@ -26,6 +26,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
26
26
  Requires-Python: >=3.10
27
27
  Requires-Dist: httpx<1.0.0,>=0.27.0
28
28
  Requires-Dist: tomlkit<2.0.0,>=0.12.3
29
+ Requires-Dist: typing-extensions>=4.5.0
29
30
  Description-Content-Type: text/markdown
30
31
 
31
32
  <p align="center">
@@ -56,7 +57,6 @@ Description-Content-Type: text/markdown
56
57
  - [CI Integration](#ci-integration)
57
58
  - [Example Workflow](#example-workflow)
58
59
  - [Detailed Check Logic](#detailed-check-logic)
59
- - [Contributing](#contributing)
60
60
  - [Dogfooding](#dogfooding)
61
61
  - [License](#license)
62
62
 
@@ -190,7 +190,7 @@ exclude = [
190
190
  ```
191
191
 
192
192
  This sets the default upstream and exclusions so you don't need to pass them on the command line every time.
193
- *Note: Any explicitly provided CLI arguments will override the values in `pyproject.toml`.*
193
+ _Note: Any explicitly provided CLI arguments will override the values in `pyproject.toml`._
194
194
 
195
195
  ### Advanced Configuration
196
196
 
@@ -212,8 +212,11 @@ exclude = [
212
212
  branch = "develop"
213
213
 
214
214
  # A directory prefix to use when looking for a configuration file in a repository. (Default: "")
215
- # Useful if the upstream pyproject.toml is not at the repository root.
215
+ # Useful if the upstream config is not at the repository root.
216
216
  path = "config/ruff"
217
+
218
+ # The local target directory or file to sync into. (Default: ".")
219
+ to = "."
217
220
  ```
218
221
 
219
222
  ## CI Integration
@@ -262,7 +265,7 @@ git commit -am "sync ruff config from upstream"
262
265
 
263
266
  ### Curated Examples
264
267
 
265
- While `ruff-sync` is designed to sync from *any* repository or URL of your choosing, this repository also provides a few curated configurations in the [`configs/`](./configs/) directory that you can use directly.
268
+ While `ruff-sync` is designed to sync from _any_ repository or URL of your choosing, this repository also provides a few curated configurations in the [`configs/`](./configs/) directory that you can use directly.
266
269
 
267
270
  For example, to sync an exhaustive "kitchen-sink" configuration that explicitly enables all rules and documents them:
268
271
 
@@ -287,8 +290,7 @@ By default, `ruff-sync` requires an existing configuration file (`pyproject.toml
287
290
  ruff-sync pull https://github.com/my-org/standards --init
288
291
  ```
289
292
 
290
- `ruff-sync` seamlessly supports both `pyproject.toml` and standalone `ruff.toml` (or `.ruff.toml`) files. If your upstream source or your local target is a `ruff.toml`, it will automatically adapt and sync the root configuration rather than looking for a `[tool.ruff]` section.
291
-
293
+ `ruff-sync` seamlessly supports both `pyproject.toml` and standalone `ruff.toml` (or `.ruff.toml`) files. If your local target is a directory, it will look for configuration files in the following order: `ruff.toml` -> `.ruff.toml` -> `pyproject.toml`. If your upstream source or your local target is a `ruff.toml`, it will automatically adapt and sync the root configuration rather than looking for a `[tool.ruff]` section.
292
294
 
293
295
  ## Detailed Check Logic
294
296
 
@@ -296,9 +298,9 @@ When you run `ruff-sync check`, it follows this process to determine if your pro
296
298
 
297
299
  ```mermaid
298
300
  flowchart TD
299
- Start([Start]) --> Local[Read Local pyproject.toml]
300
- Local --> Upstream[Download Upstream pyproject.toml]
301
- Upstream --> Extract[Extract tool.ruff section]
301
+ Start([Start]) --> Local[Read Local Configuration]
302
+ Local --> Upstream[Download Upstream Configuration]
303
+ Upstream --> Extract[Extract tool.ruff section if needed]
302
304
  Extract --> Exclude[Apply Exclusions]
303
305
  Exclude --> Merge[Perform in-memory Merge]
304
306
 
@@ -26,7 +26,6 @@
26
26
  - [CI Integration](#ci-integration)
27
27
  - [Example Workflow](#example-workflow)
28
28
  - [Detailed Check Logic](#detailed-check-logic)
29
- - [Contributing](#contributing)
30
29
  - [Dogfooding](#dogfooding)
31
30
  - [License](#license)
32
31
 
@@ -160,7 +159,7 @@ exclude = [
160
159
  ```
161
160
 
162
161
  This sets the default upstream and exclusions so you don't need to pass them on the command line every time.
163
- *Note: Any explicitly provided CLI arguments will override the values in `pyproject.toml`.*
162
+ _Note: Any explicitly provided CLI arguments will override the values in `pyproject.toml`._
164
163
 
165
164
  ### Advanced Configuration
166
165
 
@@ -182,8 +181,11 @@ exclude = [
182
181
  branch = "develop"
183
182
 
184
183
  # A directory prefix to use when looking for a configuration file in a repository. (Default: "")
185
- # Useful if the upstream pyproject.toml is not at the repository root.
184
+ # Useful if the upstream config is not at the repository root.
186
185
  path = "config/ruff"
186
+
187
+ # The local target directory or file to sync into. (Default: ".")
188
+ to = "."
187
189
  ```
188
190
 
189
191
  ## CI Integration
@@ -232,7 +234,7 @@ git commit -am "sync ruff config from upstream"
232
234
 
233
235
  ### Curated Examples
234
236
 
235
- While `ruff-sync` is designed to sync from *any* repository or URL of your choosing, this repository also provides a few curated configurations in the [`configs/`](./configs/) directory that you can use directly.
237
+ While `ruff-sync` is designed to sync from _any_ repository or URL of your choosing, this repository also provides a few curated configurations in the [`configs/`](./configs/) directory that you can use directly.
236
238
 
237
239
  For example, to sync an exhaustive "kitchen-sink" configuration that explicitly enables all rules and documents them:
238
240
 
@@ -257,8 +259,7 @@ By default, `ruff-sync` requires an existing configuration file (`pyproject.toml
257
259
  ruff-sync pull https://github.com/my-org/standards --init
258
260
  ```
259
261
 
260
- `ruff-sync` seamlessly supports both `pyproject.toml` and standalone `ruff.toml` (or `.ruff.toml`) files. If your upstream source or your local target is a `ruff.toml`, it will automatically adapt and sync the root configuration rather than looking for a `[tool.ruff]` section.
261
-
262
+ `ruff-sync` seamlessly supports both `pyproject.toml` and standalone `ruff.toml` (or `.ruff.toml`) files. If your local target is a directory, it will look for configuration files in the following order: `ruff.toml` -> `.ruff.toml` -> `pyproject.toml`. If your upstream source or your local target is a `ruff.toml`, it will automatically adapt and sync the root configuration rather than looking for a `[tool.ruff]` section.
262
263
 
263
264
  ## Detailed Check Logic
264
265
 
@@ -266,9 +267,9 @@ When you run `ruff-sync check`, it follows this process to determine if your pro
266
267
 
267
268
  ```mermaid
268
269
  flowchart TD
269
- Start([Start]) --> Local[Read Local pyproject.toml]
270
- Local --> Upstream[Download Upstream pyproject.toml]
271
- Upstream --> Extract[Extract tool.ruff section]
270
+ Start([Start]) --> Local[Read Local Configuration]
271
+ Local --> Upstream[Download Upstream Configuration]
272
+ Upstream --> Extract[Extract tool.ruff section if needed]
272
273
  Extract --> Exclude[Apply Exclusions]
273
274
  Exclude --> Merge[Perform in-memory Merge]
274
275
 
@@ -0,0 +1,150 @@
1
+ # ruff-sync FastAPI Configuration
2
+ # https://github.com/Kilo59/ruff-sync
3
+ #
4
+ # This is a Ruff configuration tailored for FastAPI / Web App development.
5
+ # It enables rules directly relevant to web development and async Python.
6
+ #
7
+ # Usage (direct Ruff config, assuming this file is vendored into your repo):
8
+ # extend = "configs/fastapi/ruff.toml"
9
+ #
10
+ # Usage (with ruff-sync in pyproject.toml):
11
+ # [tool.ruff-sync]
12
+ # path = "configs/fastapi/ruff.toml"
13
+
14
+ # Same as Black.
15
+ line-length = 88
16
+ indent-width = 4
17
+
18
+ # Assume Python 3.10. Consumers should override this to match their project's Python version.
19
+ target-version = "py310"
20
+
21
+ [lint]
22
+ # Enable rules relevant to web development and async Python.
23
+ select = [
24
+ # https://docs.astral.sh/ruff/rules/#pyflakes-f
25
+ "F", # Pyflakes: Essential checks for Python bugs
26
+ # https://docs.astral.sh/ruff/rules/#error-e
27
+ "E", # pycodestyle errors: PEP8 styling
28
+ # https://docs.astral.sh/ruff/rules/#warning-w
29
+ "W", # pycodestyle warnings: PEP8 styling
30
+ # https://docs.astral.sh/ruff/rules/#mccabe-c90
31
+ "C90", # mccabe: Code complexity (cyclomatic complexity)
32
+ # https://docs.astral.sh/ruff/rules/#isort-i
33
+ "I", # isort: Import sorting
34
+ # https://docs.astral.sh/ruff/rules/#pep8-naming-n
35
+ "N", # pep8-naming: Naming conventions
36
+ # https://docs.astral.sh/ruff/rules/#pydocstyle-d
37
+ "D", # pydocstyle: Docstring conventions
38
+ # https://docs.astral.sh/ruff/rules/#pyupgrade-up
39
+ "UP", # pyupgrade: Upgrade syntax for newer Python versions
40
+ # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann
41
+ "ANN", # flake8-annotations: Type annotation checks
42
+ # https://docs.astral.sh/ruff/rules/#flake8-async-async
43
+ "ASYNC", # flake8-async: Asynchronous code checks
44
+ # https://docs.astral.sh/ruff/rules/#flake8-bandit-s
45
+ "S", # flake8-bandit: Security testing
46
+ # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b
47
+ "B", # flake8-bugbear: Finding likely bugs and design problems
48
+ # https://docs.astral.sh/ruff/rules/#flake8-pie-pie
49
+ "PIE", # flake8-pie: Misc. lints
50
+ # https://docs.astral.sh/ruff/rules/#flake8-print-t20
51
+ "T20", # flake8-print: Check for Print statements
52
+ # https://docs.astral.sh/ruff/rules/#flake8-return-ret
53
+ "RET", # flake8-return: Check return values
54
+ # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim
55
+ "SIM", # flake8-simplify: Code simplification
56
+ # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g
57
+ "G", # flake8-logging-format: Validate logging format strings
58
+ # https://docs.astral.sh/ruff/rules/#flake8-quotes-q
59
+ "Q", # flake8-quotes: Lint for quotes
60
+ # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt
61
+ "PT", # flake8-pytest-style: Pytest style checks
62
+ # https://docs.astral.sh/ruff/rules/#pylint-pl
63
+ "PL", # Pylint: Pylint rules
64
+ ]
65
+
66
+ # Ignore rules that conflict with the Ruff formatter.
67
+ # See: https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
68
+ ignore = [
69
+ "W191", # tab-indentation
70
+ "E111", # indentation-with-invalid-multiple
71
+ "E114", # indentation-with-invalid-multiple-comment
72
+ "E117", # over-indented
73
+ "D206", # docstring-tab-indentation
74
+ "D300", # triple-single-quotes
75
+ "Q000", # bad-quotes-inline-string
76
+ "Q001", # bad-quotes-multiline-string
77
+ "Q002", # bad-quotes-docstring
78
+ "Q003", # avoidable-escaped-quote
79
+ "Q004", # unnecessary-escaped-quote
80
+ "COM812", # missing-trailing-comma
81
+ "COM819", # prohibited-trailing-comma
82
+ "ISC001", # single-line-implicit-string-concatenation
83
+ "ISC002", # multi-line-implicit-string-concatenation
84
+ ]
85
+
86
+ # Allow autofix for all enabled rules.
87
+ fixable = ["ALL"]
88
+
89
+ # Allow unused variables when underscore-prefixed.
90
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
91
+
92
+ [lint.mccabe]
93
+ # Flag errors (C901) whenever the complexity level exceeds 10.
94
+ # Default: 10
95
+ max-complexity = 10
96
+
97
+ [lint.pydocstyle]
98
+ # Use Google-style docstrings (common in FastAPI projects).
99
+ # Default: "pep257"
100
+ convention = "google"
101
+
102
+ [lint.flake8-quotes]
103
+ # Use double quotes for inline strings (same as Black).
104
+ # Default: "double"
105
+ inline-quotes = "double"
106
+ # Use double quotes for multiline strings.
107
+ # Default: "double"
108
+ multiline-quotes = "double"
109
+ # Use double quotes for docstrings.
110
+ # Default: "double"
111
+ docstring-quotes = "double"
112
+ # Avoid escaping quotes if the other quote type would save an escape.
113
+ # Default: true
114
+ avoid-escape = true
115
+
116
+ [lint.flake8-pytest-style]
117
+ # Whether to require parentheses for pytest fixtures.
118
+ # Default: true
119
+ fixture-parentheses = true
120
+ # The type of pytest.mark.parametrize names to use: "tuple", "list", or "csv"
121
+ # Default: "tuple"
122
+ parametrize-names-type = "tuple"
123
+
124
+ [lint.pylint]
125
+ # The maximum number of arguments allowed for a function.
126
+ # Default: 5
127
+ max-args = 5
128
+ # The maximum number of branches allowed for a function.
129
+ # Default: 12
130
+ max-branches = 12
131
+
132
+ [lint.pep8-naming]
133
+ # Additional decorators that should be treated as classmethod.
134
+ # Pydantic v1/v2 decorators.
135
+ classmethod-decorators = [
136
+ "pydantic.validator",
137
+ "pydantic.root_validator",
138
+ "pydantic.field_validator",
139
+ "pydantic.model_validator",
140
+ ]
141
+
142
+ [format]
143
+ # Like Black, use double quotes for strings.
144
+ quote-style = "double"
145
+ # Like Black, indent with spaces, rather than tabs.
146
+ indent-style = "space"
147
+ # Like Black, respect magic trailing commas.
148
+ skip-magic-trailing-comma = false
149
+ # Like Black, automatically detect the appropriate line ending.
150
+ line-ending = "auto"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ruff-sync"
3
- version = "0.0.5.dev1"
3
+ version = "0.0.5.dev2"
4
4
  description = "Synchronize Ruff linter configuration across projects"
5
5
  keywords = ["ruff", "linter", "config", "synchronize", "python", "linting", "automation", "tomlkit"]
6
6
  authors = [
@@ -26,6 +26,7 @@ classifiers = [
26
26
  dependencies = [
27
27
  "httpx>=0.27.0,<1.0.0",
28
28
  "tomlkit>=0.12.3,<2.0.0",
29
+ "typing-extensions>=4.5.0",
29
30
  ]
30
31
 
31
32
  [project.urls]
@@ -77,7 +78,13 @@ module = "tests.*"
77
78
  disable_error_code = ['type-arg', 'no-untyped-def']
78
79
 
79
80
  [tool.ruff-sync]
80
- exclude = ["target-version", "line-length", "lint.per-file-ignores", "lint.ignore"]
81
+ exclude = [
82
+ "target-version",
83
+ "line-length",
84
+ "lint.per-file-ignores",
85
+ "lint.ignore",
86
+ "extend-exclude",
87
+ ]
81
88
 
82
89
  [tool.ruff]
83
90
  target-version = "py310"
@@ -4,6 +4,7 @@ import asyncio
4
4
  import difflib
5
5
  import json
6
6
  import logging
7
+ import os
7
8
  import pathlib
8
9
  import re
9
10
  import subprocess
@@ -22,8 +23,9 @@ from httpx import URL
22
23
  from tomlkit import TOMLDocument, table
23
24
  from tomlkit.items import Table
24
25
  from tomlkit.toml_file import TOMLFile
26
+ from typing_extensions import deprecated
25
27
 
26
- __version__ = "0.0.5.dev1"
28
+ __version__ = "0.0.5.dev2"
27
29
 
28
30
  _DEFAULT_EXCLUDE: Final[set[str]] = {"lint.per-file-ignores"}
29
31
  _GITHUB_REPO_PATH_PARTS_COUNT: Final[int] = 2
@@ -59,7 +61,7 @@ class ColoredFormatter(logging.Formatter):
59
61
  class Arguments(NamedTuple):
60
62
  command: str
61
63
  upstream: URL
62
- source: pathlib.Path
64
+ to: pathlib.Path
63
65
  exclude: Iterable[str]
64
66
  verbose: int
65
67
  branch: str = "main"
@@ -68,15 +70,21 @@ class Arguments(NamedTuple):
68
70
  diff: bool = True
69
71
  init: bool = False
70
72
 
73
+ @property
74
+ @deprecated("Use 'to' instead")
75
+ def source(self) -> pathlib.Path:
76
+ return self.to
77
+
71
78
  @classmethod
72
79
  @lru_cache(maxsize=1)
73
80
  def fields(cls) -> set[str]:
74
- return set(cls._fields)
81
+ return set(cls._fields) | {"source"}
75
82
 
76
83
 
77
84
  class Config(TypedDict, total=False):
78
85
  upstream: str
79
- source: str
86
+ to: str
87
+ source: str # Deprecated
80
88
  exclude: list[str]
81
89
  verbose: int
82
90
  branch: str
@@ -101,19 +109,20 @@ def get_config(
101
109
  allowed_keys = set(Config.__annotations__.keys())
102
110
  for arg, value in config.items():
103
111
  if arg in allowed_keys:
112
+ if arg == "source":
113
+ LOGGER.warning(
114
+ "DeprecationWarning: [tool.ruff-sync] 'source' is deprecated. "
115
+ "Use 'to' instead."
116
+ )
104
117
  cfg_result[arg] = value
105
118
  else:
106
119
  LOGGER.warning(f"Unknown ruff-sync configuration: {arg}")
120
+ # Ensure 'to' is populated if 'source' was used
121
+ if "source" in cfg_result and "to" not in cfg_result:
122
+ cfg_result["to"] = cfg_result["source"]
107
123
  return cast("Config", cfg_result)
108
124
 
109
125
 
110
- @lru_cache(maxsize=1)
111
- def _resolve_source(source: str | pathlib.Path) -> pathlib.Path:
112
- if isinstance(source, str):
113
- source = pathlib.Path(source)
114
- return source.resolve(strict=True)
115
-
116
-
117
126
  def _get_cli_parser() -> ArgumentParser:
118
127
  parser = ArgumentParser(
119
128
  prog="ruff-sync",
@@ -148,12 +157,17 @@ def _get_cli_parser() -> ArgumentParser:
148
157
  help="The URL to download the pyproject.toml file from."
149
158
  " Optional if defined in [tool.ruff-sync].",
150
159
  )
160
+ common_parser.add_argument(
161
+ "--to",
162
+ help="The directory or file to sync ruff configuration to. Default: .",
163
+ required=False,
164
+ )
165
+ # Add --source as a deprecated alias
151
166
  common_parser.add_argument(
152
167
  "--source",
153
- type=pathlib.Path,
154
- default=".",
155
- help="The directory to sync the pyproject.toml file to. Default: .",
168
+ help="Deprecated alias for --to.",
156
169
  required=False,
170
+ metavar="PATH",
157
171
  )
158
172
  common_parser.add_argument(
159
173
  "--exclude",
@@ -216,9 +230,32 @@ def _get_cli_parser() -> ArgumentParser:
216
230
  return parser
217
231
 
218
232
 
219
- def _get_target_path(path: str | None) -> str:
233
+ def _resolve_target_path(to: pathlib.Path, upstream_url: str | None = None) -> pathlib.Path:
220
234
  """Resolve the target path for configuration files.
221
235
 
236
+ If 'to' is a file, it's used directly.
237
+ Otherwise, it looks for existing ruff/pyproject.toml in the 'to' directory.
238
+ If none found, it defaults to pyproject.toml unless the upstream is a ruff.toml.
239
+ """
240
+ if to.is_file():
241
+ return to
242
+
243
+ # If it's a directory, look for common config files
244
+ for filename in ("ruff.toml", ".ruff.toml", "pyproject.toml"):
245
+ candidate = to / filename
246
+ if candidate.exists():
247
+ return candidate
248
+
249
+ # If upstream is specified and is a ruff.toml, default to ruff.toml
250
+ if upstream_url and is_ruff_toml_file(upstream_url):
251
+ return to / "ruff.toml"
252
+
253
+ return to / "pyproject.toml"
254
+
255
+
256
+ def _resolve_upstream_target_path(path: str | None) -> str:
257
+ """Resolve the target path for configuration files in upstream repositories.
258
+
222
259
  If the path indicates a .toml file, it's treated as a direct file path.
223
260
  Otherwise, it appends 'pyproject.toml' to the path.
224
261
  """
@@ -261,7 +298,7 @@ def _convert_github_url(url: URL, branch: str = "main", path: str = "") -> URL:
261
298
  path_parts = [p for p in url.path.split("/") if p]
262
299
  if len(path_parts) == _GITHUB_REPO_PATH_PARTS_COUNT:
263
300
  org, repo = path_parts
264
- target_path = _get_target_path(path)
301
+ target_path = _resolve_upstream_target_path(path)
265
302
  raw_url = url.copy_with(
266
303
  host=_GITHUB_RAW_HOST,
267
304
  path=str(pathlib.PurePosixPath("/", org, repo, branch, target_path)),
@@ -302,7 +339,7 @@ def _convert_gitlab_url(url: URL, branch: str = "main", path: str = "") -> URL:
302
339
  # Avoid empty paths or just a slash
303
340
  path_prefix = url.path.rstrip("/")
304
341
  if path_prefix:
305
- target_path = _get_target_path(path)
342
+ target_path = _resolve_upstream_target_path(path)
306
343
  raw_url = url.copy_with(
307
344
  path=str(pathlib.PurePosixPath(path_prefix, "-", "raw", branch, target_path))
308
345
  )
@@ -414,7 +451,7 @@ async def fetch_upstream_config(
414
451
  capture_output=True,
415
452
  text=True,
416
453
  )
417
- target_path = pathlib.Path(_get_target_path(path))
454
+ target_path = pathlib.Path(_resolve_upstream_target_path(path))
418
455
 
419
456
  # Restore just the pyproject_toml file
420
457
  restore_cmd = [
@@ -674,25 +711,13 @@ def merge_ruff_toml(
674
711
  return source
675
712
 
676
713
 
677
- def _resolve_target_path(args: Arguments) -> pathlib.Path:
678
- if args.source.is_file():
679
- return args.source
680
- if (args.source / "ruff.toml").exists():
681
- return args.source / "ruff.toml"
682
- if (args.source / ".ruff.toml").exists():
683
- return args.source / ".ruff.toml"
684
- if args.upstream and is_ruff_toml_file(str(args.upstream)):
685
- return args.source / "ruff.toml"
686
- return args.source / "pyproject.toml"
687
-
688
-
689
714
  async def check(
690
715
  args: Arguments,
691
716
  ) -> int:
692
717
  """Check if the local pyproject.toml / ruff.toml is in sync with the upstream."""
693
718
  print("🔍 Checking Ruff sync status...")
694
719
 
695
- _source_toml_path = _resolve_target_path(args).resolve(strict=False)
720
+ _source_toml_path = _resolve_target_path(args.to, str(args.upstream)).resolve(strict=False)
696
721
  if not _source_toml_path.exists():
697
722
  print(
698
723
  f"❌ Configuration file {_source_toml_path} does not exist. "
@@ -780,7 +805,7 @@ async def pull(
780
805
  ) -> int:
781
806
  """Pull the upstream ruff config and apply it to the source."""
782
807
  print("🔄 Syncing Ruff...")
783
- _source_toml_path = _resolve_target_path(args).resolve(strict=False)
808
+ _source_toml_path = _resolve_target_path(args.to, str(args.upstream)).resolve(strict=False)
784
809
 
785
810
  source_toml_file = TOMLFile(_source_toml_path)
786
811
  if _source_toml_path.exists():
@@ -888,13 +913,33 @@ def _resolve_path(args: Any, config: Mapping[str, Any]) -> str:
888
913
  return ""
889
914
 
890
915
 
891
- def _resolve_args(args: Any, config: Mapping[str, Any]) -> tuple[URL, Iterable[str], str, str]:
892
- """Resolve upstream, exclude, branch, and path from CLI and config."""
916
+ def _resolve_to(args: Any, config: Mapping[str, Any], initial_to: pathlib.Path) -> pathlib.Path:
917
+ """Resolve target path from CLI, config, or default."""
918
+ if args.source:
919
+ LOGGER.warning("DeprecationWarning: --source is deprecated. Use --to instead.")
920
+ return pathlib.Path(args.source)
921
+ if args.to:
922
+ return pathlib.Path(args.to)
923
+ if "to" in config:
924
+ target = pathlib.Path(config["to"])
925
+ # Resolve relative to the directory where we found the config file
926
+ base_dir = initial_to.parent if initial_to.is_file() else initial_to
927
+ resolved = base_dir / target
928
+ LOGGER.info(f"🎯 Using target path from [tool.ruff-sync]: {resolved}")
929
+ return resolved
930
+ return initial_to
931
+
932
+
933
+ def _resolve_args(
934
+ args: Any, config: Mapping[str, Any], initial_to: pathlib.Path
935
+ ) -> tuple[URL, pathlib.Path, Iterable[str], str, str]:
936
+ """Resolve upstream, to, exclude, branch, and path from CLI and config."""
893
937
  upstream = _resolve_upstream(args, config)
938
+ to = _resolve_to(args, config, initial_to)
894
939
  exclude = _resolve_exclude(args, config)
895
940
  branch = _resolve_branch(args, config)
896
941
  path = _resolve_path(args, config)
897
- return upstream, exclude, branch, path
942
+ return upstream, to, exclude, branch, path
898
943
 
899
944
 
900
945
  def main() -> int:
@@ -922,11 +967,15 @@ def main() -> int:
922
967
  handler = logging.StreamHandler()
923
968
  handler.setFormatter(ColoredFormatter())
924
969
  LOGGER.addHandler(handler)
925
- LOGGER.propagate = False # Avoid double logging if root is also configured
970
+ LOGGER.propagate = "PYTEST_CURRENT_TEST" in os.environ # Allow capturing in tests
926
971
 
927
- config: Config = get_config(args.source)
972
+ # Determine target 'to' from CLI or use default '.'
973
+ # Defer Path conversion to avoid pyfakefs issues with captured Path class
974
+ arg_to = args.to or args.source
975
+ initial_to = pathlib.Path(arg_to) if arg_to else pathlib.Path()
976
+ config: Config = get_config(initial_to)
928
977
 
929
- upstream, exclude, branch, path = _resolve_args(args, config)
978
+ upstream, to_val, exclude, branch, path = _resolve_args(args, config, initial_to)
930
979
 
931
980
  # Convert non-raw github/gitlab upstream url to the raw equivalent
932
981
  upstream = resolve_raw_url(upstream, branch=branch, path=path)
@@ -935,7 +984,7 @@ def main() -> int:
935
984
  exec_args = Arguments(
936
985
  command=args.command,
937
986
  upstream=upstream,
938
- source=args.source,
987
+ to=to_val,
939
988
  exclude=exclude,
940
989
  verbose=args.verbose,
941
990
  branch=branch,
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ import ruff_sync
6
+
7
+
8
+ @pytest.fixture
9
+ def clear_ruff_sync_caches():
10
+ """Clear all lru_caches in ruff_sync."""
11
+ ruff_sync.get_config.cache_clear()
12
+ ruff_sync.Arguments.fields.cache_clear()