ruff-sync 0.0.1.dev2__tar.gz → 0.0.1.dev4__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 (41) hide show
  1. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/.github/dependabot.yml +1 -1
  2. ruff_sync-0.0.1.dev2/README.md → ruff_sync-0.0.1.dev4/PKG-INFO +75 -16
  3. ruff_sync-0.0.1.dev2/PKG-INFO → ruff_sync-0.0.1.dev4/README.md +45 -28
  4. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/pyproject.toml +24 -2
  5. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/ruff_sync.py +83 -13
  6. ruff_sync-0.0.1.dev4/ruff_sync_banner.png +0 -0
  7. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/scripts/dogfood.sh +1 -1
  8. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/test_basic.py +122 -5
  9. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/test_e2e.py +1 -0
  10. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/test_project.py +12 -1
  11. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/test_whitespace.py +22 -0
  12. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/uv.lock +1 -1
  13. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/.agents/TESTING.md +0 -0
  14. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/.agents/workflows/add-test-case.md +0 -0
  15. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/.github/workflows/ci.yaml +0 -0
  16. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/.github/workflows/complexity.yaml +0 -0
  17. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/.gitignore +0 -0
  18. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/.pre-commit-config.yaml +0 -0
  19. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/AGENTS.md +0 -0
  20. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/LICENSE.md +0 -0
  21. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/codecov.yml +0 -0
  22. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tasks.py +0 -0
  23. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/__init__.py +0 -0
  24. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/lifecycle_tomls/no_changes_final.toml +0 -0
  25. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/lifecycle_tomls/no_changes_initial.toml +0 -0
  26. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/lifecycle_tomls/no_changes_upstream.toml +0 -0
  27. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/lifecycle_tomls/no_dotted_keys_final.toml +0 -0
  28. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/lifecycle_tomls/no_dotted_keys_initial.toml +0 -0
  29. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/lifecycle_tomls/no_dotted_keys_upstream.toml +0 -0
  30. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/lifecycle_tomls/no_ruff_cfg_final.toml +0 -0
  31. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/lifecycle_tomls/no_ruff_cfg_initial.toml +0 -0
  32. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/lifecycle_tomls/no_ruff_cfg_upstream.toml +0 -0
  33. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/lifecycle_tomls/standard_final.toml +0 -0
  34. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/lifecycle_tomls/standard_initial.toml +0 -0
  35. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/lifecycle_tomls/standard_upstream.toml +0 -0
  36. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/ruff.toml +0 -0
  37. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/test_corner_cases.py +0 -0
  38. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/test_toml_operations.py +0 -0
  39. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/w_ruff_sync_cfg/pyproject.toml +0 -0
  40. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/wo_ruff_cfg/pyproject.toml +0 -0
  41. {ruff_sync-0.0.1.dev2 → ruff_sync-0.0.1.dev4}/tests/wo_ruff_sync_cfg/pyproject.toml +0 -0
@@ -3,7 +3,7 @@
3
3
 
4
4
  version: 2
5
5
  updates:
6
- - package-ecosystem: "pip" # Documentation: For package managers such as pipenv and poetry, you need to use the pip YAML value.
6
+ - package-ecosystem: "uv"
7
7
  directory: "/" # Location of package manifests
8
8
  schedule:
9
9
  interval: daily
@@ -1,12 +1,61 @@
1
- [![codecov](https://codecov.io/gh/Kilo59/ruff-sync/graph/badge.svg?token=kMZw0XtoFW)](https://codecov.io/gh/Kilo59/ruff-sync)
2
- [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/Kilo59/ruff-sync/main.svg)](https://results.pre-commit.ci/latest/github/Kilo59/ruff-sync/main)
3
- [![Wily](https://img.shields.io/badge/%F0%9F%A6%8A%20wily-passing-brightgreen.svg)](https://wily.readthedocs.io/)
4
- [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
1
+ Metadata-Version: 2.4
2
+ Name: ruff-sync
3
+ Version: 0.0.1.dev4
4
+ Summary: Synchronize Ruff linter configuration across projects
5
+ Project-URL: Homepage, https://github.com/Kilo59/ruff-sync
6
+ Project-URL: Documentation, https://github.com/Kilo59/ruff-sync#readme
7
+ Project-URL: Repository, https://github.com/Kilo59/ruff-sync
8
+ Project-URL: Issues, https://github.com/Kilo59/ruff-sync/issues
9
+ Project-URL: Changelog, https://github.com/Kilo59/ruff-sync/releases
10
+ Author-email: Gabriel Gore <gabriel59kg@gmail.com>
11
+ License: MIT
12
+ License-File: LICENSE.md
13
+ Keywords: automation,config,linter,linting,python,ruff,synchronize,tomlkit
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Software Development :: Quality Assurance
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: httpx<1.0.0,>=0.27.0
28
+ Requires-Dist: tomlkit<2.0.0,>=0.12.3
29
+ Description-Content-Type: text/markdown
30
+
31
+ <p align="center">
32
+ <img src="https://raw.githubusercontent.com/Kilo59/ruff-sync/main/ruff_sync_banner.png" alt="ruff-sync banner" style="max-width: 600px; width: 100%; height: auto; margin-bottom: 1rem;">
33
+ <br>
34
+ <a href="https://pypi.org/project/ruff-sync/"><img src="https://img.shields.io/pypi/v/ruff-sync" alt="PyPI version"></a>
35
+ <a href="https://codecov.io/gh/Kilo59/ruff-sync"><img src="https://codecov.io/gh/Kilo59/ruff-sync/graph/badge.svg?token=kMZw0XtoFW" alt="codecov"></a>
36
+ <a href="https://results.pre-commit.ci/latest/github/Kilo59/ruff-sync/main"><img src="https://results.pre-commit.ci/badge/github/Kilo59/ruff-sync/main.svg" alt="pre-commit.ci status"></a>
37
+ <a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
38
+ <a href="https://wily.readthedocs.io/"><img src="https://img.shields.io/badge/%F0%9F%A6%8A%20wily-passing-brightgreen.svg" alt="Wily"></a>
39
+ </p>
5
40
 
6
41
  # ruff-sync
7
42
 
8
43
  **Keep your Ruff config consistent across every repo — automatically.**
9
44
 
45
+ ---
46
+
47
+ ### Table of Contents
48
+
49
+ - [The Problem](#the-problem)
50
+ - [How It Works](#how-it-works)
51
+ - [Quick Start](#quick-start)
52
+ - [Install](#install)
53
+ - [Usage](#usage)
54
+ - [Key Features](#key-features)
55
+ - [Configuration](#configuration)
56
+ - [Contributing](#contributing)
57
+ - [License](#license)
58
+
10
59
  `ruff-sync` is a CLI tool that pulls a canonical [Ruff](https://docs.astral.sh/ruff/) configuration from an upstream `pyproject.toml` (hosted anywhere — GitHub, GitLab, a raw URL) and merges it into your local project, preserving your comments, formatting, and project-specific overrides.
11
60
 
12
61
  ## The Problem
@@ -61,28 +110,30 @@ No package registry. No publishing step. Just a URL.
61
110
 
62
111
  ### Install
63
112
 
64
- <!-- ### PyPi Install
113
+ With [uv](https://docs.astral.sh/uv/) (recommended):
65
114
 
66
115
  ```console
67
- pip install ruff-sync
68
- ``` -->
116
+ uv tool install ruff-sync
117
+ ```
69
118
 
70
- From Git (with [`uv`](https://docs.astral.sh/uv/guides/tools/)):
119
+ With [pipx](https://pipx.pypa.io/stable/):
71
120
 
72
121
  ```console
73
- uv tool install git+https://github.com/Kilo59/ruff-sync
122
+ pipx install ruff-sync
74
123
  ```
75
124
 
76
- From Git (with [`pipx`](https://pipx.pypa.io/stable/) — recommended):
125
+ With [pip](https://pip.pypa.io/en/stable/):
77
126
 
78
127
  ```console
79
- pipx install git+https://github.com/Kilo59/ruff-sync
128
+ pip install ruff-sync
80
129
  ```
81
130
 
82
- Or with pip:
131
+ #### From Source (Bleeding Edge)
132
+
133
+ If you want the latest development version:
83
134
 
84
135
  ```console
85
- pip install git+https://github.com/Kilo59/ruff-sync
136
+ uv tool install git+https://github.com/Kilo59/ruff-sync
86
137
  ```
87
138
 
88
139
  ### Usage
@@ -91,26 +142,31 @@ pip install git+https://github.com/Kilo59/ruff-sync
91
142
  # Sync from a GitHub URL (blob URLs are auto-converted to raw)
92
143
  ruff-sync https://github.com/my-org/standards/blob/main/pyproject.toml
93
144
 
145
+ # Once configured in pyproject.toml (see below), simply run:
146
+ ruff-sync
147
+
94
148
  # Sync into a specific project directory
95
- ruff-sync https://github.com/my-org/standards/blob/main/pyproject.toml --source ./my-project
149
+ ruff-sync --source ./my-project
96
150
 
97
151
  # Exclude specific sections from being overwritten using dotted paths
98
- ruff-sync https://github.com/my-org/standards/blob/main/pyproject.toml --exclude lint.per-file-ignores lint.ignore
152
+ ruff-sync --exclude lint.per-file-ignores lint.ignore
99
153
  ```
100
154
 
101
155
  ### CLI Reference
102
156
 
103
157
  ```
104
- usage: ruff-sync [-h] [--source SOURCE] [--exclude EXCLUDE [EXCLUDE ...]] upstream
158
+ usage: ruff-sync [-h] [--source SOURCE] [--exclude EXCLUDE [EXCLUDE ...]] [-v] [upstream]
105
159
 
106
160
  positional arguments:
107
161
  upstream The URL to download the pyproject.toml file from.
162
+ Optional if defined in [tool.ruff-sync]
108
163
 
109
164
  optional arguments:
110
165
  -h, --help show this help message and exit
111
166
  --source SOURCE The directory to sync the pyproject.toml file to. Default: .
112
167
  --exclude EXCLUDE [EXCLUDE ...]
113
168
  Exclude certain ruff configs. Default: lint.per-file-ignores
169
+ -v, --verbose Increase verbosity. -v for INFO, -vv for DEBUG.
114
170
  ```
115
171
 
116
172
  ## Key Features
@@ -126,6 +182,9 @@ You can configure `ruff-sync` itself in your `pyproject.toml`:
126
182
 
127
183
  ```toml
128
184
  [tool.ruff-sync]
185
+ # The source of truth for your ruff configuration
186
+ upstream = "https://github.com/my-org/standards/blob/main/pyproject.toml"
187
+
129
188
  # Use simple names for top-level keys, and dotted paths for nested keys
130
189
  exclude = [
131
190
  "target-version", # A top-level key under [tool.ruff]
@@ -1,24 +1,31 @@
1
- Metadata-Version: 2.4
2
- Name: ruff-sync
3
- Version: 0.0.1.dev2
4
- Summary: Syncronize ruff linter config settings accross projects
5
- Author-email: Gabriel Gore <gabriel59kg@gmail.com>
6
- License: MIT
7
- License-File: LICENSE.md
8
- Requires-Python: >=3.10
9
- Requires-Dist: httpx<1.0.0,>=0.27.0
10
- Requires-Dist: tomlkit<2.0.0,>=0.12.3
11
- Description-Content-Type: text/markdown
12
-
13
- [![codecov](https://codecov.io/gh/Kilo59/ruff-sync/graph/badge.svg?token=kMZw0XtoFW)](https://codecov.io/gh/Kilo59/ruff-sync)
14
- [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/Kilo59/ruff-sync/main.svg)](https://results.pre-commit.ci/latest/github/Kilo59/ruff-sync/main)
15
- [![Wily](https://img.shields.io/badge/%F0%9F%A6%8A%20wily-passing-brightgreen.svg)](https://wily.readthedocs.io/)
16
- [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/Kilo59/ruff-sync/main/ruff_sync_banner.png" alt="ruff-sync banner" style="max-width: 600px; width: 100%; height: auto; margin-bottom: 1rem;">
3
+ <br>
4
+ <a href="https://pypi.org/project/ruff-sync/"><img src="https://img.shields.io/pypi/v/ruff-sync" alt="PyPI version"></a>
5
+ <a href="https://codecov.io/gh/Kilo59/ruff-sync"><img src="https://codecov.io/gh/Kilo59/ruff-sync/graph/badge.svg?token=kMZw0XtoFW" alt="codecov"></a>
6
+ <a href="https://results.pre-commit.ci/latest/github/Kilo59/ruff-sync/main"><img src="https://results.pre-commit.ci/badge/github/Kilo59/ruff-sync/main.svg" alt="pre-commit.ci status"></a>
7
+ <a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
8
+ <a href="https://wily.readthedocs.io/"><img src="https://img.shields.io/badge/%F0%9F%A6%8A%20wily-passing-brightgreen.svg" alt="Wily"></a>
9
+ </p>
17
10
 
18
11
  # ruff-sync
19
12
 
20
13
  **Keep your Ruff config consistent across every repo — automatically.**
21
14
 
15
+ ---
16
+
17
+ ### Table of Contents
18
+
19
+ - [The Problem](#the-problem)
20
+ - [How It Works](#how-it-works)
21
+ - [Quick Start](#quick-start)
22
+ - [Install](#install)
23
+ - [Usage](#usage)
24
+ - [Key Features](#key-features)
25
+ - [Configuration](#configuration)
26
+ - [Contributing](#contributing)
27
+ - [License](#license)
28
+
22
29
  `ruff-sync` is a CLI tool that pulls a canonical [Ruff](https://docs.astral.sh/ruff/) configuration from an upstream `pyproject.toml` (hosted anywhere — GitHub, GitLab, a raw URL) and merges it into your local project, preserving your comments, formatting, and project-specific overrides.
23
30
 
24
31
  ## The Problem
@@ -73,28 +80,30 @@ No package registry. No publishing step. Just a URL.
73
80
 
74
81
  ### Install
75
82
 
76
- <!-- ### PyPi Install
83
+ With [uv](https://docs.astral.sh/uv/) (recommended):
77
84
 
78
85
  ```console
79
- pip install ruff-sync
80
- ``` -->
86
+ uv tool install ruff-sync
87
+ ```
81
88
 
82
- From Git (with [`uv`](https://docs.astral.sh/uv/guides/tools/)):
89
+ With [pipx](https://pipx.pypa.io/stable/):
83
90
 
84
91
  ```console
85
- uv tool install git+https://github.com/Kilo59/ruff-sync
92
+ pipx install ruff-sync
86
93
  ```
87
94
 
88
- From Git (with [`pipx`](https://pipx.pypa.io/stable/) — recommended):
95
+ With [pip](https://pip.pypa.io/en/stable/):
89
96
 
90
97
  ```console
91
- pipx install git+https://github.com/Kilo59/ruff-sync
98
+ pip install ruff-sync
92
99
  ```
93
100
 
94
- Or with pip:
101
+ #### From Source (Bleeding Edge)
102
+
103
+ If you want the latest development version:
95
104
 
96
105
  ```console
97
- pip install git+https://github.com/Kilo59/ruff-sync
106
+ uv tool install git+https://github.com/Kilo59/ruff-sync
98
107
  ```
99
108
 
100
109
  ### Usage
@@ -103,26 +112,31 @@ pip install git+https://github.com/Kilo59/ruff-sync
103
112
  # Sync from a GitHub URL (blob URLs are auto-converted to raw)
104
113
  ruff-sync https://github.com/my-org/standards/blob/main/pyproject.toml
105
114
 
115
+ # Once configured in pyproject.toml (see below), simply run:
116
+ ruff-sync
117
+
106
118
  # Sync into a specific project directory
107
- ruff-sync https://github.com/my-org/standards/blob/main/pyproject.toml --source ./my-project
119
+ ruff-sync --source ./my-project
108
120
 
109
121
  # Exclude specific sections from being overwritten using dotted paths
110
- ruff-sync https://github.com/my-org/standards/blob/main/pyproject.toml --exclude lint.per-file-ignores lint.ignore
122
+ ruff-sync --exclude lint.per-file-ignores lint.ignore
111
123
  ```
112
124
 
113
125
  ### CLI Reference
114
126
 
115
127
  ```
116
- usage: ruff-sync [-h] [--source SOURCE] [--exclude EXCLUDE [EXCLUDE ...]] upstream
128
+ usage: ruff-sync [-h] [--source SOURCE] [--exclude EXCLUDE [EXCLUDE ...]] [-v] [upstream]
117
129
 
118
130
  positional arguments:
119
131
  upstream The URL to download the pyproject.toml file from.
132
+ Optional if defined in [tool.ruff-sync]
120
133
 
121
134
  optional arguments:
122
135
  -h, --help show this help message and exit
123
136
  --source SOURCE The directory to sync the pyproject.toml file to. Default: .
124
137
  --exclude EXCLUDE [EXCLUDE ...]
125
138
  Exclude certain ruff configs. Default: lint.per-file-ignores
139
+ -v, --verbose Increase verbosity. -v for INFO, -vv for DEBUG.
126
140
  ```
127
141
 
128
142
  ## Key Features
@@ -138,6 +152,9 @@ You can configure `ruff-sync` itself in your `pyproject.toml`:
138
152
 
139
153
  ```toml
140
154
  [tool.ruff-sync]
155
+ # The source of truth for your ruff configuration
156
+ upstream = "https://github.com/my-org/standards/blob/main/pyproject.toml"
157
+
141
158
  # Use simple names for top-level keys, and dotted paths for nested keys
142
159
  exclude = [
143
160
  "target-version", # A top-level key under [tool.ruff]
@@ -1,18 +1,40 @@
1
1
  [project]
2
2
  name = "ruff-sync"
3
- version = "0.0.1.dev2"
4
- description = "Syncronize ruff linter config settings accross projects"
3
+ version = "0.0.1.dev4"
4
+ description = "Synchronize Ruff linter configuration across projects"
5
+ keywords = ["ruff", "linter", "config", "synchronize", "python", "linting", "automation", "tomlkit"]
5
6
  authors = [
6
7
  { name = "Gabriel Gore", email = "gabriel59kg@gmail.com" }
7
8
  ]
8
9
  license = { text = "MIT" }
9
10
  readme = "README.md"
10
11
  requires-python = ">=3.10"
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Software Development :: Quality Assurance",
25
+ ]
11
26
  dependencies = [
12
27
  "httpx>=0.27.0,<1.0.0",
13
28
  "tomlkit>=0.12.3,<2.0.0",
14
29
  ]
15
30
 
31
+ [project.urls]
32
+ Homepage = "https://github.com/Kilo59/ruff-sync"
33
+ Documentation = "https://github.com/Kilo59/ruff-sync#readme"
34
+ Repository = "https://github.com/Kilo59/ruff-sync"
35
+ Issues = "https://github.com/Kilo59/ruff-sync/issues"
36
+ Changelog = "https://github.com/Kilo59/ruff-sync/releases"
37
+
16
38
  [project.scripts]
17
39
  ruff-sync = "ruff_sync:main"
18
40
 
@@ -3,12 +3,12 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import logging
5
5
  import pathlib
6
- import warnings
6
+ import sys
7
7
  from argparse import ArgumentParser
8
8
  from collections.abc import Iterable, Mapping
9
9
  from functools import lru_cache
10
10
  from io import StringIO
11
- from typing import Any, Final, Literal, NamedTuple, overload
11
+ from typing import Any, ClassVar, Final, Literal, NamedTuple, overload
12
12
 
13
13
  import httpx
14
14
  import tomlkit
@@ -17,17 +17,40 @@ from tomlkit import TOMLDocument, table
17
17
  from tomlkit.items import Table
18
18
  from tomlkit.toml_file import TOMLFile
19
19
 
20
- __version__ = "0.0.1.dev0"
20
+ __version__ = "0.0.1.dev4"
21
21
 
22
22
  _DEFAULT_EXCLUDE: Final[set[str]] = {"lint.per-file-ignores"}
23
23
 
24
24
  LOGGER = logging.getLogger(__name__)
25
25
 
26
26
 
27
+ class ColoredFormatter(logging.Formatter):
28
+ """Logging Formatter to add colors"""
29
+
30
+ RESET: ClassVar[str] = "\x1b[0m"
31
+ COLORS: ClassVar[Mapping[int, str]] = {
32
+ logging.DEBUG: "\x1b[36m", # Cyan
33
+ logging.INFO: "\x1b[32m", # Green
34
+ logging.WARNING: "\x1b[33m", # Yellow
35
+ logging.ERROR: "\x1b[31m", # Red
36
+ logging.CRITICAL: "\x1b[1;31m", # Bold Red
37
+ }
38
+
39
+ def __init__(self, fmt: str = "%(message)s") -> None:
40
+ super().__init__(fmt)
41
+
42
+ def format(self, record: logging.LogRecord) -> str: # type: ignore[explicit-override]
43
+ if sys.stderr.isatty():
44
+ color = self.COLORS.get(record.levelno, self.RESET)
45
+ return f"{color}{super().format(record)}{self.RESET}"
46
+ return super().format(record)
47
+
48
+
27
49
  class Arguments(NamedTuple):
28
50
  upstream: URL
29
51
  source: pathlib.Path
30
52
  exclude: Iterable[str]
53
+ verbose: int
31
54
 
32
55
  @classmethod
33
56
  @lru_cache(maxsize=1)
@@ -50,7 +73,7 @@ def get_config(
50
73
  if arg in Arguments.fields():
51
74
  cfg_result[arg] = value
52
75
  else:
53
- warnings.warn(f"Unknown ruff-sync configuration: {arg}", stacklevel=2)
76
+ LOGGER.warning(f"Unknown ruff-sync configuration: {arg}")
54
77
  return cfg_result
55
78
 
56
79
 
@@ -62,13 +85,14 @@ def _resolve_source(source: str | pathlib.Path) -> pathlib.Path:
62
85
 
63
86
 
64
87
  def _get_cli_parser() -> ArgumentParser:
65
- # TODO: determine if args was provided by user or not
66
88
  # https://docs.python.org/3/library/argparse.html#nargs
67
89
  parser = ArgumentParser()
68
90
  parser.add_argument(
69
91
  "upstream",
70
92
  type=URL,
71
- help="The URL to download the pyproject.toml file from.",
93
+ nargs="?",
94
+ help="The URL to download the pyproject.toml file from."
95
+ " Optional if defined in [tool.ruff-sync].",
72
96
  )
73
97
  parser.add_argument(
74
98
  "--source",
@@ -83,6 +107,13 @@ def _get_cli_parser() -> ArgumentParser:
83
107
  help=f"Exclude certain ruff configs. Default: {' '.join(_DEFAULT_EXCLUDE)}",
84
108
  default=None,
85
109
  )
110
+ parser.add_argument(
111
+ "-v",
112
+ "--verbose",
113
+ action="count",
114
+ default=0,
115
+ help="Increase verbosity. -v for INFO, -vv for DEBUG.",
116
+ )
86
117
  return parser
87
118
 
88
119
 
@@ -100,8 +131,10 @@ def github_url_to_raw_url(url: URL) -> URL:
100
131
  raw_url_str = url_str.replace("github.com", "raw.githubusercontent.com").replace(
101
132
  "/blob/", "/"
102
133
  )
134
+ LOGGER.debug(f"Converting GitHub URL to raw content URL: {raw_url_str}")
103
135
  return httpx.URL(raw_url_str)
104
136
  else:
137
+ LOGGER.debug("URL is not a GitHub URL, returning as is.")
105
138
  return url
106
139
 
107
140
 
@@ -147,7 +180,7 @@ def get_ruff_tool_table(
147
180
  except KeyError:
148
181
  if not create_if_missing:
149
182
  return None
150
- LOGGER.info("No `tool.ruff` section found, creating it.")
183
+ LOGGER.info("No `tool.ruff` section found, creating it.")
151
184
  tool = table(True)
152
185
  ruff = table()
153
186
  tool.append("ruff", ruff)
@@ -183,7 +216,7 @@ def toml_ruff_parse(toml_s: str, exclude: Iterable[str]) -> TOMLDocument:
183
216
  """Parse a TOML string for the tool.ruff section excluding certain ruff configs."""
184
217
  ruff_toml: TOMLDocument = tomlkit.parse(toml_s)["tool"]["ruff"] # type: ignore[index,assignment]
185
218
  for section in exclude:
186
- LOGGER.info(f"Exluding section `lint.{section}` from ruff config.")
219
+ LOGGER.info(f"Excluding section `lint.{section}` from ruff config.")
187
220
  ruff_toml["lint"].pop(section, None) # type: ignore[union-attr]
188
221
  return ruff_toml
189
222
 
@@ -234,6 +267,11 @@ def merge_ruff_toml(
234
267
 
235
268
  _recursive_update(source_tool_ruff, upstream_ruff_doc)
236
269
 
270
+ # Ensure a newline at the end of the section for better readability.
271
+ # We only add it if it's missing to avoid triple newlines between sections.
272
+ if not source_tool_ruff.as_string().endswith("\n\n"):
273
+ source_tool_ruff.add(tomlkit.nl())
274
+
237
275
  return source
238
276
 
239
277
 
@@ -241,7 +279,7 @@ async def sync(
241
279
  args: Arguments,
242
280
  ) -> None:
243
281
  """Sync the upstream pyproject.toml file to the source directory."""
244
- print("Syncing Ruff...")
282
+ print("🔄 Syncing Ruff...")
245
283
  if args.source.is_file():
246
284
  _source_toml_path = args.source
247
285
  else:
@@ -261,7 +299,7 @@ async def sync(
261
299
  upstream_ruff_toml,
262
300
  )
263
301
  source_toml_file.write(merged_toml)
264
- print(f"Updated {_source_toml_path.resolve().relative_to(pathlib.Path.cwd())}")
302
+ print(f"Updated {_source_toml_path.resolve().relative_to(pathlib.Path.cwd())}")
265
303
 
266
304
 
267
305
  PARSER: Final[ArgumentParser] = _get_cli_parser()
@@ -271,6 +309,37 @@ def main() -> None:
271
309
  args = PARSER.parse_args()
272
310
  config = get_config(args.source)
273
311
 
312
+ # Configure logging
313
+ log_level = {
314
+ 0: logging.WARNING,
315
+ 1: logging.INFO,
316
+ }.get(args.verbose, logging.DEBUG)
317
+
318
+ LOGGER.setLevel(log_level)
319
+ handler = logging.StreamHandler()
320
+ handler.setFormatter(ColoredFormatter())
321
+ LOGGER.addHandler(handler)
322
+ LOGGER.propagate = False # Avoid double logging if root is also configured
323
+
324
+ # Resolve upstream: use CLI value if explicitly provided, else file config
325
+ upstream: URL
326
+ if args.upstream:
327
+ upstream = args.upstream
328
+ elif "upstream" in config:
329
+ config_upstream = config["upstream"]
330
+ if not isinstance(config_upstream, str):
331
+ PARSER.error(
332
+ "❌ upstream in [tool.ruff-sync] must be a string, "
333
+ f"got {type(config_upstream).__name__}"
334
+ )
335
+ upstream = URL(config_upstream)
336
+ LOGGER.info(f"📂 Using upstream from [tool.ruff-sync]: {upstream}")
337
+ else:
338
+ PARSER.error(
339
+ "❌ the following arguments are required: upstream "
340
+ "(or define it in [tool.ruff-sync] in pyproject.toml) 💥"
341
+ )
342
+
274
343
  # Merge exclude: use CLI value if explicitly provided, else file config,
275
344
  # else the built-in default.
276
345
  exclude: Iterable[str]
@@ -279,19 +348,20 @@ def main() -> None:
279
348
  exclude = args.exclude
280
349
  elif "exclude" in config:
281
350
  exclude = config["exclude"]
282
- LOGGER.info(f"Using exclude from [tool.ruff-sync]: {list(exclude)}")
351
+ LOGGER.info(f"🚫 Using exclude from [tool.ruff-sync]: {list(exclude)}")
283
352
  else:
284
353
  exclude = _DEFAULT_EXCLUDE
285
354
 
286
355
  # Convert non-raw github upstream url to the raw equivalent
287
- args.upstream = github_url_to_raw_url(args.upstream)
356
+ upstream = github_url_to_raw_url(upstream)
288
357
 
289
358
  asyncio.run(
290
359
  sync(
291
360
  Arguments(
292
- upstream=args.upstream,
361
+ upstream=upstream,
293
362
  source=args.source,
294
363
  exclude=exclude,
364
+ verbose=args.verbose,
295
365
  )
296
366
  )
297
367
  )
@@ -35,7 +35,7 @@ if ! git diff --quiet pyproject.toml; then
35
35
  fi
36
36
 
37
37
  # Run the tool via poetry
38
- poetry run python ruff_sync.py "$UPSTREAM"
38
+ poetry run python ruff_sync.py "$UPSTREAM" -v
39
39
 
40
40
  echo ""
41
41
  echo "✨ Dogfooding run complete!"
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import contextlib
5
+ import logging
5
6
  import os
6
7
  import pathlib
7
8
  import shutil
@@ -65,10 +66,6 @@ def pyproject_toml_s() -> str:
65
66
  return s
66
67
 
67
68
 
68
- def test_ruff_sync():
69
- assert ruff_sync.__version__ == "0.0.1.dev0"
70
-
71
-
72
69
  @pytest.fixture
73
70
  def toml_s() -> str:
74
71
  """A sample pyproject.toml file with ruff config."""
@@ -273,7 +270,9 @@ async def test_sync_updates_ruff_config(
273
270
  upstream = URL("https://example.com/pyproject.toml")
274
271
  upstream_toml = httpx.get(upstream).text # blocking but doesn't matter
275
272
  await ruff_sync.sync(
276
- ruff_sync.Arguments(upstream=upstream, source=fake_fs_source, exclude=())
273
+ ruff_sync.Arguments(
274
+ upstream=upstream, source=fake_fs_source, exclude=(), verbose=0
275
+ )
277
276
  )
278
277
  updated_toml = fake_fs_source.read_text()
279
278
  updated_ruff_config: Table = tomlkit.parse(updated_toml)["tool"]["ruff"] # type: ignore[index,assignment]
@@ -406,6 +405,123 @@ def test_exclude_resolution_default(monkeypatch: pytest.MonkeyPatch):
406
405
  assert captured_args[0].exclude == ruff_sync._DEFAULT_EXCLUDE
407
406
 
408
407
 
408
+ def test_upstream_resolution_cli_precedence(monkeypatch: pytest.MonkeyPatch):
409
+ """CLI upstream should override config."""
410
+ captured_args: list[ruff_sync.Arguments] = []
411
+
412
+ def mock_sync(args: ruff_sync.Arguments) -> Any:
413
+ captured_args.append(args)
414
+ return asyncio.sleep(0)
415
+
416
+ monkeypatch.setattr(sys, "argv", ["ruff-sync", "http://cli.com"])
417
+ monkeypatch.setattr(
418
+ ruff_sync, "get_config", lambda _: {"upstream": "http://config.com"}
419
+ )
420
+ monkeypatch.setattr(ruff_sync, "sync", mock_sync)
421
+ monkeypatch.setattr(asyncio, "run", lambda _coro: None)
422
+
423
+ ruff_sync.main()
424
+
425
+ assert len(captured_args) == 1
426
+ assert str(captured_args[0].upstream) == "http://cli.com"
427
+
428
+
429
+ def test_upstream_resolution_missing(
430
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
431
+ ) -> None:
432
+ """Error when no upstream is provided via CLI or config."""
433
+ captured_args: list[ruff_sync.Arguments] = []
434
+
435
+ def mock_sync(args: ruff_sync.Arguments) -> Any:
436
+ captured_args.append(args)
437
+ return asyncio.sleep(0)
438
+
439
+ # No CLI upstream argument
440
+ monkeypatch.setattr(sys, "argv", ["ruff-sync"])
441
+ # No upstream in config
442
+ monkeypatch.setattr(ruff_sync, "get_config", lambda _: {})
443
+ # Ensure sync is never called if upstream is missing
444
+ monkeypatch.setattr(ruff_sync, "sync", mock_sync)
445
+ monkeypatch.setattr(asyncio, "run", lambda _coro: None)
446
+
447
+ with pytest.raises(SystemExit) as excinfo:
448
+ ruff_sync.main()
449
+
450
+ # Non-zero exit code on failure
451
+ assert excinfo.value.code != 0
452
+
453
+ captured = capsys.readouterr()
454
+ # Error message should indicate that an upstream is required
455
+ assert "upstream" in captured.err
456
+ assert "[tool.ruff-sync]" in captured.err
457
+
458
+ # When erroring early, sync must not be invoked
459
+ assert captured_args == []
460
+
461
+
462
+ def test_upstream_resolution_config_precedence(monkeypatch: pytest.MonkeyPatch):
463
+ """[tool.ruff-sync] upstream should be used if CLI one is missing."""
464
+ captured_args: list[ruff_sync.Arguments] = []
465
+
466
+ def mock_sync(args: ruff_sync.Arguments) -> Any:
467
+ captured_args.append(args)
468
+ return asyncio.sleep(0)
469
+
470
+ monkeypatch.setattr(sys, "argv", ["ruff-sync"])
471
+ monkeypatch.setattr(
472
+ ruff_sync, "get_config", lambda _: {"upstream": "http://config.com"}
473
+ )
474
+ monkeypatch.setattr(ruff_sync, "sync", mock_sync)
475
+ monkeypatch.setattr(asyncio, "run", lambda _coro: None)
476
+
477
+ ruff_sync.main()
478
+
479
+ assert len(captured_args) == 1
480
+ assert str(captured_args[0].upstream) == "http://config.com"
481
+
482
+
483
+ @pytest.mark.parametrize(
484
+ ["verbose_count", "expected_level"],
485
+ [
486
+ (0, logging.WARNING),
487
+ (1, logging.INFO),
488
+ (2, logging.DEBUG),
489
+ (3, logging.DEBUG),
490
+ ],
491
+ )
492
+ def test_verbosity_log_level(
493
+ monkeypatch: pytest.MonkeyPatch, verbose_count: int, expected_level: int
494
+ ):
495
+ """Test that the log level is correctly set based on the verbose count."""
496
+ captured_args: list[ruff_sync.Arguments] = []
497
+
498
+ def mock_sync(args: ruff_sync.Arguments) -> Any:
499
+ captured_args.append(args)
500
+ return asyncio.sleep(0)
501
+
502
+ argv = ["ruff-sync", "http://example.com"]
503
+ if verbose_count > 0:
504
+ argv.append(f"-{'v' * verbose_count}")
505
+
506
+ monkeypatch.setattr(sys, "argv", argv)
507
+ monkeypatch.setattr(ruff_sync, "get_config", lambda _: {})
508
+ monkeypatch.setattr(ruff_sync, "sync", mock_sync)
509
+ monkeypatch.setattr(asyncio, "run", lambda _coro: None)
510
+
511
+ # Reset LOGGER state before test
512
+ monkeypatch.setattr(ruff_sync.LOGGER, "level", logging.NOTSET)
513
+ monkeypatch.setattr(ruff_sync.LOGGER, "handlers", [])
514
+
515
+ ruff_sync.main()
516
+
517
+ # Verify that the computed log level matches what we expect for this verbosity
518
+ assert ruff_sync.LOGGER.level == expected_level
519
+
520
+ # Verify that the verbose flag value propagates into Arguments.verbose
521
+ assert len(captured_args) == 1
522
+ assert captured_args[0].verbose == verbose_count
523
+
524
+
409
525
  @pytest.mark.asyncio
410
526
  async def test_sync_default_exclude(fs: FakeFilesystem):
411
527
  """Integration style test for default exclude functionality."""
@@ -431,6 +547,7 @@ target-version = "py311"
431
547
  upstream=URL("https://example.com/pyproject.toml"),
432
548
  source=ff_path,
433
549
  exclude=ruff_sync._DEFAULT_EXCLUDE,
550
+ verbose=0,
434
551
  )
435
552
  )
436
553
 
@@ -93,6 +93,7 @@ async def test_ruff_sync(prep_env):
93
93
  upstream=prep_env.upstream_url,
94
94
  source=prep_env.source_path,
95
95
  exclude={},
96
+ verbose=0,
96
97
  )
97
98
  )
98
99
 
@@ -4,13 +4,15 @@ import logging
4
4
  import pathlib
5
5
  import sys
6
6
  from pprint import pformat as pf
7
- from typing import TYPE_CHECKING, Final
7
+ from typing import TYPE_CHECKING, Any, Final, cast
8
8
 
9
9
  import pytest
10
10
  import tomlkit
11
11
  from packaging.version import Version
12
12
  from ruamel.yaml import YAML
13
13
 
14
+ import ruff_sync
15
+
14
16
  if TYPE_CHECKING:
15
17
  from collections.abc import Mapping
16
18
 
@@ -62,5 +64,14 @@ def test_pre_commit_versions_are_in_sync(
62
64
  )
63
65
 
64
66
 
67
+ def test_ruff_sync_version_is_in_sync_with_pyproject():
68
+ """
69
+ Ensure the version in ruff_sync.py matches the version in pyproject.toml
70
+ """
71
+ toml_doc = tomlkit.loads(PYPROJECT_TOML.read_text())
72
+ pyproject_version = cast("Any", toml_doc)["project"]["version"]
73
+ assert ruff_sync.__version__ == pyproject_version
74
+
75
+
65
76
  if __name__ == "__main__":
66
77
  pytest.main([__file__, "-vv", "-rEf"])
@@ -101,5 +101,27 @@ lint.per-file-ignores = {"__init__.py" = ["F401", "F403"]}
101
101
  assert list(per_file_ignores["__init__.py"]) == ["F401", "F403"]
102
102
 
103
103
 
104
+ def test_merge_adds_newline_at_end():
105
+ """
106
+ Test that merging adds a newline at the end of the ruff section if missing.
107
+ """
108
+ source_toml_s = """[tool.ruff]
109
+ target-version = "py310"
110
+ """
111
+ upstream_ruff_s = """[tool.ruff]
112
+ line-length = 100
113
+ """
114
+ source_doc = tomlkit.parse(source_toml_s)
115
+ upstream_ruff = cast("Any", tomlkit.parse(upstream_ruff_s))["tool"]["ruff"]
116
+
117
+ merged_doc = ruff_sync.merge_ruff_toml(source_doc, upstream_ruff)
118
+ merged_s = merged_doc.as_string()
119
+
120
+ print(f"Merged Result:\n{merged_s!r}")
121
+ # In TOML, sections usually end with a newline.
122
+ # We want to ensure it ends with \n\n if it's the last section.
123
+ assert merged_s.endswith("\n\n") or "\n\n[" in merged_s
124
+
125
+
104
126
  if __name__ == "__main__":
105
127
  pytest.main([__file__, "-vv"])
@@ -990,7 +990,7 @@ wheels = [
990
990
 
991
991
  [[package]]
992
992
  name = "ruff-sync"
993
- version = "0.0.1.dev2"
993
+ version = "0.0.1.dev4"
994
994
  source = { editable = "." }
995
995
  dependencies = [
996
996
  { name = "httpx" },
File without changes
File without changes