ruff-sync 0.1.0.dev1__tar.gz → 0.1.0.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.
- ruff_sync-0.1.0.dev2/.agents/skills/release-notes-generation/SKILL.md +82 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/AGENTS.md +1 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/PKG-INFO +11 -6
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/README.md +10 -5
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/docs/configuration.md +13 -1
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/docs/index.md +1 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/docs/usage.md +7 -5
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/pyproject.toml +1 -1
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/src/ruff_sync/cli.py +50 -21
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/src/ruff_sync/core.py +52 -43
- ruff_sync-0.1.0.dev2/tests/lifecycle_tomls/multi_upstream_final.toml +8 -0
- ruff_sync-0.1.0.dev2/tests/lifecycle_tomls/multi_upstream_initial.toml +7 -0
- ruff_sync-0.1.0.dev2/tests/lifecycle_tomls/multi_upstream_up1.toml +5 -0
- ruff_sync-0.1.0.dev2/tests/lifecycle_tomls/multi_upstream_up2.toml +5 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/test_basic.py +153 -99
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/test_check.py +65 -6
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/test_deprecation.py +1 -1
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/test_e2e.py +51 -6
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/test_scaffold.py +6 -6
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/uv.lock +1 -1
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.agents/TESTING.md +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.agents/skills/mkdocs-generation/SKILL.md +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.agents/skills/mkdocs-generation/examples.md +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.agents/skills/mkdocs-generation/templates/api-reference.md +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.agents/skills/mkdocs-generation/templates/getting-started.md +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.agents/skills/mkdocs-generation/templates/index.md +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.agents/skills/mkdocs-generation/templates/mkdocs.yml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.agents/workflows/add-test-case.md +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.git-blame-ignore-revs +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.github/dependabot.yml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.github/workflows/ci.yaml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.github/workflows/complexity.yaml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.github/workflows/docs.yaml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.gitignore +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/.pre-commit-config.yaml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/LICENSE.md +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/codecov.yml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/configs/fastapi/ruff.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/configs/kitchen-sink/ruff.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/docs/assets/ruff_sync_banner.png +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/docs/ci-integration.md +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/docs/gen_ref_pages.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/docs/installation.md +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/docs/troubleshooting.md +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/mkdocs.yml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/scripts/check_dogfood.sh +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/scripts/gitclone_dogfood.sh +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/scripts/pull_dogfood.sh +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/skills-lock.json +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/src/ruff_sync/__init__.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/src/ruff_sync/__main__.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tasks.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/__init__.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/conftest.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/no_changes_final.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/no_changes_initial.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/no_changes_upstream.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/no_dotted_keys_final.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/no_dotted_keys_initial.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/no_dotted_keys_upstream.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/no_ruff_cfg_final.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/no_ruff_cfg_initial.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/no_ruff_cfg_upstream.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/readme_excludes_final.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/readme_excludes_initial.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/readme_excludes_upstream.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/standard_final.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/standard_initial.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/lifecycle_tomls/standard_upstream.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/ruff.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/test_config_validation.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/test_corner_cases.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/test_git_fetch.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/test_project.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/test_toml_operations.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/test_url_handling.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/test_whitespace.py +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/w_ruff_sync_cfg/pyproject.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/wo_ruff_cfg/pyproject.toml +0 -0
- {ruff_sync-0.1.0.dev1 → ruff_sync-0.1.0.dev2}/tests/wo_ruff_sync_cfg/pyproject.toml +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: release-notes-generation
|
|
3
|
+
description: Draft professional and categorized release notes for ruff-sync using GitHub CLI, git history, and the `invoke release` task.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Release Notes Generation
|
|
7
|
+
|
|
8
|
+
This skill guides you through drafting high-quality release notes for `ruff-sync`. It leverages the `invoke release` task and GitHub CLI for context.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
- **GitHub CLI (`gh`)**: Must be authenticated.
|
|
13
|
+
- **Invoke**: Dev tasks are defined in `tasks.py`.
|
|
14
|
+
- **Git**: Recent history and tags must be available.
|
|
15
|
+
|
|
16
|
+
## Workflow
|
|
17
|
+
|
|
18
|
+
### 1. Gather Context
|
|
19
|
+
|
|
20
|
+
Before drafting, understand what has changed since the last release.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Get the latest release tag and notes
|
|
24
|
+
gh release list --limit 1
|
|
25
|
+
gh release view <tag>
|
|
26
|
+
|
|
27
|
+
# List merged PRs since the last tag
|
|
28
|
+
# Replace <tag> with the tag found above
|
|
29
|
+
gh pr list --state merged --search "merged:><tag-date>"
|
|
30
|
+
|
|
31
|
+
# Or simple git log
|
|
32
|
+
git log <tag>..HEAD --oneline
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Create Draft Release
|
|
36
|
+
|
|
37
|
+
Use the project's built-in release task to scaffold the release. This task automatically tags the release based on the version in `pyproject.toml` and uses GitHub's `--generate-notes` feature to create a starting point.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Create a draft release (default behavior of invoke release)
|
|
41
|
+
uv run invoke release --draft --skip-tests
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> [!NOTE]
|
|
45
|
+
> `invoke release` will:
|
|
46
|
+
> 1. Check if you are on `main`.
|
|
47
|
+
> 2. Check for clean git state.
|
|
48
|
+
> 3. Create a GitHub Release with `--generate-notes`.
|
|
49
|
+
|
|
50
|
+
### 3. Refine Release Notes
|
|
51
|
+
|
|
52
|
+
GitHub's automatically generated notes are a good start but often lack professional categorization and narrative. Use the following structure for final refinement:
|
|
53
|
+
|
|
54
|
+
#### Categorization
|
|
55
|
+
- **🚀 Features**: New capabilities added to `ruff_sync`.
|
|
56
|
+
- **🐞 Bug Fixes**: Issues resolved in CLI, merging logic, or HTTP handling.
|
|
57
|
+
- **✨ Improvements**: Enhancements to existing features, performance, or logging.
|
|
58
|
+
- **📖 Documentation**: Updates to `README.md`, `docs/`, or docstrings.
|
|
59
|
+
- **🛠️ Maintenance**: Dependency updates, CI changes, or test refactoring.
|
|
60
|
+
|
|
61
|
+
#### Writing Style
|
|
62
|
+
- Use clear, action-oriented language (e.g., "Add support...", "Fix issue where...", "Refactor...").
|
|
63
|
+
- Link to PRs and contributors using their GitHub handles.
|
|
64
|
+
- Include a "Breaking Changes" section if applicable (use `[!WARNING]` alerts).
|
|
65
|
+
|
|
66
|
+
### 4. Finalize
|
|
67
|
+
|
|
68
|
+
Once the notes are drafted and refined, you can view the draft on GitHub or update it via CLI.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# View the draft notes
|
|
72
|
+
gh release view v<version>
|
|
73
|
+
|
|
74
|
+
# Edit the draft (opens your editor)
|
|
75
|
+
gh release edit v<version> --notes "your new notes"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Tips
|
|
79
|
+
|
|
80
|
+
- **Consistency**: Refer to the `AGENTS.md` for project-specific terminology (e.g., "Upstream Layers").
|
|
81
|
+
- **Screenshots**: If the release includes significantly visible changes (e.g., new logging or CLI output formats), consider embedding a screenshot or recording in the notes.
|
|
82
|
+
- **Automated Summary**: You can ask the AI assistant to "Draft release notes based on the git log since <tag>" to get a structured summary before applying it to the release.
|
|
@@ -150,6 +150,7 @@ uv run coverage run -m pytest -vv
|
|
|
150
150
|
- Use `pathlib` over `os.path` (enforced by `PTH` rules).
|
|
151
151
|
- Prefer f-strings for logging (we ignore `G004`).
|
|
152
152
|
- Do not create custom exception classes for simple errors (`TRY003` is ignored).
|
|
153
|
+
- **Prefer `NamedTuple` for return types** over plain tuples to improve readability and type safety.
|
|
153
154
|
|
|
154
155
|
### TOML Handling
|
|
155
156
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ruff-sync
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.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://kilo59.github.io/ruff-sync/
|
|
@@ -163,6 +163,7 @@ Run `ruff-sync --help` for full details on all available options.
|
|
|
163
163
|
## Key Features
|
|
164
164
|
|
|
165
165
|
- 🏗️ **Format-preserving merges** — Uses [tomlkit](https://github.com/sdispater/tomlkit) under the hood, so your comments, whitespace, and TOML structure are preserved. No reformatting surprises.
|
|
166
|
+
- 📂 **Upstream Layers** — Merge configurations from several sources sequentially (e.g., base company config + team-specific overrides).
|
|
166
167
|
- 🌐 **GitHub & GitLab URL support** — Automatically converts GitHub/GitLab repository URLs, tree (directory) URLs, or blob (file) URLs to raw content URLs.
|
|
167
168
|
- 🔍 **Smart configuration discovery** — Point at a directory and `ruff-sync` will automatically find your config. It checks `pyproject.toml`, `ruff.toml`, and `.ruff.toml` (in that order).
|
|
168
169
|
- 📥 **Git clone support** — If the URL starts with `git@` or uses the `ssh://`, `git://`, or `git+ssh://` schemes, `ruff-sync` will perform an efficient shallow clone (using `--filter=blob:none` and `--no-checkout`) to safely extract the configuration with minimal network traffic.
|
|
@@ -200,8 +201,9 @@ Here are all the possible values that can be provided in `[tool.ruff-sync]` alon
|
|
|
200
201
|
|
|
201
202
|
```toml
|
|
202
203
|
[tool.ruff-sync]
|
|
203
|
-
# The source of truth URL for your Ruff configuration. (Required, unless passed via CLI)
|
|
204
|
-
|
|
204
|
+
# The source of truth URL(s) for your Ruff configuration. (Required, unless passed via CLI)
|
|
205
|
+
# Accepts a single string URL or a list of URLs.
|
|
206
|
+
upstream = ["https://github.com/my-org/standards", "https://github.com/my-org/team-tweaks"]
|
|
205
207
|
|
|
206
208
|
# A list of config keys to exclude from being synced. (Default: ["lint.per-file-ignores"])
|
|
207
209
|
# Use simple names for top-level keys, and dotted paths for nested keys.
|
|
@@ -329,10 +331,13 @@ When you run `ruff-sync check`, it follows this process to determine if your pro
|
|
|
329
331
|
```mermaid
|
|
330
332
|
flowchart TD
|
|
331
333
|
Start([Start]) --> Local[Read Local Configuration]
|
|
332
|
-
Local -->
|
|
333
|
-
|
|
334
|
+
Local --> Upstreams{For each Upstream}
|
|
335
|
+
Upstreams --> Download[Download/Clone Configuration]
|
|
336
|
+
Download --> Extract[Extract section if needed]
|
|
334
337
|
Extract --> Exclude[Apply Exclusions]
|
|
335
|
-
Exclude --> Merge[
|
|
338
|
+
Exclude --> Merge[Merge into in-memory Doc]
|
|
339
|
+
Merge --> Upstreams
|
|
340
|
+
Upstreams -- Done --> Comparison
|
|
336
341
|
|
|
337
342
|
subgraph Comparison [Comparison Logic]
|
|
338
343
|
direction TB
|
|
@@ -132,6 +132,7 @@ Run `ruff-sync --help` for full details on all available options.
|
|
|
132
132
|
## Key Features
|
|
133
133
|
|
|
134
134
|
- 🏗️ **Format-preserving merges** — Uses [tomlkit](https://github.com/sdispater/tomlkit) under the hood, so your comments, whitespace, and TOML structure are preserved. No reformatting surprises.
|
|
135
|
+
- 📂 **Upstream Layers** — Merge configurations from several sources sequentially (e.g., base company config + team-specific overrides).
|
|
135
136
|
- 🌐 **GitHub & GitLab URL support** — Automatically converts GitHub/GitLab repository URLs, tree (directory) URLs, or blob (file) URLs to raw content URLs.
|
|
136
137
|
- 🔍 **Smart configuration discovery** — Point at a directory and `ruff-sync` will automatically find your config. It checks `pyproject.toml`, `ruff.toml`, and `.ruff.toml` (in that order).
|
|
137
138
|
- 📥 **Git clone support** — If the URL starts with `git@` or uses the `ssh://`, `git://`, or `git+ssh://` schemes, `ruff-sync` will perform an efficient shallow clone (using `--filter=blob:none` and `--no-checkout`) to safely extract the configuration with minimal network traffic.
|
|
@@ -169,8 +170,9 @@ Here are all the possible values that can be provided in `[tool.ruff-sync]` alon
|
|
|
169
170
|
|
|
170
171
|
```toml
|
|
171
172
|
[tool.ruff-sync]
|
|
172
|
-
# The source of truth URL for your Ruff configuration. (Required, unless passed via CLI)
|
|
173
|
-
|
|
173
|
+
# The source of truth URL(s) for your Ruff configuration. (Required, unless passed via CLI)
|
|
174
|
+
# Accepts a single string URL or a list of URLs.
|
|
175
|
+
upstream = ["https://github.com/my-org/standards", "https://github.com/my-org/team-tweaks"]
|
|
174
176
|
|
|
175
177
|
# A list of config keys to exclude from being synced. (Default: ["lint.per-file-ignores"])
|
|
176
178
|
# Use simple names for top-level keys, and dotted paths for nested keys.
|
|
@@ -298,10 +300,13 @@ When you run `ruff-sync check`, it follows this process to determine if your pro
|
|
|
298
300
|
```mermaid
|
|
299
301
|
flowchart TD
|
|
300
302
|
Start([Start]) --> Local[Read Local Configuration]
|
|
301
|
-
Local -->
|
|
302
|
-
|
|
303
|
+
Local --> Upstreams{For each Upstream}
|
|
304
|
+
Upstreams --> Download[Download/Clone Configuration]
|
|
305
|
+
Download --> Extract[Extract section if needed]
|
|
303
306
|
Extract --> Exclude[Apply Exclusions]
|
|
304
|
-
Exclude --> Merge[
|
|
307
|
+
Exclude --> Merge[Merge into in-memory Doc]
|
|
308
|
+
Merge --> Upstreams
|
|
309
|
+
Upstreams -- Done --> Comparison
|
|
305
310
|
|
|
306
311
|
subgraph Comparison [Comparison Logic]
|
|
307
312
|
direction TB
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
| Key | Type | Default | Description |
|
|
8
8
|
| :--- | :--- | :--- | :--- |
|
|
9
|
-
| `upstream` | `str` | *Required* | The URL of the upstream `pyproject.toml` or `ruff.toml`. |
|
|
9
|
+
| `upstream` | `str \| list[str]` | *Required* | The URL(s) of the upstream `pyproject.toml` or `ruff.toml`. |
|
|
10
10
|
| `to` | `str` | `"."` | The local directory or file where configuration should be merged. |
|
|
11
11
|
| `exclude` | `list[str]` | `["lint.per-file-ignores"]` | A list of configuration keys to preserve locally. |
|
|
12
12
|
| `branch` | `str` | `"main"` | The default branch to use when resolving repository URLs. |
|
|
@@ -58,6 +58,18 @@ If your projects are on different Python versions but share linting rules:
|
|
|
58
58
|
exclude = ["target-version"]
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
#### Sequential merging of multiple sources
|
|
62
|
+
|
|
63
|
+
You can specify multiple upstream sources as a list. They will be merged in order, with later sources overriding earlier ones.
|
|
64
|
+
|
|
65
|
+
```toml
|
|
66
|
+
[tool.ruff-sync]
|
|
67
|
+
upstream = [
|
|
68
|
+
"https://github.com/my-org/shared-config",
|
|
69
|
+
"https://github.com/my-org/team-overrides",
|
|
70
|
+
]
|
|
71
|
+
```
|
|
72
|
+
|
|
61
73
|
## Deprecation Notes
|
|
62
74
|
|
|
63
75
|
- The key `source` in `[tool.ruff-sync]` is deprecated and will be removed in a future version. Use `to` instead.
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* **⚡ Fast & Lightweight**: Zero-config needed for most projects.
|
|
16
16
|
* **✨ Formatting Preserved**: Uses `tomlkit` to keep your comments, indentation, and whitespace exactly as they are.
|
|
17
17
|
* **🛡️ Smart Merging**: Safely merges nested tables (like `lint.per-file-ignores`) without overwriting local overrides.
|
|
18
|
+
* **📂 Upstream Layers**: Combine and merge configurations from several sources sequentially.
|
|
18
19
|
* **🔗 Flexible Sources**: Sync from GitHub, GitLab, raw URLs, or local files.
|
|
19
20
|
* **✅ CI Ready**: Built-in `check` command with semantic diffs for automated pipelines.
|
|
20
21
|
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
The `pull` command downloads the upstream configuration and merges it into your local file.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
ruff-sync pull [UPSTREAM_URL] [--to PATH] [--exclude KEY...] [--init]
|
|
12
|
+
ruff-sync pull [UPSTREAM_URL...] [--to PATH] [--exclude KEY...] [--init]
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
* **`UPSTREAM_URL
|
|
15
|
+
* **`UPSTREAM_URL...`**: One or more URLs to the source `pyproject.toml` or `ruff.toml`. Optional if defined in your local config.
|
|
16
16
|
* **`--to`**: Where to save the merged config (defaults to `.`).
|
|
17
17
|
* **`--exclude`**: Dotted paths of keys to keep local (e.g., `lint.isort`).
|
|
18
18
|
* **`--init`**: Create a new `pyproject.toml` if it doesn't exist.
|
|
@@ -22,7 +22,7 @@ ruff-sync pull [UPSTREAM_URL] [--to PATH] [--exclude KEY...] [--init]
|
|
|
22
22
|
The `check` command verifies if your local configuration matches the upstream one.
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
ruff-sync check [UPSTREAM_URL] [--semantic] [--diff]
|
|
25
|
+
ruff-sync check [UPSTREAM_URL...] [--semantic] [--diff]
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
* **`--semantic`**: Ignore "non-functional" differences like whitespace, comments, or key order.
|
|
@@ -92,7 +92,9 @@ graph TD
|
|
|
92
92
|
I --> L[Extract tool.ruff]
|
|
93
93
|
K --> L
|
|
94
94
|
L --> M[Apply Exclusions]
|
|
95
|
-
M --> N[Merge into
|
|
96
|
-
N -->
|
|
95
|
+
M --> N[Merge into in-memory TOML]
|
|
96
|
+
N --> Loop{More Upstreams?}
|
|
97
|
+
Loop -- Yes --> F
|
|
98
|
+
Loop -- No --> O[Save File]
|
|
97
99
|
O --> P[End]
|
|
98
100
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ruff-sync"
|
|
3
|
-
version = "0.1.0.
|
|
3
|
+
version = "0.1.0.dev2"
|
|
4
4
|
description = "Synchronize Ruff linter configuration across projects"
|
|
5
5
|
keywords = ["ruff", "linter", "config", "synchronize", "python", "linting", "automation", "tomlkit"]
|
|
6
6
|
authors = [
|
|
@@ -44,7 +44,7 @@ __all__: Final[list[str]] = [
|
|
|
44
44
|
"main",
|
|
45
45
|
]
|
|
46
46
|
|
|
47
|
-
__version__ = "0.1.0.
|
|
47
|
+
__version__ = "0.1.0.dev2"
|
|
48
48
|
|
|
49
49
|
LOGGER = logging.getLogger(__name__)
|
|
50
50
|
|
|
@@ -79,7 +79,7 @@ class Arguments(NamedTuple):
|
|
|
79
79
|
"""CLI arguments for the ruff-sync tool."""
|
|
80
80
|
|
|
81
81
|
command: str
|
|
82
|
-
upstream: URL
|
|
82
|
+
upstream: tuple[URL, ...]
|
|
83
83
|
to: pathlib.Path
|
|
84
84
|
exclude: Iterable[str]
|
|
85
85
|
verbose: int
|
|
@@ -102,6 +102,16 @@ class Arguments(NamedTuple):
|
|
|
102
102
|
return set(cls._fields) | {"source"}
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
class ResolvedArgs(NamedTuple):
|
|
106
|
+
"""Internal container for resolved arguments."""
|
|
107
|
+
|
|
108
|
+
upstream: tuple[URL, ...]
|
|
109
|
+
to: pathlib.Path
|
|
110
|
+
exclude: Iterable[str]
|
|
111
|
+
branch: str
|
|
112
|
+
path: str
|
|
113
|
+
|
|
114
|
+
|
|
105
115
|
@lru_cache(maxsize=1)
|
|
106
116
|
def get_config(
|
|
107
117
|
source: pathlib.Path,
|
|
@@ -173,9 +183,9 @@ def _get_cli_parser() -> ArgumentParser:
|
|
|
173
183
|
common_parser.add_argument(
|
|
174
184
|
"upstream",
|
|
175
185
|
type=URL,
|
|
176
|
-
nargs="
|
|
177
|
-
help=f"
|
|
178
|
-
" Optional if defined in [tool.ruff-sync].",
|
|
186
|
+
nargs="*",
|
|
187
|
+
help=f"One or more URLs to download the {RuffConfigFileName.PYPROJECT_TOML} file from."
|
|
188
|
+
" Optional if defined in [tool.ruff-sync]. Upstreams are merged sequentially.",
|
|
179
189
|
)
|
|
180
190
|
common_parser.add_argument(
|
|
181
191
|
"--to",
|
|
@@ -253,20 +263,41 @@ def _get_cli_parser() -> ArgumentParser:
|
|
|
253
263
|
PARSER: Final[ArgumentParser] = _get_cli_parser()
|
|
254
264
|
|
|
255
265
|
|
|
256
|
-
def _resolve_upstream(args: Any, config: Mapping[str, Any]) -> URL:
|
|
257
|
-
"""Resolve upstream URL from CLI or config."""
|
|
266
|
+
def _resolve_upstream(args: Any, config: Mapping[str, Any]) -> tuple[URL, ...]:
|
|
267
|
+
"""Resolve upstream URL(s) from CLI or config."""
|
|
258
268
|
if args.upstream:
|
|
259
|
-
|
|
269
|
+
upstreams = tuple(cast("Iterable[URL]", args.upstream))
|
|
270
|
+
# Log CLI upstreams for consistency with config sourcing
|
|
271
|
+
summary = (
|
|
272
|
+
f"{upstreams[0]}... ({len(upstreams)} total)"
|
|
273
|
+
if len(upstreams) > 1
|
|
274
|
+
else str(upstreams[0])
|
|
275
|
+
)
|
|
276
|
+
LOGGER.info(f"📂 Using upstream(s) from CLI: {summary}")
|
|
277
|
+
return upstreams
|
|
260
278
|
if "upstream" in config:
|
|
261
279
|
config_upstream = config["upstream"]
|
|
262
|
-
if
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
280
|
+
if isinstance(config_upstream, str):
|
|
281
|
+
upstream = (URL(config_upstream),)
|
|
282
|
+
LOGGER.info(f"📂 Using upstream from [tool.ruff-sync]: {upstream[0]}")
|
|
283
|
+
return upstream
|
|
284
|
+
if isinstance(config_upstream, list):
|
|
285
|
+
if not config_upstream:
|
|
286
|
+
PARSER.error("❌ [tool.ruff-sync].upstream list cannot be empty.")
|
|
287
|
+
if not all(isinstance(u, str) for u in config_upstream):
|
|
288
|
+
PARSER.error(
|
|
289
|
+
"❌ all items in [tool.ruff-sync].upstream must be strings, "
|
|
290
|
+
f"got {[type(u).__name__ for u in config_upstream]}"
|
|
291
|
+
)
|
|
292
|
+
upstreams = tuple(URL(u) for u in config_upstream)
|
|
293
|
+
LOGGER.info(f"📂 Using {len(upstreams)} upstreams from [tool.ruff-sync]")
|
|
294
|
+
return upstreams
|
|
295
|
+
|
|
296
|
+
PARSER.error(
|
|
297
|
+
"❌ upstream in [tool.ruff-sync] must be a string or a list of strings, "
|
|
298
|
+
f"got {type(config_upstream).__name__}"
|
|
299
|
+
)
|
|
300
|
+
|
|
270
301
|
PARSER.error(
|
|
271
302
|
"❌ the following arguments are required: upstream "
|
|
272
303
|
f"(or define it in [tool.ruff-sync] in {RuffConfigFileName.PYPROJECT_TOML}) 💥"
|
|
@@ -323,16 +354,14 @@ def _resolve_to(args: Any, config: Mapping[str, Any], initial_to: pathlib.Path)
|
|
|
323
354
|
return initial_to
|
|
324
355
|
|
|
325
356
|
|
|
326
|
-
def _resolve_args(
|
|
327
|
-
args: Any, config: Mapping[str, Any], initial_to: pathlib.Path
|
|
328
|
-
) -> tuple[URL, pathlib.Path, Iterable[str], str, str]:
|
|
357
|
+
def _resolve_args(args: Any, config: Mapping[str, Any], initial_to: pathlib.Path) -> ResolvedArgs:
|
|
329
358
|
"""Resolve upstream, to, exclude, branch, and path from CLI and config."""
|
|
330
359
|
upstream = _resolve_upstream(args, config)
|
|
331
360
|
to = _resolve_to(args, config, initial_to)
|
|
332
361
|
exclude = _resolve_exclude(args, config)
|
|
333
362
|
branch = _resolve_branch(args, config)
|
|
334
363
|
path = _resolve_path(args, config)
|
|
335
|
-
return upstream, to, exclude, branch, path
|
|
364
|
+
return ResolvedArgs(upstream, to, exclude, branch, path)
|
|
336
365
|
|
|
337
366
|
|
|
338
367
|
def main() -> int:
|
|
@@ -372,7 +401,7 @@ def main() -> int:
|
|
|
372
401
|
upstream, to_val, exclude, branch, path = _resolve_args(args, config, initial_to)
|
|
373
402
|
|
|
374
403
|
# Convert non-raw github/gitlab upstream url to the raw equivalent
|
|
375
|
-
upstream = resolve_raw_url(
|
|
404
|
+
upstream = tuple(resolve_raw_url(u, branch=branch, path=path) for u in upstream)
|
|
376
405
|
|
|
377
406
|
# Create Arguments object
|
|
378
407
|
exec_args = Arguments(
|
|
@@ -95,7 +95,7 @@ class FetchResult(NamedTuple):
|
|
|
95
95
|
class Config(TypedDict, total=False):
|
|
96
96
|
"""Configuration schema for [tool.ruff-sync] in pyproject.toml."""
|
|
97
97
|
|
|
98
|
-
upstream: str
|
|
98
|
+
upstream: str | list[str]
|
|
99
99
|
to: str
|
|
100
100
|
source: str # Deprecated
|
|
101
101
|
exclude: list[str]
|
|
@@ -107,12 +107,14 @@ class Config(TypedDict, total=False):
|
|
|
107
107
|
init: bool
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
def resolve_target_path(
|
|
110
|
+
def resolve_target_path(
|
|
111
|
+
to: pathlib.Path, upstreams: Iterable[str | URL] | None = None
|
|
112
|
+
) -> pathlib.Path:
|
|
111
113
|
"""Resolve the target path for configuration files.
|
|
112
114
|
|
|
113
115
|
If 'to' is a file, it's used directly.
|
|
114
116
|
Otherwise, it looks for existing ruff/pyproject.toml in the 'to' directory.
|
|
115
|
-
If none found, it defaults to pyproject.toml unless the upstream is a ruff.toml.
|
|
117
|
+
If none found, it defaults to pyproject.toml unless the first upstream is a ruff.toml.
|
|
116
118
|
"""
|
|
117
119
|
if to.is_file():
|
|
118
120
|
return to
|
|
@@ -123,8 +125,11 @@ def resolve_target_path(to: pathlib.Path, upstream_url: str | URL | None = None)
|
|
|
123
125
|
if candidate.exists():
|
|
124
126
|
return candidate
|
|
125
127
|
|
|
128
|
+
# Use the first upstream URL as a hint for the default file name
|
|
129
|
+
first_upstream = next(iter(upstreams), None) if upstreams else None
|
|
130
|
+
|
|
126
131
|
# If upstream is specified and is a ruff.toml, default to ruff.toml
|
|
127
|
-
if
|
|
132
|
+
if first_upstream and is_ruff_toml_file(first_upstream):
|
|
128
133
|
return to / RuffConfigFileName.RUFF_TOML
|
|
129
134
|
|
|
130
135
|
return to / RuffConfigFileName.PYPROJECT_TOML
|
|
@@ -686,6 +691,34 @@ def merge_ruff_toml(
|
|
|
686
691
|
return source
|
|
687
692
|
|
|
688
693
|
|
|
694
|
+
async def _merge_multiple_upstreams(
|
|
695
|
+
target_doc: TOMLDocument,
|
|
696
|
+
is_target_ruff_toml: bool,
|
|
697
|
+
args: Arguments,
|
|
698
|
+
client: httpx.AsyncClient,
|
|
699
|
+
) -> TOMLDocument:
|
|
700
|
+
"""Sequentially fetch and merge all upstreams into the target document."""
|
|
701
|
+
for upstream_url in args.upstream:
|
|
702
|
+
fetch_result = await fetch_upstream_config(
|
|
703
|
+
upstream_url, client, branch=args.branch, path=args.path
|
|
704
|
+
)
|
|
705
|
+
LOGGER.info(f"Loaded upstream file from {fetch_result.resolved_upstream}")
|
|
706
|
+
|
|
707
|
+
is_upstream_ruff_toml = is_ruff_toml_file(fetch_result.resolved_upstream)
|
|
708
|
+
|
|
709
|
+
upstream_ruff_toml = get_ruff_config(
|
|
710
|
+
fetch_result.buffer.read(),
|
|
711
|
+
is_ruff_toml=is_upstream_ruff_toml,
|
|
712
|
+
create_if_missing=False,
|
|
713
|
+
exclude=args.exclude,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
target_doc = merge_ruff_toml(
|
|
717
|
+
target_doc, upstream_ruff_toml, is_ruff_toml=is_target_ruff_toml
|
|
718
|
+
)
|
|
719
|
+
return target_doc
|
|
720
|
+
|
|
721
|
+
|
|
689
722
|
async def check(
|
|
690
723
|
args: Arguments,
|
|
691
724
|
) -> int:
|
|
@@ -720,30 +753,19 @@ async def check(
|
|
|
720
753
|
source_toml_file = TOMLFile(_source_toml_path)
|
|
721
754
|
source_doc = source_toml_file.read()
|
|
722
755
|
|
|
756
|
+
# Create a copy for comparison
|
|
757
|
+
source_doc_copy = tomlkit.parse(source_doc.as_string())
|
|
758
|
+
merged_doc = source_doc_copy
|
|
759
|
+
|
|
723
760
|
async with httpx.AsyncClient() as client:
|
|
724
|
-
|
|
725
|
-
|
|
761
|
+
merged_doc = await _merge_multiple_upstreams(
|
|
762
|
+
merged_doc,
|
|
763
|
+
is_target_ruff_toml=is_ruff_toml_file(_source_toml_path.name),
|
|
764
|
+
args=args,
|
|
765
|
+
client=client,
|
|
726
766
|
)
|
|
727
|
-
LOGGER.info(f"Loaded upstream file from {fetch_result.resolved_upstream}")
|
|
728
767
|
|
|
729
|
-
is_upstream_ruff_toml = is_ruff_toml_file(fetch_result.resolved_upstream)
|
|
730
768
|
is_source_ruff_toml = is_ruff_toml_file(_source_toml_path.name)
|
|
731
|
-
|
|
732
|
-
upstream_ruff_toml = get_ruff_config(
|
|
733
|
-
fetch_result.buffer.read(),
|
|
734
|
-
is_ruff_toml=is_upstream_ruff_toml,
|
|
735
|
-
create_if_missing=False,
|
|
736
|
-
exclude=args.exclude,
|
|
737
|
-
)
|
|
738
|
-
|
|
739
|
-
# Create a copy for comparison
|
|
740
|
-
source_doc_copy = tomlkit.parse(source_doc.as_string())
|
|
741
|
-
merged_doc = merge_ruff_toml(
|
|
742
|
-
source_doc_copy,
|
|
743
|
-
upstream_ruff_toml,
|
|
744
|
-
is_ruff_toml=is_source_ruff_toml,
|
|
745
|
-
)
|
|
746
|
-
|
|
747
769
|
if args.semantic:
|
|
748
770
|
if is_source_ruff_toml:
|
|
749
771
|
source_ruff = source_doc
|
|
@@ -837,28 +859,15 @@ async def pull(
|
|
|
837
859
|
)
|
|
838
860
|
return 1
|
|
839
861
|
|
|
840
|
-
# NOTE: there's no particular reason to use async here.
|
|
841
862
|
async with httpx.AsyncClient() as client:
|
|
842
|
-
|
|
843
|
-
|
|
863
|
+
source_doc = await _merge_multiple_upstreams(
|
|
864
|
+
source_doc,
|
|
865
|
+
is_target_ruff_toml=is_ruff_toml_file(_source_toml_path.name),
|
|
866
|
+
args=args,
|
|
867
|
+
client=client,
|
|
844
868
|
)
|
|
845
|
-
LOGGER.info(f"Loaded upstream file from {fetch_result.resolved_upstream}")
|
|
846
869
|
|
|
847
|
-
|
|
848
|
-
is_source_ruff_toml = is_ruff_toml_file(_source_toml_path.name)
|
|
849
|
-
|
|
850
|
-
upstream_ruff_toml = get_ruff_config(
|
|
851
|
-
fetch_result.buffer.read(),
|
|
852
|
-
is_ruff_toml=is_upstream_ruff_toml,
|
|
853
|
-
create_if_missing=False,
|
|
854
|
-
exclude=args.exclude,
|
|
855
|
-
)
|
|
856
|
-
merged_toml = merge_ruff_toml(
|
|
857
|
-
source_doc,
|
|
858
|
-
upstream_ruff_toml,
|
|
859
|
-
is_ruff_toml=is_source_ruff_toml,
|
|
860
|
-
)
|
|
861
|
-
source_toml_file.write(merged_toml)
|
|
870
|
+
source_toml_file.write(source_doc)
|
|
862
871
|
try:
|
|
863
872
|
rel_path = _source_toml_path.resolve().relative_to(pathlib.Path.cwd())
|
|
864
873
|
except ValueError:
|