ruff-sync 0.1.4.dev1__py3-none-any.whl → 0.1.5.dev2__py3-none-any.whl
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/__init__.py +12 -2
- ruff_sync/cli.py +191 -81
- ruff_sync/config_io.py +136 -0
- ruff_sync/constants.py +66 -4
- ruff_sync/core.py +21 -101
- ruff_sync/dependencies.py +38 -0
- ruff_sync/system.py +45 -0
- {ruff_sync-0.1.4.dev1.dist-info → ruff_sync-0.1.5.dev2.dist-info}/METADATA +3 -1
- ruff_sync-0.1.5.dev2.dist-info/RECORD +15 -0
- ruff_sync-0.1.4.dev1.dist-info/RECORD +0 -12
- {ruff_sync-0.1.4.dev1.dist-info → ruff_sync-0.1.5.dev2.dist-info}/WHEEL +0 -0
- {ruff_sync-0.1.4.dev1.dist-info → ruff_sync-0.1.5.dev2.dist-info}/entry_points.txt +0 -0
- {ruff_sync-0.1.4.dev1.dist-info → ruff_sync-0.1.5.dev2.dist-info}/licenses/LICENSE.md +0 -0
ruff_sync/__init__.py
CHANGED
|
@@ -11,38 +11,48 @@ from .cli import (
|
|
|
11
11
|
get_config,
|
|
12
12
|
main,
|
|
13
13
|
)
|
|
14
|
+
from .config_io import (
|
|
15
|
+
RuffConfigFileName,
|
|
16
|
+
is_ruff_toml_file,
|
|
17
|
+
load_local_ruff_config,
|
|
18
|
+
resolve_target_path,
|
|
19
|
+
)
|
|
20
|
+
from .constants import OutputFormat
|
|
14
21
|
from .core import (
|
|
15
22
|
Config,
|
|
16
23
|
FetchResult,
|
|
17
|
-
RuffConfigFileName,
|
|
18
24
|
check,
|
|
19
25
|
fetch_upstream_config,
|
|
20
26
|
get_ruff_config,
|
|
21
27
|
get_ruff_tool_table,
|
|
22
|
-
is_ruff_toml_file,
|
|
23
28
|
merge_ruff_toml,
|
|
24
29
|
pull,
|
|
25
30
|
resolve_raw_url,
|
|
26
31
|
to_git_url,
|
|
27
32
|
toml_ruff_parse,
|
|
28
33
|
)
|
|
34
|
+
from .formatters import get_formatter
|
|
29
35
|
|
|
30
36
|
__all__ = [
|
|
31
37
|
"Arguments",
|
|
32
38
|
"Config",
|
|
33
39
|
"FetchResult",
|
|
40
|
+
"OutputFormat",
|
|
34
41
|
"RuffConfigFileName",
|
|
35
42
|
"__version__",
|
|
36
43
|
"check",
|
|
37
44
|
"fetch_upstream_config",
|
|
38
45
|
"get_config",
|
|
46
|
+
"get_formatter",
|
|
39
47
|
"get_ruff_config",
|
|
40
48
|
"get_ruff_tool_table",
|
|
41
49
|
"is_ruff_toml_file",
|
|
50
|
+
"load_local_ruff_config",
|
|
42
51
|
"main",
|
|
43
52
|
"merge_ruff_toml",
|
|
44
53
|
"pull",
|
|
45
54
|
"resolve_raw_url",
|
|
55
|
+
"resolve_target_path",
|
|
46
56
|
"to_git_url",
|
|
47
57
|
"toml_ruff_parse",
|
|
48
58
|
]
|
ruff_sync/cli.py
CHANGED
|
@@ -16,25 +16,23 @@ from functools import lru_cache
|
|
|
16
16
|
from importlib import metadata
|
|
17
17
|
from typing import (
|
|
18
18
|
TYPE_CHECKING,
|
|
19
|
-
Any,
|
|
20
19
|
ClassVar,
|
|
21
20
|
Final,
|
|
21
|
+
Literal,
|
|
22
22
|
NamedTuple,
|
|
23
|
-
|
|
23
|
+
Protocol,
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
import tomlkit
|
|
27
27
|
from httpx import URL
|
|
28
|
-
from typing_extensions import deprecated
|
|
28
|
+
from typing_extensions import deprecated, override
|
|
29
29
|
|
|
30
30
|
from ruff_sync.constants import (
|
|
31
31
|
DEFAULT_BRANCH,
|
|
32
32
|
DEFAULT_EXCLUDE,
|
|
33
33
|
DEFAULT_PATH,
|
|
34
|
-
|
|
35
|
-
MissingType,
|
|
34
|
+
ConfKey,
|
|
36
35
|
OutputFormat,
|
|
37
|
-
resolve_defaults,
|
|
38
36
|
)
|
|
39
37
|
from ruff_sync.core import (
|
|
40
38
|
Config,
|
|
@@ -51,6 +49,7 @@ if TYPE_CHECKING:
|
|
|
51
49
|
__all__: Final[list[str]] = [
|
|
52
50
|
"Arguments",
|
|
53
51
|
"ColoredFormatter",
|
|
52
|
+
"OutputFormat",
|
|
54
53
|
"get_config",
|
|
55
54
|
"main",
|
|
56
55
|
]
|
|
@@ -61,6 +60,9 @@ except metadata.PackageNotFoundError:
|
|
|
61
60
|
__version__ = "0.0.0-dev"
|
|
62
61
|
|
|
63
62
|
|
|
63
|
+
CIProvider = Literal[OutputFormat.GITHUB, OutputFormat.GITLAB]
|
|
64
|
+
|
|
65
|
+
|
|
64
66
|
LOGGER = logging.getLogger(__name__)
|
|
65
67
|
|
|
66
68
|
|
|
@@ -80,7 +82,8 @@ class ColoredFormatter(logging.Formatter):
|
|
|
80
82
|
"""Initialize the formatter with a format string."""
|
|
81
83
|
super().__init__(fmt)
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
@override
|
|
86
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
84
87
|
"""Format the log record with colors if the output is a TTY."""
|
|
85
88
|
if sys.stderr.isatty():
|
|
86
89
|
color = self.COLORS.get(record.levelno, self.RESET)
|
|
@@ -94,14 +97,14 @@ class Arguments(NamedTuple):
|
|
|
94
97
|
command: str
|
|
95
98
|
upstream: tuple[URL, ...]
|
|
96
99
|
to: pathlib.Path
|
|
97
|
-
exclude: Iterable[str]
|
|
100
|
+
exclude: Iterable[str]
|
|
98
101
|
verbose: int
|
|
99
|
-
branch: str
|
|
100
|
-
path: str |
|
|
102
|
+
branch: str = DEFAULT_BRANCH
|
|
103
|
+
path: str | None = None
|
|
101
104
|
semantic: bool = False
|
|
102
105
|
diff: bool = True
|
|
103
106
|
init: bool = False
|
|
104
|
-
pre_commit: bool
|
|
107
|
+
pre_commit: bool = False
|
|
105
108
|
save: bool | None = None
|
|
106
109
|
output_format: OutputFormat = OutputFormat.TEXT
|
|
107
110
|
|
|
@@ -123,9 +126,30 @@ class ResolvedArgs(NamedTuple):
|
|
|
123
126
|
|
|
124
127
|
upstream: tuple[URL, ...]
|
|
125
128
|
to: pathlib.Path
|
|
126
|
-
exclude: Iterable[str]
|
|
127
|
-
branch: str
|
|
128
|
-
path: str |
|
|
129
|
+
exclude: Iterable[str]
|
|
130
|
+
branch: str
|
|
131
|
+
path: str | None
|
|
132
|
+
output_format: OutputFormat
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class CLIArguments(Protocol):
|
|
136
|
+
"""Protocol for parsed CLI arguments from ArgumentParser."""
|
|
137
|
+
|
|
138
|
+
command: str | None
|
|
139
|
+
upstream: list[URL]
|
|
140
|
+
to: str | None
|
|
141
|
+
source: str | None
|
|
142
|
+
exclude: list[str] | None
|
|
143
|
+
verbose: int
|
|
144
|
+
branch: str | None
|
|
145
|
+
path: str | None
|
|
146
|
+
pre_commit: bool | None
|
|
147
|
+
output_format: OutputFormat | None
|
|
148
|
+
# Subcommand specific
|
|
149
|
+
init: bool
|
|
150
|
+
save: bool | None
|
|
151
|
+
semantic: bool
|
|
152
|
+
diff: bool
|
|
129
153
|
|
|
130
154
|
|
|
131
155
|
@lru_cache(maxsize=1)
|
|
@@ -142,32 +166,39 @@ def get_config(
|
|
|
142
166
|
"""
|
|
143
167
|
local_toml = source / RuffConfigFileName.PYPROJECT_TOML
|
|
144
168
|
# TODO: use pydantic to validate the toml file
|
|
145
|
-
cfg_result:
|
|
169
|
+
cfg_result: Config = {}
|
|
146
170
|
if local_toml.exists():
|
|
147
171
|
toml = tomlkit.parse(local_toml.read_text())
|
|
148
172
|
config = toml.get("tool", {}).get("ruff-sync")
|
|
149
173
|
if config:
|
|
150
174
|
allowed_keys = set(Config.__annotations__.keys())
|
|
151
|
-
for
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
for raw_key, value in config.items():
|
|
176
|
+
# Check for legacy 'source' key to emit deprecation warning
|
|
177
|
+
if raw_key.replace("-", "_") == ConfKey.to_attr(ConfKey.SOURCE):
|
|
178
|
+
LOGGER.warning(
|
|
179
|
+
f"DeprecationWarning: [tool.ruff-sync] '{raw_key}' "
|
|
180
|
+
f"is deprecated. Use '{ConfKey.TO}' instead."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Map legacy names (source, pre_commit_sync) to canonical
|
|
184
|
+
# (to, pre-commit-version-sync)
|
|
185
|
+
canonical_key = ConfKey.get_canonical(raw_key)
|
|
186
|
+
|
|
187
|
+
# Normalize TOML key (dashes) to internal Python attribute name (underscores)
|
|
188
|
+
# e.g. "pre-commit-version-sync" -> "pre_commit_version_sync"
|
|
189
|
+
arg_attr = ConfKey.to_attr(canonical_key)
|
|
190
|
+
|
|
191
|
+
if arg_attr in allowed_keys:
|
|
192
|
+
cfg_result[arg_attr] = value # type: ignore[literal-required]
|
|
165
193
|
else:
|
|
166
|
-
LOGGER.warning(f"Unknown ruff-sync configuration: {
|
|
194
|
+
LOGGER.warning(f"Unknown ruff-sync configuration: {raw_key}")
|
|
195
|
+
|
|
167
196
|
# Ensure 'to' is populated if 'source' was used
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
197
|
+
to_attr = ConfKey.to_attr(ConfKey.TO)
|
|
198
|
+
source_attr = ConfKey.to_attr(ConfKey.SOURCE)
|
|
199
|
+
if source_attr in cfg_result and to_attr not in cfg_result:
|
|
200
|
+
cfg_result[to_attr] = cfg_result[source_attr] # type: ignore[literal-required]
|
|
201
|
+
return cfg_result
|
|
171
202
|
|
|
172
203
|
|
|
173
204
|
def _get_cli_parser() -> ArgumentParser:
|
|
@@ -255,8 +286,8 @@ def _get_cli_parser() -> ArgumentParser:
|
|
|
255
286
|
"--output-format",
|
|
256
287
|
type=OutputFormat,
|
|
257
288
|
choices=list(OutputFormat),
|
|
258
|
-
default=
|
|
259
|
-
help="Format for output. Default: text.",
|
|
289
|
+
default=None,
|
|
290
|
+
help="Format for output. Default: text (auto-detected in CI).",
|
|
260
291
|
)
|
|
261
292
|
|
|
262
293
|
# Pull subcommand (the default action)
|
|
@@ -299,16 +330,23 @@ def _get_cli_parser() -> ArgumentParser:
|
|
|
299
330
|
help="Do not show a diff.",
|
|
300
331
|
)
|
|
301
332
|
|
|
333
|
+
# Inspect subcommand
|
|
334
|
+
subparsers.add_parser(
|
|
335
|
+
"inspect",
|
|
336
|
+
parents=[common_parser],
|
|
337
|
+
help="Open a Terminal UI to explore and interrogate your local ruff configuration.",
|
|
338
|
+
)
|
|
339
|
+
|
|
302
340
|
return parser
|
|
303
341
|
|
|
304
342
|
|
|
305
343
|
PARSER: Final[ArgumentParser] = _get_cli_parser()
|
|
306
344
|
|
|
307
345
|
|
|
308
|
-
def _resolve_upstream(args:
|
|
346
|
+
def _resolve_upstream(args: CLIArguments, config: Config) -> tuple[URL, ...]:
|
|
309
347
|
"""Resolve upstream URL(s) from CLI or config."""
|
|
310
348
|
if args.upstream:
|
|
311
|
-
upstreams = tuple(
|
|
349
|
+
upstreams = tuple(args.upstream)
|
|
312
350
|
# Log CLI upstreams for consistency with config sourcing
|
|
313
351
|
summary = (
|
|
314
352
|
f"{upstreams[0]}... ({len(upstreams)} total)"
|
|
@@ -346,54 +384,80 @@ def _resolve_upstream(args: Any, config: Mapping[str, Any]) -> tuple[URL, ...]:
|
|
|
346
384
|
)
|
|
347
385
|
|
|
348
386
|
|
|
349
|
-
def _resolve_exclude(args:
|
|
387
|
+
def _resolve_exclude(args: CLIArguments, config: Config) -> Iterable[str]:
|
|
350
388
|
"""Resolve exclude patterns from CLI or config.
|
|
351
389
|
|
|
352
390
|
Returns the CLI value if provided, otherwise the value from
|
|
353
391
|
``[tool.ruff-sync].exclude`` in the config. If neither is set,
|
|
354
|
-
returns ``
|
|
355
|
-
can apply the ``DEFAULT_EXCLUDE`` set downstream.
|
|
392
|
+
returns ``DEFAULT_EXCLUDE``.
|
|
356
393
|
"""
|
|
357
394
|
if args.exclude is not None:
|
|
358
|
-
return
|
|
395
|
+
return args.exclude
|
|
359
396
|
if "exclude" in config:
|
|
360
|
-
exclude = config["exclude"]
|
|
397
|
+
exclude: Iterable[str] = config["exclude"]
|
|
361
398
|
LOGGER.info(f"🚫 Using exclude from [tool.ruff-sync]: {list(exclude)}")
|
|
362
|
-
return
|
|
363
|
-
return
|
|
399
|
+
return exclude
|
|
400
|
+
return DEFAULT_EXCLUDE
|
|
364
401
|
|
|
365
402
|
|
|
366
|
-
def _resolve_branch(args:
|
|
403
|
+
def _resolve_branch(args: CLIArguments, config: Config) -> str:
|
|
367
404
|
"""Resolve branch name from CLI, config, or MISSING.
|
|
368
405
|
|
|
369
406
|
An empty string (``--branch ""``) is treated as falsy and falls back to
|
|
370
407
|
the config or default, preserving the original behaviour.
|
|
371
408
|
"""
|
|
372
|
-
if
|
|
373
|
-
return
|
|
409
|
+
if args.branch:
|
|
410
|
+
return args.branch
|
|
374
411
|
if "branch" in config:
|
|
375
|
-
branch =
|
|
412
|
+
branch: str = config["branch"]
|
|
376
413
|
LOGGER.info(f"🌿 Using branch from [tool.ruff-sync]: {branch}")
|
|
377
414
|
return branch
|
|
378
|
-
return
|
|
415
|
+
return DEFAULT_BRANCH
|
|
379
416
|
|
|
380
417
|
|
|
381
|
-
def _resolve_path(args:
|
|
418
|
+
def _resolve_path(args: CLIArguments, config: Config) -> str:
|
|
382
419
|
"""Resolve path prefix from CLI, config, or MISSING.
|
|
383
420
|
|
|
384
421
|
An empty string (``--path ""``) is treated as falsy and falls back to
|
|
385
422
|
the config or default, preserving the original behaviour.
|
|
386
423
|
"""
|
|
387
|
-
if
|
|
388
|
-
return
|
|
424
|
+
if args.path:
|
|
425
|
+
return args.path
|
|
389
426
|
if "path" in config:
|
|
390
|
-
path =
|
|
427
|
+
path: str = config["path"]
|
|
391
428
|
LOGGER.info(f"📄 Using path from [tool.ruff-sync]: {path}")
|
|
392
429
|
return path
|
|
393
|
-
return
|
|
430
|
+
return DEFAULT_PATH
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _resolve_output_format(args: CLIArguments, config: Config) -> OutputFormat:
|
|
434
|
+
"""Resolve output format from CLI, config, or environment auto-detection."""
|
|
435
|
+
if args.output_format:
|
|
436
|
+
return args.output_format
|
|
437
|
+
|
|
438
|
+
if "output_format" in config:
|
|
439
|
+
fmt_str = config["output_format"]
|
|
440
|
+
try:
|
|
441
|
+
fmt = OutputFormat(fmt_str)
|
|
442
|
+
except ValueError:
|
|
443
|
+
valid_formats = ", ".join(f.value for f in OutputFormat)
|
|
444
|
+
LOGGER.warning(
|
|
445
|
+
f"Unknown output format in config: {fmt_str}. Valid values: {valid_formats}"
|
|
446
|
+
)
|
|
447
|
+
else:
|
|
448
|
+
LOGGER.info(f"📊 Using output format from [tool.ruff-sync]: {fmt}")
|
|
449
|
+
return fmt
|
|
450
|
+
|
|
451
|
+
# Auto-detection
|
|
452
|
+
provider = _detect_ci_provider()
|
|
453
|
+
if provider:
|
|
454
|
+
LOGGER.info(f"🤖 Auto-detected CI environment: {provider}")
|
|
455
|
+
return provider
|
|
456
|
+
|
|
457
|
+
return OutputFormat.TEXT
|
|
394
458
|
|
|
395
459
|
|
|
396
|
-
def _resolve_to(args:
|
|
460
|
+
def _resolve_to(args: CLIArguments, config: Config, initial_to: pathlib.Path) -> pathlib.Path:
|
|
397
461
|
"""Resolve target path from CLI, config, or default."""
|
|
398
462
|
if args.source:
|
|
399
463
|
LOGGER.warning("DeprecationWarning: --source is deprecated. Use --to instead.")
|
|
@@ -410,14 +474,62 @@ def _resolve_to(args: Any, config: Mapping[str, Any], initial_to: pathlib.Path)
|
|
|
410
474
|
return initial_to
|
|
411
475
|
|
|
412
476
|
|
|
413
|
-
def _resolve_args(args:
|
|
477
|
+
def _resolve_args(args: CLIArguments, config: Config, initial_to: pathlib.Path) -> ResolvedArgs:
|
|
414
478
|
"""Resolve upstream, to, exclude, branch, and path from CLI and config."""
|
|
415
479
|
upstream = _resolve_upstream(args, config)
|
|
416
480
|
to = _resolve_to(args, config, initial_to)
|
|
417
481
|
exclude = _resolve_exclude(args, config)
|
|
418
482
|
branch = _resolve_branch(args, config)
|
|
419
483
|
path = _resolve_path(args, config)
|
|
420
|
-
|
|
484
|
+
output_format = _resolve_output_format(args, config)
|
|
485
|
+
|
|
486
|
+
# Normalize path "" -> None to match core expectation
|
|
487
|
+
final_path: str | None = path or None
|
|
488
|
+
|
|
489
|
+
return ResolvedArgs(
|
|
490
|
+
upstream,
|
|
491
|
+
to,
|
|
492
|
+
exclude,
|
|
493
|
+
branch,
|
|
494
|
+
final_path,
|
|
495
|
+
output_format,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _resolve_pre_commit(args: CLIArguments, config: Config) -> bool:
|
|
500
|
+
"""Resolve pre-commit sync setting from CLI or config."""
|
|
501
|
+
if args.pre_commit is not None:
|
|
502
|
+
return args.pre_commit
|
|
503
|
+
|
|
504
|
+
pre_commit_attr = ConfKey.to_attr(ConfKey.PRE_COMMIT_VERSION_SYNC)
|
|
505
|
+
if pre_commit_attr in config:
|
|
506
|
+
val = config.get(pre_commit_attr)
|
|
507
|
+
return bool(val)
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _detect_ci_provider() -> CIProvider | None:
|
|
512
|
+
"""Return the current CI provider, or None if not in a CI environment."""
|
|
513
|
+
if os.environ.get("GITHUB_ACTIONS") == "true":
|
|
514
|
+
return OutputFormat.GITHUB
|
|
515
|
+
if os.environ.get("GITLAB_CI") == "true":
|
|
516
|
+
return OutputFormat.GITLAB
|
|
517
|
+
return None
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _validate_ci_output_format(args: Arguments) -> None:
|
|
521
|
+
"""Log a warning if the specified output format does not match the CI environment."""
|
|
522
|
+
provider = _detect_ci_provider()
|
|
523
|
+
if provider is OutputFormat.GITHUB and args.output_format == OutputFormat.GITLAB:
|
|
524
|
+
LOGGER.warning(
|
|
525
|
+
"⚠️ GitLab output format detected in GitHub Actions environment. "
|
|
526
|
+
"Did you mean '--output-format github'?"
|
|
527
|
+
)
|
|
528
|
+
elif provider is OutputFormat.GITLAB and args.output_format == OutputFormat.GITHUB:
|
|
529
|
+
LOGGER.warning(
|
|
530
|
+
"⚠️ GitHub output format detected in GitLab CI environment. "
|
|
531
|
+
"Did you mean '--output-format gitlab'?"
|
|
532
|
+
)
|
|
421
533
|
|
|
422
534
|
|
|
423
535
|
def main() -> int:
|
|
@@ -426,6 +538,7 @@ def main() -> int:
|
|
|
426
538
|
if len(sys.argv) > 1 and sys.argv[1] not in (
|
|
427
539
|
"pull",
|
|
428
540
|
"check",
|
|
541
|
+
"inspect",
|
|
429
542
|
"-h",
|
|
430
543
|
"--help",
|
|
431
544
|
"--version",
|
|
@@ -460,24 +573,14 @@ def main() -> int:
|
|
|
460
573
|
initial_to = pathlib.Path(arg_to) if arg_to else pathlib.Path()
|
|
461
574
|
config: Config = get_config(initial_to)
|
|
462
575
|
|
|
463
|
-
upstream, to_val, exclude, branch, path = _resolve_args(args, config, initial_to)
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
elif "pre-commit-version-sync" in config:
|
|
469
|
-
pre_commit_val = cast("Any", config).get("pre-commit-version-sync")
|
|
470
|
-
elif "pre_commit_version_sync" in config:
|
|
471
|
-
pre_commit_val = config.get("pre_commit_version_sync", MISSING)
|
|
472
|
-
else:
|
|
473
|
-
pre_commit_val = MISSING
|
|
474
|
-
|
|
475
|
-
# Build Arguments first so _resolve_defaults can centralize MISSING → default
|
|
476
|
-
# resolution for branch and path (keeps cli.main and core._merge_multiple_upstreams
|
|
477
|
-
# in sync with a single source of truth).
|
|
576
|
+
upstream, to_val, exclude, branch, path, output_format = _resolve_args(args, config, initial_to)
|
|
577
|
+
pre_commit_val = _resolve_pre_commit(args, config)
|
|
578
|
+
|
|
579
|
+
resolved_upstream = tuple(resolve_raw_url(u, branch=branch, path=path) for u in upstream)
|
|
580
|
+
|
|
478
581
|
exec_args = Arguments(
|
|
479
582
|
command=args.command,
|
|
480
|
-
upstream=
|
|
583
|
+
upstream=resolved_upstream,
|
|
481
584
|
to=to_val,
|
|
482
585
|
exclude=exclude,
|
|
483
586
|
verbose=args.verbose,
|
|
@@ -488,18 +591,25 @@ def main() -> int:
|
|
|
488
591
|
init=getattr(args, "init", False),
|
|
489
592
|
pre_commit=pre_commit_val,
|
|
490
593
|
save=getattr(args, "save", None),
|
|
491
|
-
output_format=
|
|
594
|
+
output_format=output_format,
|
|
492
595
|
)
|
|
493
596
|
|
|
494
|
-
#
|
|
495
|
-
|
|
496
|
-
res_branch, res_path, _exclude = resolve_defaults(branch, path, exclude)
|
|
497
|
-
resolved_upstream = tuple(
|
|
498
|
-
resolve_raw_url(u, branch=res_branch, path=res_path) for u in upstream
|
|
499
|
-
)
|
|
500
|
-
exec_args = exec_args._replace(upstream=resolved_upstream)
|
|
597
|
+
# Warn if the specified output format doesn't match the current CI environment
|
|
598
|
+
_validate_ci_output_format(exec_args)
|
|
501
599
|
|
|
502
600
|
try:
|
|
601
|
+
if exec_args.command == "inspect":
|
|
602
|
+
from ruff_sync.dependencies import require_dependency # noqa: PLC0415
|
|
603
|
+
|
|
604
|
+
try:
|
|
605
|
+
require_dependency("textual", extra_name="tui")
|
|
606
|
+
except ImportError as e:
|
|
607
|
+
LOGGER.error(f"❌ {e}") # noqa: TRY400
|
|
608
|
+
return 1
|
|
609
|
+
|
|
610
|
+
LOGGER.error("❌ The Terminal UI (inspect) is not yet implemented.")
|
|
611
|
+
return 1
|
|
612
|
+
|
|
503
613
|
if exec_args.command == "check":
|
|
504
614
|
return asyncio.run(check(exec_args))
|
|
505
615
|
return asyncio.run(pull(exec_args))
|
ruff_sync/config_io.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Configuration file discovery and loading logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import enum
|
|
6
|
+
import pathlib
|
|
7
|
+
from collections.abc import Iterable, Mapping
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
import tomlkit
|
|
12
|
+
from tomlkit.items import Table
|
|
13
|
+
from typing_extensions import override
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from httpx import URL
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@enum.unique
|
|
20
|
+
class RuffConfigFileName(str, enum.Enum):
|
|
21
|
+
"""Enumeration of Ruff configuration filenames."""
|
|
22
|
+
|
|
23
|
+
PYPROJECT_TOML = "pyproject.toml"
|
|
24
|
+
RUFF_TOML = "ruff.toml"
|
|
25
|
+
DOT_RUFF_TOML = ".ruff.toml"
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def tried_order(cls) -> list[RuffConfigFileName]:
|
|
29
|
+
"""Return the order in which configuration files should be tried."""
|
|
30
|
+
return [cls.RUFF_TOML, cls.DOT_RUFF_TOML, cls.PYPROJECT_TOML]
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
"""Return the filename as a string."""
|
|
35
|
+
return self.value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_ruff_toml_file(path_or_url: str | URL) -> bool:
|
|
39
|
+
"""Return True if the path or URL indicates a ruff.toml file.
|
|
40
|
+
|
|
41
|
+
This handles cases with query strings or fragments by examining only the path component.
|
|
42
|
+
"""
|
|
43
|
+
parsed = urlparse(str(path_or_url))
|
|
44
|
+
|
|
45
|
+
# If it's a URL with a scheme/netloc, use the parsed path component.
|
|
46
|
+
# Otherwise, fall back to stripping any query/fragment from the raw string.
|
|
47
|
+
if parsed.scheme or parsed.netloc:
|
|
48
|
+
path = parsed.path
|
|
49
|
+
else:
|
|
50
|
+
path = str(path_or_url).split("?", 1)[0].split("#", 1)[0]
|
|
51
|
+
|
|
52
|
+
return pathlib.Path(path).name in (
|
|
53
|
+
RuffConfigFileName.RUFF_TOML,
|
|
54
|
+
RuffConfigFileName.DOT_RUFF_TOML,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_target_path(
|
|
59
|
+
to: pathlib.Path, upstreams: Iterable[str | URL] | None = None
|
|
60
|
+
) -> pathlib.Path:
|
|
61
|
+
"""Resolve the target path for configuration files.
|
|
62
|
+
|
|
63
|
+
If 'to' is a file, it's used directly.
|
|
64
|
+
Otherwise, it looks for existing ruff/pyproject.toml in the 'to' directory.
|
|
65
|
+
If none found, it defaults to pyproject.toml unless the first upstream is a ruff.toml.
|
|
66
|
+
"""
|
|
67
|
+
if to.is_file():
|
|
68
|
+
return to
|
|
69
|
+
|
|
70
|
+
# If it's a directory, look for common config files
|
|
71
|
+
for filename in RuffConfigFileName.tried_order():
|
|
72
|
+
candidate = to / filename
|
|
73
|
+
if candidate.exists():
|
|
74
|
+
return candidate
|
|
75
|
+
|
|
76
|
+
# Use the first upstream URL as a hint for the default file name
|
|
77
|
+
first_upstream = next(iter(upstreams), None) if upstreams else None
|
|
78
|
+
|
|
79
|
+
# If upstream is specified and is a ruff.toml, default to ruff.toml
|
|
80
|
+
if first_upstream and is_ruff_toml_file(first_upstream):
|
|
81
|
+
return to / RuffConfigFileName.RUFF_TOML
|
|
82
|
+
|
|
83
|
+
return to / RuffConfigFileName.PYPROJECT_TOML
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def load_local_ruff_config(target: pathlib.Path) -> dict[str, Any]:
|
|
87
|
+
"""Load the local Ruff configuration as a plain dictionary.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
target: The directory or file path to load configuration from.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A plain dictionary containing the [tool.ruff] configuration.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
FileNotFoundError: If no configuration file is found at the target path.
|
|
97
|
+
TypeError: If the configuration structure is invalid.
|
|
98
|
+
"""
|
|
99
|
+
config_path = resolve_target_path(target)
|
|
100
|
+
if not config_path.exists():
|
|
101
|
+
msg = f"No Ruff configuration file found at: {config_path}"
|
|
102
|
+
raise FileNotFoundError(msg)
|
|
103
|
+
|
|
104
|
+
content = config_path.read_text(encoding="utf-8")
|
|
105
|
+
doc = tomlkit.parse(content)
|
|
106
|
+
|
|
107
|
+
is_ruff_toml = config_path.name in (
|
|
108
|
+
RuffConfigFileName.RUFF_TOML,
|
|
109
|
+
RuffConfigFileName.DOT_RUFF_TOML,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if is_ruff_toml:
|
|
113
|
+
# For ruff.toml, the entire document is the configuration
|
|
114
|
+
unwrapped = doc.unwrap() if hasattr(doc, "unwrap") else doc
|
|
115
|
+
return unwrapped if isinstance(unwrapped, dict) else {}
|
|
116
|
+
|
|
117
|
+
# For pyproject.toml, extract [tool.ruff]
|
|
118
|
+
try:
|
|
119
|
+
# We use isinstance checks instead of cast to satisfy mypy
|
|
120
|
+
tool = doc.get("tool")
|
|
121
|
+
if not isinstance(tool, Mapping):
|
|
122
|
+
return {}
|
|
123
|
+
ruff = tool.get("ruff")
|
|
124
|
+
if ruff is None:
|
|
125
|
+
return {}
|
|
126
|
+
if not isinstance(ruff, (Mapping, Table)):
|
|
127
|
+
msg = f"Expected table for [tool.ruff], got {type(ruff)}"
|
|
128
|
+
raise TypeError(msg)
|
|
129
|
+
|
|
130
|
+
data = ruff.unwrap() if hasattr(ruff, "unwrap") else ruff
|
|
131
|
+
if not isinstance(data, dict):
|
|
132
|
+
return {}
|
|
133
|
+
except KeyError:
|
|
134
|
+
return {}
|
|
135
|
+
else:
|
|
136
|
+
return data
|
ruff_sync/constants.py
CHANGED
|
@@ -15,6 +15,7 @@ __all__: Final[list[str]] = [
|
|
|
15
15
|
"DEFAULT_EXCLUDE",
|
|
16
16
|
"DEFAULT_PATH",
|
|
17
17
|
"MISSING",
|
|
18
|
+
"ConfKey",
|
|
18
19
|
"MissingType",
|
|
19
20
|
"OutputFormat",
|
|
20
21
|
"resolve_defaults",
|
|
@@ -61,6 +62,69 @@ class OutputFormat(str, enum.Enum):
|
|
|
61
62
|
return self.value
|
|
62
63
|
|
|
63
64
|
|
|
65
|
+
@enum.unique
|
|
66
|
+
class ConfKey(str, enum.Enum):
|
|
67
|
+
"""Centralized configuration keys for [tool.ruff-sync].
|
|
68
|
+
|
|
69
|
+
These are the canonical names used in the pyproject.toml configuration file.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
UPSTREAM = "upstream"
|
|
73
|
+
TO = "to"
|
|
74
|
+
EXCLUDE = "exclude"
|
|
75
|
+
BRANCH = "branch"
|
|
76
|
+
PATH = "path"
|
|
77
|
+
PRE_COMMIT_VERSION_SYNC = "pre-commit-version-sync"
|
|
78
|
+
OUTPUT_FORMAT = "output-format"
|
|
79
|
+
SEMANTIC = "semantic"
|
|
80
|
+
DIFF = "diff"
|
|
81
|
+
INIT = "init"
|
|
82
|
+
SAVE = "save"
|
|
83
|
+
VERBOSE = "verbose"
|
|
84
|
+
|
|
85
|
+
# Legacy / Alias Keys
|
|
86
|
+
SOURCE = "source" # Legacy for 'to'
|
|
87
|
+
PRE_COMMIT = "pre-commit" # Legacy for 'pre-commit-version-sync'
|
|
88
|
+
PRE_COMMIT_SYNC_LEGACY = "pre-commit-sync" # Legacy for 'pre-commit-version-sync'
|
|
89
|
+
|
|
90
|
+
@override
|
|
91
|
+
def __str__(self) -> str:
|
|
92
|
+
"""Return the string value for TOML keys."""
|
|
93
|
+
return self.value
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def to_attr(cls, key: str | ConfKey) -> str:
|
|
97
|
+
"""Normalize a configuration key to its Python attribute name (underscore)."""
|
|
98
|
+
return str(key).replace("-", "_")
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def get_canonical(cls, key: str) -> str:
|
|
102
|
+
"""Map legacy or aliased configuration keys to their canonical names.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
key: The raw key from the configuration file.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The canonical ConfKey name (still as a string for use in logic).
|
|
109
|
+
"""
|
|
110
|
+
# Normalize the input key to underscores for robust alias matching
|
|
111
|
+
norm_key = cls.to_attr(key)
|
|
112
|
+
|
|
113
|
+
# Handle aliases for 'to'
|
|
114
|
+
if norm_key == cls.to_attr(cls.SOURCE):
|
|
115
|
+
return cls.TO.value
|
|
116
|
+
|
|
117
|
+
# Handle aliases for 'pre-commit-version-sync'
|
|
118
|
+
if norm_key in (
|
|
119
|
+
cls.to_attr(cls.PRE_COMMIT_SYNC_LEGACY),
|
|
120
|
+
cls.to_attr(cls.PRE_COMMIT),
|
|
121
|
+
):
|
|
122
|
+
return cls.PRE_COMMIT_VERSION_SYNC.value
|
|
123
|
+
|
|
124
|
+
# Return the original key (even if unknown, let 'allowed_keys' handle it)
|
|
125
|
+
return key
|
|
126
|
+
|
|
127
|
+
|
|
64
128
|
def resolve_defaults(
|
|
65
129
|
branch: str | MissingType,
|
|
66
130
|
path: str | MissingType,
|
|
@@ -69,8 +133,7 @@ def resolve_defaults(
|
|
|
69
133
|
"""Resolve MISSING sentinel values to their effective defaults.
|
|
70
134
|
|
|
71
135
|
This is the single source of truth for MISSING → default resolution across
|
|
72
|
-
|
|
73
|
-
layers in sync without introducing a circular dependency between them.
|
|
136
|
+
the CLI and internal logic, keeping the layers in sync.
|
|
74
137
|
|
|
75
138
|
Args:
|
|
76
139
|
branch: The resolved branch value or ``MISSING``.
|
|
@@ -80,8 +143,7 @@ def resolve_defaults(
|
|
|
80
143
|
Returns:
|
|
81
144
|
A ``(branch, path, exclude)`` tuple with defaults applied.
|
|
82
145
|
``path`` is normalised to ``None`` (not ``""``) so callers can forward
|
|
83
|
-
it directly to :func:`~ruff_sync.core.resolve_raw_url
|
|
84
|
-
:func:`~ruff_sync.core.fetch_upstreams_concurrently`.
|
|
146
|
+
it directly to :func:`~ruff_sync.core.resolve_raw_url`.
|
|
85
147
|
"""
|
|
86
148
|
eff_branch = branch if branch is not MISSING else DEFAULT_BRANCH
|
|
87
149
|
raw_path = path if path is not MISSING else DEFAULT_PATH
|
ruff_sync/core.py
CHANGED
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import difflib
|
|
7
|
-
import enum
|
|
8
7
|
import json
|
|
9
8
|
import logging
|
|
10
9
|
import pathlib
|
|
@@ -23,7 +22,6 @@ from typing import (
|
|
|
23
22
|
TypedDict,
|
|
24
23
|
overload,
|
|
25
24
|
)
|
|
26
|
-
from urllib.parse import urlparse
|
|
27
25
|
|
|
28
26
|
import httpx
|
|
29
27
|
import tomlkit
|
|
@@ -31,12 +29,13 @@ from httpx import URL
|
|
|
31
29
|
from tomlkit import TOMLDocument, table
|
|
32
30
|
from tomlkit.items import Table
|
|
33
31
|
from tomlkit.toml_file import TOMLFile
|
|
34
|
-
from typing_extensions import override
|
|
35
32
|
|
|
33
|
+
from ruff_sync.config_io import RuffConfigFileName, is_ruff_toml_file, resolve_target_path
|
|
36
34
|
from ruff_sync.constants import (
|
|
37
35
|
DEFAULT_BRANCH,
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
DEFAULT_EXCLUDE,
|
|
37
|
+
DEFAULT_PATH,
|
|
38
|
+
ConfKey,
|
|
40
39
|
)
|
|
41
40
|
from ruff_sync.formatters import ResultFormatter, get_formatter
|
|
42
41
|
from ruff_sync.pre_commit import sync_pre_commit
|
|
@@ -76,25 +75,6 @@ _HTTP_OK: Final = 200
|
|
|
76
75
|
_HTTP_NOT_FOUND: Final[int] = 404
|
|
77
76
|
|
|
78
77
|
|
|
79
|
-
@enum.unique
|
|
80
|
-
class RuffConfigFileName(str, enum.Enum):
|
|
81
|
-
"""Enumeration of Ruff configuration filenames."""
|
|
82
|
-
|
|
83
|
-
PYPROJECT_TOML = "pyproject.toml"
|
|
84
|
-
RUFF_TOML = "ruff.toml"
|
|
85
|
-
DOT_RUFF_TOML = ".ruff.toml"
|
|
86
|
-
|
|
87
|
-
@classmethod
|
|
88
|
-
def tried_order(cls) -> list[RuffConfigFileName]:
|
|
89
|
-
"""Return the order in which configuration files should be tried."""
|
|
90
|
-
return [cls.RUFF_TOML, cls.DOT_RUFF_TOML, cls.PYPROJECT_TOML]
|
|
91
|
-
|
|
92
|
-
@override
|
|
93
|
-
def __str__(self) -> str:
|
|
94
|
-
"""Return the filename as a string."""
|
|
95
|
-
return self.value
|
|
96
|
-
|
|
97
|
-
|
|
98
78
|
class FetchResult(NamedTuple):
|
|
99
79
|
"""Result of fetching an upstream configuration."""
|
|
100
80
|
|
|
@@ -141,34 +121,7 @@ class Config(TypedDict, total=False):
|
|
|
141
121
|
diff: bool
|
|
142
122
|
init: bool
|
|
143
123
|
pre_commit_version_sync: bool
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def resolve_target_path(
|
|
147
|
-
to: pathlib.Path, upstreams: Iterable[str | URL] | None = None
|
|
148
|
-
) -> pathlib.Path:
|
|
149
|
-
"""Resolve the target path for configuration files.
|
|
150
|
-
|
|
151
|
-
If 'to' is a file, it's used directly.
|
|
152
|
-
Otherwise, it looks for existing ruff/pyproject.toml in the 'to' directory.
|
|
153
|
-
If none found, it defaults to pyproject.toml unless the first upstream is a ruff.toml.
|
|
154
|
-
"""
|
|
155
|
-
if to.is_file():
|
|
156
|
-
return to
|
|
157
|
-
|
|
158
|
-
# If it's a directory, look for common config files
|
|
159
|
-
for filename in RuffConfigFileName.tried_order():
|
|
160
|
-
candidate = to / filename
|
|
161
|
-
if candidate.exists():
|
|
162
|
-
return candidate
|
|
163
|
-
|
|
164
|
-
# Use the first upstream URL as a hint for the default file name
|
|
165
|
-
first_upstream = next(iter(upstreams), None) if upstreams else None
|
|
166
|
-
|
|
167
|
-
# If upstream is specified and is a ruff.toml, default to ruff.toml
|
|
168
|
-
if first_upstream and is_ruff_toml_file(first_upstream):
|
|
169
|
-
return to / RuffConfigFileName.RUFF_TOML
|
|
170
|
-
|
|
171
|
-
return to / RuffConfigFileName.PYPROJECT_TOML
|
|
124
|
+
output_format: str
|
|
172
125
|
|
|
173
126
|
|
|
174
127
|
def _resolve_upstream_target_path(path: str | None) -> str:
|
|
@@ -530,29 +483,6 @@ async def fetch_upstream_config(
|
|
|
530
483
|
raise httpx.HTTPStatusError(msg, request=err.request, response=err.response) from None
|
|
531
484
|
|
|
532
485
|
|
|
533
|
-
def is_ruff_toml_file(path_or_url: str | URL) -> bool:
|
|
534
|
-
"""Return True if the path or URL indicates a ruff.toml file.
|
|
535
|
-
|
|
536
|
-
This handles:
|
|
537
|
-
- Plain paths (e.g. "ruff.toml", ".ruff.toml", "configs/ruff.toml")
|
|
538
|
-
- URLs with query strings or fragments (e.g. "ruff.toml?ref=main", "ruff.toml#L10")
|
|
539
|
-
by examining only the path component (or the part before any query/fragment).
|
|
540
|
-
"""
|
|
541
|
-
parsed = urlparse(str(path_or_url))
|
|
542
|
-
|
|
543
|
-
# If it's a URL with a scheme/netloc, use the parsed path component.
|
|
544
|
-
# Otherwise, fall back to stripping any query/fragment from the raw string.
|
|
545
|
-
if parsed.scheme or parsed.netloc:
|
|
546
|
-
path = parsed.path
|
|
547
|
-
else:
|
|
548
|
-
path = str(path_or_url).split("?", 1)[0].split("#", 1)[0]
|
|
549
|
-
|
|
550
|
-
return pathlib.Path(path).name in (
|
|
551
|
-
RuffConfigFileName.RUFF_TOML,
|
|
552
|
-
RuffConfigFileName.DOT_RUFF_TOML,
|
|
553
|
-
)
|
|
554
|
-
|
|
555
|
-
|
|
556
486
|
@overload
|
|
557
487
|
def get_ruff_config(
|
|
558
488
|
toml: str | TOMLDocument,
|
|
@@ -798,18 +728,6 @@ async def fetch_upstreams_concurrently(
|
|
|
798
728
|
return fetch_results
|
|
799
729
|
|
|
800
730
|
|
|
801
|
-
def _resolve_defaults(
|
|
802
|
-
args: Arguments,
|
|
803
|
-
) -> tuple[str, str | None, Iterable[str]]:
|
|
804
|
-
"""Resolve MISSING sentinel values in *args* to their effective defaults.
|
|
805
|
-
|
|
806
|
-
Delegates to :func:`~ruff_sync.constants.resolve_defaults` so that the
|
|
807
|
-
MISSING → default logic is centralised in one place and shared with
|
|
808
|
-
``cli.main`` without coupling the two layers together.
|
|
809
|
-
"""
|
|
810
|
-
return resolve_defaults(args.branch, args.path, args.exclude)
|
|
811
|
-
|
|
812
|
-
|
|
813
731
|
async def _merge_multiple_upstreams(
|
|
814
732
|
target_doc: TOMLDocument,
|
|
815
733
|
is_target_ruff_toml: bool,
|
|
@@ -821,7 +739,7 @@ async def _merge_multiple_upstreams(
|
|
|
821
739
|
Downloads are performed concurrently via fetch_upstreams_concurrently,
|
|
822
740
|
while merging remains sequential to preserve layering order.
|
|
823
741
|
"""
|
|
824
|
-
branch, path, exclude =
|
|
742
|
+
branch, path, exclude = args.branch, args.path, args.exclude
|
|
825
743
|
|
|
826
744
|
fetch_results = await fetch_upstreams_concurrently(
|
|
827
745
|
args.upstream, client, branch=branch, path=path
|
|
@@ -1017,7 +935,8 @@ async def check(
|
|
|
1017
935
|
... )
|
|
1018
936
|
>>> # asyncio.run(check(args))
|
|
1019
937
|
"""
|
|
1020
|
-
|
|
938
|
+
output_format = args.output_format
|
|
939
|
+
fmt: ResultFormatter = get_formatter(output_format)
|
|
1021
940
|
try:
|
|
1022
941
|
fmt.note("🔍 Checking Ruff sync status...")
|
|
1023
942
|
|
|
@@ -1144,32 +1063,32 @@ def serialize_ruff_sync_config(doc: TOMLDocument, args: Arguments) -> None:
|
|
|
1144
1063
|
|
|
1145
1064
|
# TODO: Consider only saving upstream if it differs from existing config
|
|
1146
1065
|
if len(args.upstream) == 1:
|
|
1147
|
-
ruff_sync_table[
|
|
1066
|
+
ruff_sync_table[ConfKey.UPSTREAM] = str(args.upstream[0])
|
|
1148
1067
|
else:
|
|
1149
1068
|
urls_array = tomlkit.array()
|
|
1150
1069
|
urls_array.multiline(True)
|
|
1151
1070
|
for url in args.upstream:
|
|
1152
1071
|
urls_array.append(str(url))
|
|
1153
|
-
ruff_sync_table[
|
|
1072
|
+
ruff_sync_table[ConfKey.UPSTREAM] = urls_array
|
|
1154
1073
|
|
|
1155
1074
|
# Normalize excludes and de-duplicate while preserving order.
|
|
1156
1075
|
# Only compute and persist excludes when explicitly provided so that
|
|
1157
1076
|
# DEFAULT_EXCLUDE remains an implicit default and is not serialized.
|
|
1158
|
-
if args.exclude
|
|
1077
|
+
if args.exclude != DEFAULT_EXCLUDE:
|
|
1159
1078
|
normalized_excludes = list(dict.fromkeys(args.exclude))
|
|
1160
1079
|
exclude_array = tomlkit.array()
|
|
1161
1080
|
for ex in normalized_excludes:
|
|
1162
1081
|
exclude_array.append(ex)
|
|
1163
|
-
ruff_sync_table[
|
|
1082
|
+
ruff_sync_table[ConfKey.EXCLUDE] = exclude_array
|
|
1164
1083
|
|
|
1165
|
-
if args.branch
|
|
1166
|
-
ruff_sync_table[
|
|
1084
|
+
if args.branch != DEFAULT_BRANCH:
|
|
1085
|
+
ruff_sync_table[ConfKey.BRANCH] = args.branch
|
|
1167
1086
|
|
|
1168
|
-
if args.path is not
|
|
1169
|
-
ruff_sync_table[
|
|
1087
|
+
if args.path != DEFAULT_PATH and args.path is not None:
|
|
1088
|
+
ruff_sync_table[ConfKey.PATH] = args.path
|
|
1170
1089
|
|
|
1171
|
-
if args.pre_commit
|
|
1172
|
-
ruff_sync_table[
|
|
1090
|
+
if args.pre_commit:
|
|
1091
|
+
ruff_sync_table[ConfKey.PRE_COMMIT_VERSION_SYNC] = args.pre_commit
|
|
1173
1092
|
|
|
1174
1093
|
|
|
1175
1094
|
async def pull(
|
|
@@ -1194,7 +1113,8 @@ async def pull(
|
|
|
1194
1113
|
... )
|
|
1195
1114
|
>>> # asyncio.run(pull(args))
|
|
1196
1115
|
"""
|
|
1197
|
-
|
|
1116
|
+
output_format = args.output_format
|
|
1117
|
+
fmt = get_formatter(output_format)
|
|
1198
1118
|
try:
|
|
1199
1119
|
fmt.note("🔄 Syncing Ruff...")
|
|
1200
1120
|
_source_toml_path = resolve_target_path(args.to, args.upstream).resolve(strict=False)
|
|
@@ -1247,7 +1167,7 @@ async def pull(
|
|
|
1247
1167
|
rel_path = _source_toml_path.resolve()
|
|
1248
1168
|
fmt.success(f"✅ Updated {rel_path}")
|
|
1249
1169
|
|
|
1250
|
-
if args.pre_commit
|
|
1170
|
+
if args.pre_commit:
|
|
1251
1171
|
sync_pre_commit(pathlib.Path.cwd(), dry_run=False)
|
|
1252
1172
|
|
|
1253
1173
|
return 0
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Utilities for handling optional dependencies and lazy loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
__all__: Final[list[str]] = ["is_installed", "require_dependency"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_installed(package_name: str) -> bool:
|
|
12
|
+
"""Check if a package is installed without importing it.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
package_name: The name of the package to check.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
True if the package is available, False otherwise.
|
|
19
|
+
"""
|
|
20
|
+
return importlib.util.find_spec(package_name) is not None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def require_dependency(package_name: str, extra_name: str) -> None:
|
|
24
|
+
"""Ensure a dependency is installed, or raise a helpful ImportError.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
package_name: The name of the required package.
|
|
28
|
+
extra_name: The name of the ruff-sync extra that provides this package.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ImportError: If the package is not installed.
|
|
32
|
+
"""
|
|
33
|
+
if not is_installed(package_name):
|
|
34
|
+
msg = (
|
|
35
|
+
f"The '{package_name}' package is required for this feature. "
|
|
36
|
+
f"Install it with: pip install 'ruff-sync[{extra_name}]'"
|
|
37
|
+
)
|
|
38
|
+
raise ImportError(msg)
|
ruff_sync/system.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""System and subprocess utilities for ruff-sync."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Final
|
|
8
|
+
|
|
9
|
+
LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def get_ruff_rule_markdown(rule_code: str) -> str | None:
|
|
13
|
+
"""Execute `ruff rule <CODE>` and return the Markdown documentation.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
rule_code: The Ruff rule code (e.g., 'RUF012').
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The Markdown documentation for the rule, or None if the execution fails
|
|
20
|
+
or the rule is not found.
|
|
21
|
+
"""
|
|
22
|
+
cmd: Final[list[str]] = ["ruff", "rule", rule_code]
|
|
23
|
+
LOGGER.debug(f"Executing system command: {' '.join(cmd)}")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
process = await asyncio.create_subprocess_exec(
|
|
27
|
+
*cmd,
|
|
28
|
+
stdout=asyncio.subprocess.PIPE,
|
|
29
|
+
stderr=asyncio.subprocess.PIPE,
|
|
30
|
+
)
|
|
31
|
+
stdout, stderr = await process.communicate()
|
|
32
|
+
|
|
33
|
+
if process.returncode != 0:
|
|
34
|
+
msg = stderr.decode().strip()
|
|
35
|
+
LOGGER.warning(f"Ruff command failed with exit code {process.returncode}: {msg}")
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
return stdout.decode().strip()
|
|
39
|
+
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
LOGGER.exception("Ruff executable not found in PATH.")
|
|
42
|
+
return None
|
|
43
|
+
except Exception:
|
|
44
|
+
LOGGER.exception(f"Unexpected error executing ruff rule {rule_code}")
|
|
45
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ruff-sync
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5.dev2
|
|
4
4
|
Summary: Synchronize Ruff linter configuration across projects
|
|
5
5
|
Project-URL: Homepage, https://github.com/Kilo59/ruff-sync
|
|
6
6
|
Project-URL: Documentation, https://kilo59.github.io/ruff-sync/
|
|
@@ -27,6 +27,8 @@ Requires-Python: >=3.10
|
|
|
27
27
|
Requires-Dist: httpx<1.0.0,>=0.27.0
|
|
28
28
|
Requires-Dist: tomlkit<2.0.0,>=0.12.3
|
|
29
29
|
Requires-Dist: typing-extensions>=4.5.0
|
|
30
|
+
Provides-Extra: tui
|
|
31
|
+
Requires-Dist: textual>=8.2.2; extra == 'tui'
|
|
30
32
|
Description-Content-Type: text/markdown
|
|
31
33
|
|
|
32
34
|
<p align="center">
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ruff_sync/__init__.py,sha256=BFndMiyH2201W9mvl585ox331iwY_L5T_uX9HUJS81c,1101
|
|
2
|
+
ruff_sync/__main__.py,sha256=oMIbv9ywToleBKJrZ-GMo_KhM4lHg02nJe7t6neWzJE,179
|
|
3
|
+
ruff_sync/cli.py,sha256=hdq5M7GdMQm5BrJrRVEwl9LRJvqtYvbgM-GRrkS6SK0,20726
|
|
4
|
+
ruff_sync/config_io.py,sha256=3GlFjFlii_hBLaNnUJRfFI0iozkngTBbfqu4rLrpZKQ,4348
|
|
5
|
+
ruff_sync/constants.py,sha256=yE6ORJaErY8Bz5KTDDk7kRKLOlJ-xgS7Gvz_mFpjDJY,4604
|
|
6
|
+
ruff_sync/core.py,sha256=RtuKFRHjZmvi5W7bD3quD4ueqD8wt_5R2xWbtx3_MbI,42599
|
|
7
|
+
ruff_sync/dependencies.py,sha256=ND4WMPKmXjRTuanMVsYxZJ60mR6umffCnFJVZEpV8p4,1145
|
|
8
|
+
ruff_sync/formatters.py,sha256=sDZgx7SYNM1pjKshZLTgQHuAeT0P_tqAeV0VRZuz5FA,26311
|
|
9
|
+
ruff_sync/pre_commit.py,sha256=0ltXUkmocbrSwwf6cPbNc_K5K9n-gvou98xJy9YUhr0,5303
|
|
10
|
+
ruff_sync/system.py,sha256=sAqp6U4OQ4tWmfX2QD4b2ztutfz0SrqnAfuwaoygrbM,1350
|
|
11
|
+
ruff_sync-0.1.5.dev2.dist-info/METADATA,sha256=3pAjsPujF8BUALhRBy3DcME-WVa4E2OXeiq81DUO8b8,21464
|
|
12
|
+
ruff_sync-0.1.5.dev2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
13
|
+
ruff_sync-0.1.5.dev2.dist-info/entry_points.txt,sha256=z8TlIElMWLlqY_YWglHeMBksDohf2voVih6YnCBCWzk,45
|
|
14
|
+
ruff_sync-0.1.5.dev2.dist-info/licenses/LICENSE.md,sha256=RcCP8t8xyCx2cvMZSLjBrPqJfctciWC6q85ceQAcEfA,1080
|
|
15
|
+
ruff_sync-0.1.5.dev2.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
ruff_sync/__init__.py,sha256=k3I_7nbBGsORKMVpbE57pzkRiV18WvztBmw5UQRmASo,849
|
|
2
|
-
ruff_sync/__main__.py,sha256=oMIbv9ywToleBKJrZ-GMo_KhM4lHg02nJe7t6neWzJE,179
|
|
3
|
-
ruff_sync/cli.py,sha256=MKhLxUMlduR8aPhCzWLDewBm77HDqeZqCcJ7JyND1GI,17724
|
|
4
|
-
ruff_sync/constants.py,sha256=10YBAxowzmd_0wwBHeAKgWRYdyI5WnK-OiPl16prNxc,2817
|
|
5
|
-
ruff_sync/core.py,sha256=0PEvEdSsO8lKsScw9qwi08xK37HG8e3V-BKeR6JotzU,45302
|
|
6
|
-
ruff_sync/formatters.py,sha256=sDZgx7SYNM1pjKshZLTgQHuAeT0P_tqAeV0VRZuz5FA,26311
|
|
7
|
-
ruff_sync/pre_commit.py,sha256=0ltXUkmocbrSwwf6cPbNc_K5K9n-gvou98xJy9YUhr0,5303
|
|
8
|
-
ruff_sync-0.1.4.dev1.dist-info/METADATA,sha256=pyknLtGnrKrR_2fGmPQOn3JYy6mjE9F8YVS1BDDtbjw,21398
|
|
9
|
-
ruff_sync-0.1.4.dev1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
-
ruff_sync-0.1.4.dev1.dist-info/entry_points.txt,sha256=z8TlIElMWLlqY_YWglHeMBksDohf2voVih6YnCBCWzk,45
|
|
11
|
-
ruff_sync-0.1.4.dev1.dist-info/licenses/LICENSE.md,sha256=RcCP8t8xyCx2cvMZSLjBrPqJfctciWC6q85ceQAcEfA,1080
|
|
12
|
-
ruff_sync-0.1.4.dev1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|