ruff-sync 0.0.4.dev1__tar.gz → 0.0.5.dev1__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 (54) hide show
  1. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/.github/workflows/ci.yaml +1 -0
  2. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/PKG-INFO +19 -9
  3. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/README.md +18 -8
  4. ruff_sync-0.0.5.dev1/configs/kitchen-sink/ruff.toml +262 -0
  5. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/pyproject.toml +1 -1
  6. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/ruff_sync.py +80 -15
  7. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/test_url_handling.py +107 -2
  8. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/uv.lock +1 -1
  9. ruff_sync-0.0.4.dev1/tests/test_url_parsing.py +0 -31
  10. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/.agents/TESTING.md +0 -0
  11. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/.agents/workflows/add-test-case.md +0 -0
  12. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/.git-blame-ignore-revs +0 -0
  13. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/.github/dependabot.yml +0 -0
  14. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/.github/workflows/complexity.yaml +0 -0
  15. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/.gitignore +0 -0
  16. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/.pre-commit-config.yaml +0 -0
  17. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/AGENTS.md +0 -0
  18. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/LICENSE.md +0 -0
  19. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/codecov.yml +0 -0
  20. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/ruff_sync_banner.png +0 -0
  21. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/scripts/check_dogfood.sh +0 -0
  22. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/scripts/gitclone_dogfood.sh +0 -0
  23. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/scripts/pull_dogfood.sh +0 -0
  24. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tasks.py +0 -0
  25. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/__init__.py +0 -0
  26. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/no_changes_final.toml +0 -0
  27. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/no_changes_initial.toml +0 -0
  28. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/no_changes_upstream.toml +0 -0
  29. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/no_dotted_keys_final.toml +0 -0
  30. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/no_dotted_keys_initial.toml +0 -0
  31. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/no_dotted_keys_upstream.toml +0 -0
  32. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/no_ruff_cfg_final.toml +0 -0
  33. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/no_ruff_cfg_initial.toml +0 -0
  34. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/no_ruff_cfg_upstream.toml +0 -0
  35. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/readme_excludes_final.toml +0 -0
  36. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/readme_excludes_initial.toml +0 -0
  37. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/readme_excludes_upstream.toml +0 -0
  38. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/standard_final.toml +0 -0
  39. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/standard_initial.toml +0 -0
  40. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/lifecycle_tomls/standard_upstream.toml +0 -0
  41. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/ruff.toml +0 -0
  42. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/test_basic.py +0 -0
  43. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/test_check.py +0 -0
  44. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/test_config_validation.py +0 -0
  45. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/test_corner_cases.py +0 -0
  46. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/test_e2e.py +0 -0
  47. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/test_git_fetch.py +0 -0
  48. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/test_project.py +0 -0
  49. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/test_scaffold.py +0 -0
  50. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/test_toml_operations.py +0 -0
  51. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/test_whitespace.py +0 -0
  52. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/w_ruff_sync_cfg/pyproject.toml +0 -0
  53. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/wo_ruff_cfg/pyproject.toml +0 -0
  54. {ruff_sync-0.0.4.dev1 → ruff_sync-0.0.5.dev1}/tests/wo_ruff_sync_cfg/pyproject.toml +0 -0
@@ -88,6 +88,7 @@ jobs:
88
88
  ruff-sync --version
89
89
  ruff-sync https://github.com/Kilo59/ruff-sync
90
90
  ruff-sync check https://github.com/Kilo59/ruff-sync
91
+ ! ruff-sync check --semantic https://github.com/Kilo59/ruff-sync --path configs/kitchen-sink
91
92
 
92
93
  publish:
93
94
  name: Build and publish to PyPI
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ruff-sync
3
- Version: 0.0.4.dev1
3
+ Version: 0.0.5.dev1
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
@@ -130,14 +130,6 @@ With [pip](https://pip.pypa.io/en/stable/):
130
130
  pip install ruff-sync
131
131
  ```
132
132
 
133
- #### From Source (Bleeding Edge)
134
-
135
- If you want the latest development version:
136
-
137
- ```console
138
- uv tool install git+https://github.com/Kilo59/ruff-sync
139
- ```
140
-
141
133
  ### Usage
142
134
 
143
135
  ```console
@@ -268,6 +260,24 @@ git diff pyproject.toml # review the changes
268
260
  git commit -am "sync ruff config from upstream"
269
261
  ```
270
262
 
263
+ ### Curated Examples
264
+
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.
266
+
267
+ For example, to sync an exhaustive "kitchen-sink" configuration that explicitly enables all rules and documents them:
268
+
269
+ ```console
270
+ ruff-sync https://github.com/Kilo59/ruff-sync/blob/main/configs/kitchen-sink/ruff.toml
271
+ ```
272
+
273
+ Or configure it using `pyproject.toml` so it's always the default for your local project:
274
+
275
+ ```toml
276
+ [tool.ruff-sync]
277
+ upstream = "https://github.com/Kilo59/ruff-sync"
278
+ path = "configs/kitchen-sink"
279
+ ```
280
+
271
281
  ## Bootstrapping a New Project
272
282
 
273
283
  By default, `ruff-sync` requires an existing configuration file (`pyproject.toml` or `ruff.toml`) to merge into. If you are starting a fresh project and want to initialize it with your organization's Ruff settings, you can use the `--init` flag to scaffold a new file automatically.
@@ -100,14 +100,6 @@ With [pip](https://pip.pypa.io/en/stable/):
100
100
  pip install ruff-sync
101
101
  ```
102
102
 
103
- #### From Source (Bleeding Edge)
104
-
105
- If you want the latest development version:
106
-
107
- ```console
108
- uv tool install git+https://github.com/Kilo59/ruff-sync
109
- ```
110
-
111
103
  ### Usage
112
104
 
113
105
  ```console
@@ -238,6 +230,24 @@ git diff pyproject.toml # review the changes
238
230
  git commit -am "sync ruff config from upstream"
239
231
  ```
240
232
 
233
+ ### Curated Examples
234
+
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.
236
+
237
+ For example, to sync an exhaustive "kitchen-sink" configuration that explicitly enables all rules and documents them:
238
+
239
+ ```console
240
+ ruff-sync https://github.com/Kilo59/ruff-sync/blob/main/configs/kitchen-sink/ruff.toml
241
+ ```
242
+
243
+ Or configure it using `pyproject.toml` so it's always the default for your local project:
244
+
245
+ ```toml
246
+ [tool.ruff-sync]
247
+ upstream = "https://github.com/Kilo59/ruff-sync"
248
+ path = "configs/kitchen-sink"
249
+ ```
250
+
241
251
  ## Bootstrapping a New Project
242
252
 
243
253
  By default, `ruff-sync` requires an existing configuration file (`pyproject.toml` or `ruff.toml`) to merge into. If you are starting a fresh project and want to initialize it with your organization's Ruff settings, you can use the `--init` flag to scaffold a new file automatically.
@@ -0,0 +1,262 @@
1
+ # ruff-sync Kitchen Sink Configuration
2
+ # https://github.com/Kilo59/ruff-sync
3
+ #
4
+ # This file enables ALL possible Ruff rules as of Ruff v0.15.5
5
+ # It explicitly lists all rule categories and provides links to their documentation.
6
+ # This serves as a comprehensive reference for what is possible with Ruff.
7
+
8
+ # Same as Black.
9
+ line-length = 88
10
+ indent-width = 4
11
+
12
+ # Assume Python 3.10. Consumers should override this to match
13
+ # their project's supported Python version.
14
+ target-version = "py310"
15
+
16
+ [lint]
17
+ # Enable all rule categories explicitly.
18
+ select = [
19
+ # https://docs.astral.sh/ruff/rules/#pyflakes-f
20
+ "F", # Pyflakes: Essential checks for python bugs
21
+ # https://docs.astral.sh/ruff/rules/#error-e
22
+ "E", # pycodestyle errors: PEP8 styling
23
+ # https://docs.astral.sh/ruff/rules/#warning-w
24
+ "W", # pycodestyle warnings: PEP8 styling
25
+ # https://docs.astral.sh/ruff/rules/#mccabe-c90
26
+ "C90", # mccabe: Code complexity (cyclomatic complexity)
27
+ # https://docs.astral.sh/ruff/rules/#isort-i
28
+ "I", # isort: Import sorting
29
+ # https://docs.astral.sh/ruff/rules/#pep8-naming-n
30
+ "N", # pep8-naming: Naming conventions
31
+ # https://docs.astral.sh/ruff/rules/#pydocstyle-d
32
+ "D", # pydocstyle: Docstring conventions
33
+ # https://docs.astral.sh/ruff/rules/#pyupgrade-up
34
+ "UP", # pyupgrade: Upgrade syntax for newer Python versions
35
+ # https://docs.astral.sh/ruff/rules/#flake8-2020-ytt
36
+ "YTT", # flake8-2020: Checks for sys.version and sys.version_info usage
37
+ # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann
38
+ "ANN", # flake8-annotations: Type annotation checks
39
+ # https://docs.astral.sh/ruff/rules/#flake8-async-async
40
+ "ASYNC", # flake8-async: Asynchronous code checks
41
+ # https://docs.astral.sh/ruff/rules/#flake8-bandit-s
42
+ "S", # flake8-bandit: Security testing
43
+ # https://docs.astral.sh/ruff/rules/#flake8-blind-except-ble
44
+ "BLE", # flake8-blind-except: Checks for blind except: statements
45
+ # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt
46
+ "FBT", # flake8-boolean-trap: Boolean trap checks
47
+ # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b
48
+ "B", # flake8-bugbear: Finding likely bugs and design problems
49
+ # https://docs.astral.sh/ruff/rules/#flake8-builtins-a
50
+ "A", # flake8-builtins: Check for python builtins being used as variables or parameters
51
+ # https://docs.astral.sh/ruff/rules/#flake8-commas-com
52
+ "COM", # flake8-commas: Trailing commas checks
53
+ # https://docs.astral.sh/ruff/rules/#flake8-copyright-cpy
54
+ # "CPY", # flake8-copyright: Copyright notice checks (Note: requires `preview = true`)
55
+ # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4
56
+ "C4", # flake8-comprehensions: Write better list/set/dict comprehensions
57
+ # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz
58
+ "DTZ", # flake8-datetimez: Usage of unsafe naive datetime class
59
+ # https://docs.astral.sh/ruff/rules/#flake8-debugger-t10
60
+ "T10", # flake8-debugger: Check for pdb/ipdb imports and set_traces
61
+ # https://docs.astral.sh/ruff/rules/#flake8-django-dj
62
+ "DJ", # flake8-django: Django specific code quality checks
63
+ # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em
64
+ "EM", # flake8-errmsg: Nicer error messages
65
+ # https://docs.astral.sh/ruff/rules/#flake8-executable-exe
66
+ "EXE", # flake8-executable: Executable permissions and shebangs
67
+ # https://docs.astral.sh/ruff/rules/#flake8-future-annotations-fa
68
+ "FA", # flake8-future-annotations: Verify python 3.7+ from __future__ import annotations
69
+ # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc
70
+ "ISC", # flake8-implicit-str-concat: Implicit string concatenation checks
71
+ # https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn
72
+ "ICN", # flake8-import-conventions: Enforce standard naming for standard libraries
73
+ # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g
74
+ "G", # flake8-logging-format: Validate logging format strings
75
+ # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp
76
+ "INP", # flake8-no-pep420: Ban PEP-420 implicit namespace packages
77
+ # https://docs.astral.sh/ruff/rules/#flake8-pie-pie
78
+ "PIE", # flake8-pie: Misc. lints
79
+ # https://docs.astral.sh/ruff/rules/#flake8-print-t20
80
+ "T20", # flake8-print: Check for Print statements in python files
81
+ # https://docs.astral.sh/ruff/rules/#flake8-pyi-pyi
82
+ "PYI", # flake8-pyi: Linting for .pyi files
83
+ # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt
84
+ "PT", # flake8-pytest-style: Checking common style issues or inconsistencies with pytest
85
+ # https://docs.astral.sh/ruff/rules/#flake8-quotes-q
86
+ "Q", # flake8-quotes: Lint for quotes
87
+ # https://docs.astral.sh/ruff/rules/#flake8-raise-rse
88
+ "RSE", # flake8-raise: Find and correct raise statements
89
+ # https://docs.astral.sh/ruff/rules/#flake8-return-ret
90
+ "RET", # flake8-return: Check return values
91
+ # https://docs.astral.sh/ruff/rules/#flake8-self-slf
92
+ "SLF", # flake8-self: Private member access checks
93
+ # https://docs.astral.sh/ruff/rules/#flake8-slots-slot
94
+ "SLOT", # flake8-slots: Require __slots__ for subclasses of immutable types
95
+ # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim
96
+ "SIM", # flake8-simplify: Code simplification
97
+ # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid
98
+ "TID", # flake8-tidy-imports: Tidy imports
99
+ # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc
100
+ "TC", # flake8-type-checking: Move imports into type-checking blocks
101
+ # https://docs.astral.sh/ruff/rules/#flake8-gettext-int
102
+ "ARG", # flake8-unused-arguments: Unused argument checks
103
+ # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
104
+ "PTH", # flake8-use-pathlib: Use pathlib instead of os.path
105
+ # https://docs.astral.sh/ruff/rules/#flake8-todos-td
106
+ "TD", # flake8-todos: TODO comments formatting
107
+ # https://docs.astral.sh/ruff/rules/#flake8-fixme-fix
108
+ "FIX", # flake8-fixme: Check for FIXME, XXX and other developer notes
109
+ # https://docs.astral.sh/ruff/rules/#eradicate-era
110
+ "ERA", # eradicate: Found commented out code
111
+ # https://docs.astral.sh/ruff/rules/#pandas-vet-pd
112
+ "PD", # pandas-vet: Pandas code checks
113
+ # https://docs.astral.sh/ruff/rules/#pygrep-hooks-pgh
114
+ "PGH", # pygrep-hooks: Pygrep hooks
115
+ # https://docs.astral.sh/ruff/rules/#pylint-pl
116
+ "PL", # Pylint: Pylint rules
117
+ # https://docs.astral.sh/ruff/rules/#tryceratops-try
118
+ "TRY", # tryceratops: Prevent Exception Handling AntiPatterns
119
+ # https://docs.astral.sh/ruff/rules/#flynt-fly
120
+ "FLY", # flynt: Convert string formatting to f-strings
121
+ # https://docs.astral.sh/ruff/rules/#numpy-specific-rules-npy
122
+ "NPY", # NumPy-specific rules: NumPy conventions
123
+ # https://docs.astral.sh/ruff/rules/#airflow-air
124
+ "AIR", # Airflow: Airflow best practices
125
+ # https://docs.astral.sh/ruff/rules/#perflint-perf
126
+ "PERF", # Perflint: Performance anti-patterns
127
+ # https://docs.astral.sh/ruff/rules/#refurb-furb
128
+ "FURB", # refurb: Modernize Python code
129
+ # https://docs.astral.sh/ruff/rules/#flake8-logging-log
130
+ "LOG", # flake8-logging: Better logging practices
131
+ # https://docs.astral.sh/ruff/rules/#pydoclint-doc
132
+ # "DOC", # pydoclint: Validate docstrings against function signatures (Note: requires `preview = true`)
133
+ # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
134
+ "RUF" # Ruff-specific rules: Rules unique to Ruff
135
+ ]
136
+
137
+ # We don't ignore any rules in this config, as it aims to be exhaustive.
138
+ # WARNING: Some of the rules enabled above may be mutually exclusive or
139
+ # conflict with each other (e.g., D212 and D213). You will likely need to
140
+ # remove some rules from the `select` list or add them to the `ignore`
141
+ # list below based on your preferences and project conventions.
142
+ #
143
+ # Below is a list of lint rules that conflict with the Ruff formatter.
144
+ # We ignore these by default so `ruff format` will run without warnings.
145
+ # See: https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
146
+ ignore = [
147
+ "W191", # tab-indentation
148
+ "E111", # indentation-with-invalid-multiple
149
+ "E114", # indentation-with-invalid-multiple-comment
150
+ "E117", # over-indented
151
+ "D206", # docstring-tab-indentation
152
+ "D300", # triple-single-quotes
153
+ "Q000", # bad-quotes-inline-string
154
+ "Q001", # bad-quotes-multiline-string
155
+ "Q002", # bad-quotes-docstring
156
+ "Q003", # avoidable-escaped-quote
157
+ "Q004", # unnecessary-escaped-quote
158
+ "COM812", # missing-trailing-comma
159
+ "COM819", # prohibited-trailing-comma
160
+ "ISC001", # single-line-implicit-string-concatenation
161
+ "ISC002", # multi-line-implicit-string-concatenation
162
+ ]
163
+
164
+ # Allow autofix for all enabled rules (when `--fix`) is provided.
165
+ fixable = ["ALL"]
166
+ unfixable = []
167
+
168
+ # Allow unused variables when underscore-prefixed.
169
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
170
+
171
+ [lint.mccabe]
172
+ # Flag errors (`C901`) whenever the complexity level exceeds 10.
173
+ # Default: 10
174
+ max-complexity = 10
175
+
176
+ [lint.pydocstyle]
177
+ # Which style to use for docstrings. Can be "google", "numpy", or "pep257".
178
+ # Default: "pep257"
179
+ convention = "pep257"
180
+
181
+ [lint.flake8-quotes]
182
+ # The style to use for inline strings: "single" or "double"
183
+ # Default: "double"
184
+ inline-quotes = "double"
185
+ # The style to use for multiline strings
186
+ # Default: "double"
187
+ multiline-quotes = "double"
188
+ # The style to use for docstrings
189
+ # Default: "double"
190
+ docstring-quotes = "double"
191
+ # Whether to avoid escaping quotes if the other quote type would save an escape
192
+ # Default: true
193
+ avoid-escape = true
194
+
195
+ [lint.flake8-tidy-imports]
196
+ # Ban absolute imports from specific modules
197
+ # ban-relative-imports = "all" # Default: "parents"
198
+
199
+ [lint.isort]
200
+ # How to group imports: "std", "third-party", "first-party", "local-folder".
201
+ # Default: ["future", "standard-library", "third-party", "first-party", "local-folder"]
202
+ # section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
203
+
204
+ [format]
205
+ # Like Black, use double quotes for strings.
206
+ quote-style = "double"
207
+
208
+ # Like Black, indent with spaces, rather than tabs.
209
+ indent-style = "space"
210
+
211
+ # Like Black, respect magic trailing commas.
212
+ skip-magic-trailing-comma = false
213
+
214
+ # Like Black, automatically detect the appropriate line ending.
215
+ line-ending = "auto"
216
+
217
+ # Enable auto-formatting of code examples in docstrings. Markdown,
218
+ # reStructuredText code/literal blocks and doctests are all supported.
219
+ #
220
+ # This is currently disabled by default, but it is planned for this
221
+ # to be opt-out in the future.
222
+ docstring-code-format = true
223
+
224
+ # Set the line length limit used when formatting code snippets in
225
+ # docstrings.
226
+ #
227
+ # This only has an effect when the `docstring-code-format` setting is
228
+ # enabled.
229
+ docstring-code-line-length = "dynamic"
230
+
231
+ [lint.flake8-pytest-style]
232
+ # Whether to require parentheses for pytest fixtures.
233
+ # Default: true
234
+ fixture-parentheses = true
235
+ # The type of pytest.mark.parametrize names to use: "tuple", "list", or "csv"
236
+ # Default: "tuple"
237
+ parametrize-names-type = "tuple"
238
+
239
+ [lint.pylint]
240
+ # The maximum number of arguments allowed for a function.
241
+ # Default: 5
242
+ max-args = 5
243
+ # The maximum return statements allowed for a function.
244
+ # Default: 6
245
+ max-returns = 6
246
+ # The maximum number of branches allowed for a function.
247
+ # Default: 12
248
+ max-branches = 12
249
+ # The maximum number of local variables allowed for a function.
250
+ # Default: 15
251
+ max-locals = 15
252
+
253
+ [lint.pep8-naming]
254
+ # Additional functions or methods that should be exempt from the lower_case_with_underscores rule.
255
+ # Default: []
256
+ ignore-names = []
257
+ # Additional decorators that should be treated as classmethod.
258
+ # Default: []
259
+ classmethod-decorators = [
260
+ "pydantic.validator",
261
+ "pydantic.root_validator",
262
+ ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ruff-sync"
3
- version = "0.0.4.dev1"
3
+ version = "0.0.5.dev1"
4
4
  description = "Synchronize Ruff linter configuration across projects"
5
5
  keywords = ["ruff", "linter", "config", "synchronize", "python", "linting", "automation", "tomlkit"]
6
6
  authors = [
@@ -23,7 +23,7 @@ from tomlkit import TOMLDocument, table
23
23
  from tomlkit.items import Table
24
24
  from tomlkit.toml_file import TOMLFile
25
25
 
26
- __version__ = "0.0.4.dev1"
26
+ __version__ = "0.0.5.dev1"
27
27
 
28
28
  _DEFAULT_EXCLUDE: Final[set[str]] = {"lint.per-file-ignores"}
29
29
  _GITHUB_REPO_PATH_PARTS_COUNT: Final[int] = 2
@@ -216,17 +216,35 @@ def _get_cli_parser() -> ArgumentParser:
216
216
  return parser
217
217
 
218
218
 
219
+ def _get_target_path(path: str | None) -> str:
220
+ """Resolve the target path for configuration files.
221
+
222
+ If the path indicates a .toml file, it's treated as a direct file path.
223
+ Otherwise, it appends 'pyproject.toml' to the path.
224
+ """
225
+ if not path:
226
+ return "pyproject.toml"
227
+
228
+ # Use PurePosixPath to handle URL-style paths consistently
229
+ posix_path = pathlib.PurePosixPath(path.strip("/"))
230
+ if posix_path.suffix == ".toml":
231
+ return str(posix_path)
232
+
233
+ return str(posix_path / "pyproject.toml")
234
+
235
+
219
236
  def _convert_github_url(url: URL, branch: str = "main", path: str = "") -> URL:
220
237
  """Convert a GitHub URL to its corresponding raw content URL.
221
238
 
222
239
  Supports:
223
240
  - Blob URLs: https://github.com/org/repo/blob/branch/path/to/file
224
- - Repo URLs: https://github.com/org/repo (defaults to {branch}/{path}/pyproject.toml)
241
+ - Repo URLs: https://github.com/org/repo (defaults to {branch}/{path}/pyproject.toml if path
242
+ doesn't end in .toml)
225
243
 
226
244
  Args:
227
245
  url (URL): The GitHub URL to be converted.
228
246
  branch (str): The default branch to use for repo URLs.
229
- path (str): The directory prefix for pyproject.toml.
247
+ path (str): The directory prefix for pyproject.toml, or a direct path to a .toml file.
230
248
 
231
249
  Returns:
232
250
  URL: The corresponding raw content URL.
@@ -243,10 +261,10 @@ def _convert_github_url(url: URL, branch: str = "main", path: str = "") -> URL:
243
261
  path_parts = [p for p in url.path.split("/") if p]
244
262
  if len(path_parts) == _GITHUB_REPO_PATH_PARTS_COUNT:
245
263
  org, repo = path_parts
246
- target_path = f"{path.strip('/')}/pyproject.toml" if path else "pyproject.toml"
264
+ target_path = _get_target_path(path)
247
265
  raw_url = url.copy_with(
248
266
  host=_GITHUB_RAW_HOST,
249
- path=f"/{org}/{repo}/{branch}/{target_path}",
267
+ path=str(pathlib.PurePosixPath("/", org, repo, branch, target_path)),
250
268
  )
251
269
  LOGGER.info(f"Converting GitHub repo URL to raw content URL: {raw_url}")
252
270
  return raw_url
@@ -260,12 +278,13 @@ def _convert_gitlab_url(url: URL, branch: str = "main", path: str = "") -> URL:
260
278
 
261
279
  Supports:
262
280
  - Blob URLs: https://gitlab.com/org/repo/-/blob/branch/path/to/file
263
- - Repo URLs: https://gitlab.com/org/repo (defaults to {branch}/{path}/pyproject.toml)
281
+ - Repo URLs: https://gitlab.com/org/repo (defaults to {branch}/{path}/pyproject.toml if path
282
+ doesn't end in .toml)
264
283
 
265
284
  Args:
266
285
  url (URL): The GitLab URL to be converted.
267
286
  branch (str): The default branch to use for repo URLs.
268
- path (str): The directory prefix for pyproject.toml.
287
+ path (str): The directory prefix for pyproject.toml, or a direct path to a .toml file.
269
288
 
270
289
  Returns:
271
290
  URL: The corresponding raw content URL.
@@ -283,8 +302,10 @@ def _convert_gitlab_url(url: URL, branch: str = "main", path: str = "") -> URL:
283
302
  # Avoid empty paths or just a slash
284
303
  path_prefix = url.path.rstrip("/")
285
304
  if path_prefix:
286
- target_path = f"{path.strip('/')}/pyproject.toml" if path else "pyproject.toml"
287
- raw_url = url.copy_with(path=f"{path_prefix}/-/raw/{branch}/{target_path}")
305
+ target_path = _get_target_path(path)
306
+ raw_url = url.copy_with(
307
+ path=str(pathlib.PurePosixPath(path_prefix, "-", "raw", branch, target_path))
308
+ )
288
309
  LOGGER.info(f"Converting GitLab repo URL to raw content URL: {raw_url}")
289
310
  return raw_url
290
311
 
@@ -297,6 +318,32 @@ def is_git_url(url: URL) -> bool:
297
318
  return str(url).startswith("git@") or url.scheme in ("ssh", "git", "git+ssh")
298
319
 
299
320
 
321
+ def to_git_url(url: URL) -> URL | None:
322
+ """
323
+ Attempt to convert a browser or raw URL to a git (SSH) URL.
324
+
325
+ Supports GitHub and GitLab.
326
+ """
327
+ if is_git_url(url):
328
+ return url
329
+
330
+ if url.host in _GITHUB_HOSTS or url.host == _GITHUB_RAW_HOST:
331
+ path_parts = [p for p in url.path.split("/") if p]
332
+ if len(path_parts) >= _GITHUB_REPO_PATH_PARTS_COUNT:
333
+ org, repo = path_parts[:_GITHUB_REPO_PATH_PARTS_COUNT]
334
+ repo = repo.removesuffix(".git")
335
+ return URL(f"git@github.com:{org}/{repo}.git")
336
+
337
+ if url.host in _GITLAB_HOSTS:
338
+ path = url.path.strip("/")
339
+ project_path = path.split("/-/")[0] if "/-/" in path else path
340
+ if project_path:
341
+ project_path = project_path.removesuffix(".git")
342
+ return URL(f"git@{url.host}:{project_path}.git")
343
+
344
+ return None
345
+
346
+
300
347
  def resolve_raw_url(url: URL, branch: str = "main", path: str | None = None) -> URL:
301
348
  """Convert a GitHub or GitLab repository/blob URL to a raw content URL.
302
349
 
@@ -367,11 +414,7 @@ async def fetch_upstream_config(
367
414
  capture_output=True,
368
415
  text=True,
369
416
  )
370
- target_path = (
371
- pathlib.Path(path.strip("/")) / "pyproject.toml"
372
- if path
373
- else pathlib.Path("pyproject.toml")
374
- )
417
+ target_path = pathlib.Path(_get_target_path(path))
375
418
 
376
419
  # Restore just the pyproject_toml file
377
420
  restore_cmd = [
@@ -424,7 +467,29 @@ async def fetch_upstream_config(
424
467
  content = await asyncio.to_thread(_git_clone_and_read)
425
468
  return StringIO(content)
426
469
 
427
- return await download(url, client)
470
+ try:
471
+ return await download(url, client)
472
+ except httpx.HTTPStatusError as e:
473
+ msg = f"HTTP error {e.response.status_code} when downloading from {url}"
474
+ git_url = to_git_url(url)
475
+ if git_url:
476
+ # sys.argv[1] might be -v or something else when running via pytest
477
+ try:
478
+ cmd = sys.argv[1]
479
+ if cmd not in ("pull", "check"):
480
+ cmd = "pull"
481
+ except IndexError:
482
+ cmd = "pull"
483
+ msg += (
484
+ f"\n\n💡 Check the URL and your permissions. "
485
+ "You might want to try cloning via git instead:\n\n"
486
+ f" ruff-sync {cmd} {git_url}"
487
+ )
488
+ else:
489
+ msg += "\n\n💡 Check the URL and your permissions."
490
+
491
+ # Re-raise with a more helpful message while preserving the original exception context
492
+ raise httpx.HTTPStatusError(msg, request=e.request, response=e.response) from None
428
493
 
429
494
 
430
495
  def is_ruff_toml_file(path_or_url: str) -> bool:
@@ -1,9 +1,32 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import httpx
3
4
  import pytest
4
- from httpx import URL
5
+ from httpx import URL, AsyncClient
5
6
 
6
- from ruff_sync import resolve_raw_url
7
+ from ruff_sync import fetch_upstream_config, is_ruff_toml_file, resolve_raw_url, to_git_url
8
+
9
+
10
+ @pytest.mark.parametrize(
11
+ "path_or_url,expected",
12
+ [
13
+ ("ruff.toml", True),
14
+ (".ruff.toml", True),
15
+ ("configs/ruff.toml", True),
16
+ ("pyproject.toml", False),
17
+ ("https://example.com/ruff.toml", True),
18
+ ("https://example.com/ruff.toml?ref=main", True),
19
+ ("https://example.com/ruff.toml#L10", True),
20
+ ("https://example.com/path/to/ruff.toml?query=1#frag", True),
21
+ ("https://example.com/pyproject.toml?file=ruff.toml", False),
22
+ ("https://example.com/ruff.toml/other", False),
23
+ # Case where it's not a URL but has query/fragment characters
24
+ ("ruff.toml?raw=1", True),
25
+ ("ruff.toml#section", True),
26
+ ],
27
+ )
28
+ def test_is_ruff_toml_file(path_or_url: str, expected: bool):
29
+ assert is_ruff_toml_file(path_or_url) is expected
7
30
 
8
31
 
9
32
  @pytest.mark.parametrize(
@@ -111,3 +134,85 @@ def test_raw_url_with_branch_and_path(input_url: str, branch: str, path: str, ex
111
134
  url = URL(input_url)
112
135
  result = resolve_raw_url(url, branch=branch, path=path)
113
136
  assert str(result) == expected_url
137
+
138
+
139
+ @pytest.mark.parametrize(
140
+ "input_url, expected_git_url",
141
+ [
142
+ # GitHub Browser URLs
143
+ (
144
+ "https://github.com/pydantic/pydantic/blob/main/pyproject.toml",
145
+ "git@github.com:pydantic/pydantic.git",
146
+ ),
147
+ (
148
+ "https://github.com/org/repo/blob/develop/config/ruff.toml",
149
+ "git@github.com:org/repo.git",
150
+ ),
151
+ # GitHub Repo URLs
152
+ (
153
+ "https://github.com/pydantic/pydantic",
154
+ "git@github.com:pydantic/pydantic.git",
155
+ ),
156
+ # GitHub Raw URLs
157
+ (
158
+ "https://raw.githubusercontent.com/pydantic/pydantic/main/pyproject.toml",
159
+ "git@github.com:pydantic/pydantic.git",
160
+ ),
161
+ # GitLab Repo URLs
162
+ (
163
+ "https://gitlab.com/gitlab-org/gitlab",
164
+ "git@gitlab.com:gitlab-org/gitlab.git",
165
+ ),
166
+ (
167
+ "https://gitlab.com/gitlab-org/nested/group/sub-a/sub-b/project",
168
+ "git@gitlab.com:gitlab-org/nested/group/sub-a/sub-b/project.git",
169
+ ),
170
+ # GitLab Blob URLs
171
+ (
172
+ "https://gitlab.com/gitlab-org/gitlab/-/blob/master/pyproject.toml",
173
+ "git@gitlab.com:gitlab-org/gitlab.git",
174
+ ),
175
+ # Already git URLs
176
+ (
177
+ "git@github.com:org/repo.git",
178
+ "git@github.com:org/repo.git",
179
+ ),
180
+ # Non-matching URLs
181
+ (
182
+ "https://example.com/pyproject.toml",
183
+ None,
184
+ ),
185
+ ],
186
+ )
187
+ def test_to_git_url(input_url: str, expected_git_url: str | None):
188
+ url = URL(input_url)
189
+ result = to_git_url(url)
190
+ if expected_git_url is None:
191
+ assert result is None
192
+ else:
193
+ assert str(result) == expected_git_url
194
+
195
+
196
+ @pytest.mark.asyncio
197
+ async def test_fetch_upstream_config_http_error_with_git_suggestion(monkeypatch):
198
+ url = URL("https://github.com/org/repo/blob/main/pyproject.toml")
199
+
200
+ async def mock_get(*args, **kwargs):
201
+ # Create a mock response with 404 status
202
+ request = httpx.Request("GET", url)
203
+ response = httpx.Response(404, request=request)
204
+ response.raise_for_status()
205
+
206
+ # We need to mock the client's get method
207
+ # Since fetch_upstream_config uses the client passed to it
208
+
209
+ async with AsyncClient() as client:
210
+ monkeypatch.setattr(client, "get", mock_get)
211
+
212
+ with pytest.raises(httpx.HTTPStatusError) as excinfo:
213
+ await fetch_upstream_config(url, client, branch="main", path="")
214
+
215
+ error_msg = str(excinfo.value)
216
+ assert "HTTP error 404" in error_msg
217
+ assert "git@github.com:org/repo.git" in error_msg
218
+ assert "ruff-sync pull" in error_msg
@@ -990,7 +990,7 @@ wheels = [
990
990
 
991
991
  [[package]]
992
992
  name = "ruff-sync"
993
- version = "0.0.4.dev1"
993
+ version = "0.0.5.dev1"
994
994
  source = { editable = "." }
995
995
  dependencies = [
996
996
  { name = "httpx" },
@@ -1,31 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import pytest
4
-
5
- from ruff_sync import is_ruff_toml_file
6
-
7
-
8
- @pytest.mark.parametrize(
9
- "path_or_url,expected",
10
- [
11
- ("ruff.toml", True),
12
- (".ruff.toml", True),
13
- ("configs/ruff.toml", True),
14
- ("pyproject.toml", False),
15
- ("https://example.com/ruff.toml", True),
16
- ("https://example.com/ruff.toml?ref=main", True),
17
- ("https://example.com/ruff.toml#L10", True),
18
- ("https://example.com/path/to/ruff.toml?query=1#frag", True),
19
- ("https://example.com/pyproject.toml?file=ruff.toml", False),
20
- ("https://example.com/ruff.toml/other", False),
21
- # Case where it's not a URL but has query/fragment characters
22
- ("ruff.toml?raw=1", True),
23
- ("ruff.toml#section", True),
24
- ],
25
- )
26
- def test_is_ruff_toml_file(path_or_url: str, expected: bool):
27
- assert is_ruff_toml_file(path_or_url) is expected
28
-
29
-
30
- if __name__ == "__main__":
31
- pytest.main([__file__, "-vv"])
File without changes
File without changes