ruff-sync 0.1.6.dev2__py3-none-any.whl → 0.1.6.dev4__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/cli.py +131 -37
- ruff_sync/constants.py +41 -3
- ruff_sync/core.py +34 -17
- ruff_sync/validation.py +102 -2
- {ruff_sync-0.1.6.dev2.dist-info → ruff_sync-0.1.6.dev4.dist-info}/METADATA +1 -1
- {ruff_sync-0.1.6.dev2.dist-info → ruff_sync-0.1.6.dev4.dist-info}/RECORD +9 -9
- {ruff_sync-0.1.6.dev2.dist-info → ruff_sync-0.1.6.dev4.dist-info}/WHEEL +0 -0
- {ruff_sync-0.1.6.dev2.dist-info → ruff_sync-0.1.6.dev4.dist-info}/entry_points.txt +0 -0
- {ruff_sync-0.1.6.dev2.dist-info → ruff_sync-0.1.6.dev4.dist-info}/licenses/LICENSE.md +0 -0
ruff_sync/cli.py
CHANGED
|
@@ -31,7 +31,9 @@ from ruff_sync.constants import (
|
|
|
31
31
|
DEFAULT_BRANCH,
|
|
32
32
|
DEFAULT_EXCLUDE,
|
|
33
33
|
DEFAULT_PATH,
|
|
34
|
+
MISSING,
|
|
34
35
|
ConfKey,
|
|
36
|
+
MissingType,
|
|
35
37
|
OutputFormat,
|
|
36
38
|
)
|
|
37
39
|
from ruff_sync.core import (
|
|
@@ -105,11 +107,11 @@ class Arguments(NamedTuple):
|
|
|
105
107
|
semantic: bool = False
|
|
106
108
|
diff: bool = True
|
|
107
109
|
init: bool = False
|
|
108
|
-
pre_commit: bool =
|
|
110
|
+
pre_commit: bool | MissingType = MISSING
|
|
109
111
|
save: bool | None = None
|
|
110
112
|
output_format: OutputFormat = OutputFormat.TEXT
|
|
111
|
-
validate: bool =
|
|
112
|
-
strict: bool =
|
|
113
|
+
validate: bool | MissingType = MISSING # run --validate checks before writing
|
|
114
|
+
strict: bool | MissingType = MISSING # treat warnings as errors (implies --validate)
|
|
113
115
|
|
|
114
116
|
@property
|
|
115
117
|
@deprecated("Use 'to' instead")
|
|
@@ -123,6 +125,53 @@ class Arguments(NamedTuple):
|
|
|
123
125
|
"""Return the set of all field names, including deprecated ones."""
|
|
124
126
|
return set(cls._fields) | {"source"}
|
|
125
127
|
|
|
128
|
+
def resolve(self) -> ExecutionArgs:
|
|
129
|
+
"""Resolve all MISSING sentinels to their effective defaults for execution."""
|
|
130
|
+
from ruff_sync.constants import resolve_bool_flags # noqa: PLC0415
|
|
131
|
+
|
|
132
|
+
eff_validate, eff_strict, eff_pre_commit = resolve_bool_flags(
|
|
133
|
+
self.validate,
|
|
134
|
+
self.strict,
|
|
135
|
+
self.pre_commit,
|
|
136
|
+
)
|
|
137
|
+
return ExecutionArgs(
|
|
138
|
+
command=self.command,
|
|
139
|
+
upstream=self.upstream,
|
|
140
|
+
to=self.to,
|
|
141
|
+
exclude=self.exclude,
|
|
142
|
+
verbose=self.verbose,
|
|
143
|
+
branch=self.branch,
|
|
144
|
+
path=self.path,
|
|
145
|
+
semantic=self.semantic,
|
|
146
|
+
diff=self.diff,
|
|
147
|
+
init=self.init,
|
|
148
|
+
pre_commit=eff_pre_commit,
|
|
149
|
+
save=self.save,
|
|
150
|
+
output_format=self.output_format,
|
|
151
|
+
validate=eff_validate,
|
|
152
|
+
strict=eff_strict,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ExecutionArgs(NamedTuple):
|
|
157
|
+
"""Resolved arguments for execution logic — all sentinels replaced with concrete values."""
|
|
158
|
+
|
|
159
|
+
command: str
|
|
160
|
+
upstream: tuple[URL, ...]
|
|
161
|
+
to: pathlib.Path
|
|
162
|
+
exclude: Iterable[str]
|
|
163
|
+
verbose: int
|
|
164
|
+
branch: str
|
|
165
|
+
path: str | None
|
|
166
|
+
semantic: bool
|
|
167
|
+
diff: bool
|
|
168
|
+
init: bool
|
|
169
|
+
pre_commit: bool # plain bool — MISSING resolved to default
|
|
170
|
+
save: bool | None
|
|
171
|
+
output_format: OutputFormat
|
|
172
|
+
validate: bool # plain bool — MISSING resolved to default
|
|
173
|
+
strict: bool # plain bool — MISSING resolved to default
|
|
174
|
+
|
|
126
175
|
|
|
127
176
|
class ResolvedArgs(NamedTuple):
|
|
128
177
|
"""Internal container for resolved arguments."""
|
|
@@ -153,8 +202,8 @@ class CLIArguments(Protocol):
|
|
|
153
202
|
save: bool | None
|
|
154
203
|
semantic: bool
|
|
155
204
|
diff: bool
|
|
156
|
-
validate: bool
|
|
157
|
-
strict: bool
|
|
205
|
+
validate: bool | None
|
|
206
|
+
strict: bool | None
|
|
158
207
|
|
|
159
208
|
|
|
160
209
|
@lru_cache(maxsize=1)
|
|
@@ -294,6 +343,24 @@ def _get_cli_parser() -> ArgumentParser:
|
|
|
294
343
|
default=None,
|
|
295
344
|
help="Format for output. Default: text (auto-detected in CI).",
|
|
296
345
|
)
|
|
346
|
+
common_parser.add_argument(
|
|
347
|
+
"--validate",
|
|
348
|
+
action=BooleanOptionalAction,
|
|
349
|
+
default=None,
|
|
350
|
+
help=(
|
|
351
|
+
"Validate the merged config with Ruff before writing to disk. "
|
|
352
|
+
"Aborts if Ruff rejects the config. Off by default."
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
common_parser.add_argument(
|
|
356
|
+
"--strict",
|
|
357
|
+
action=BooleanOptionalAction,
|
|
358
|
+
default=None,
|
|
359
|
+
help=(
|
|
360
|
+
"Treat all validation warnings as hard failures. Implies --validate. "
|
|
361
|
+
"Aborts if any version mismatch or deprecated rule is detected."
|
|
362
|
+
),
|
|
363
|
+
)
|
|
297
364
|
|
|
298
365
|
# Pull subcommand (the default action)
|
|
299
366
|
pull_parser = subparsers.add_parser(
|
|
@@ -312,24 +379,6 @@ def _get_cli_parser() -> ArgumentParser:
|
|
|
312
379
|
default=None,
|
|
313
380
|
help="Save CLI arguments to [tool.ruff-sync] in pyproject.toml.",
|
|
314
381
|
)
|
|
315
|
-
pull_parser.add_argument(
|
|
316
|
-
"--validate",
|
|
317
|
-
action="store_true",
|
|
318
|
-
default=False,
|
|
319
|
-
help=(
|
|
320
|
-
"Validate the merged config with Ruff before writing to disk. "
|
|
321
|
-
"Aborts if Ruff rejects the config. Off by default."
|
|
322
|
-
),
|
|
323
|
-
)
|
|
324
|
-
pull_parser.add_argument(
|
|
325
|
-
"--strict",
|
|
326
|
-
action="store_true",
|
|
327
|
-
default=False,
|
|
328
|
-
help=(
|
|
329
|
-
"Treat all validation warnings as hard failures. Implies --validate. "
|
|
330
|
-
"Aborts if any version mismatch or deprecated rule is detected."
|
|
331
|
-
),
|
|
332
|
-
)
|
|
333
382
|
|
|
334
383
|
# Check subcommand
|
|
335
384
|
check_parser = subparsers.add_parser(
|
|
@@ -500,6 +549,44 @@ def _resolve_to(args: CLIArguments, config: Config, initial_to: pathlib.Path) ->
|
|
|
500
549
|
return initial_to
|
|
501
550
|
|
|
502
551
|
|
|
552
|
+
def _resolve_validate(args: CLIArguments, config: Config) -> bool | MissingType:
|
|
553
|
+
"""Resolve validation setting from CLI or config.
|
|
554
|
+
|
|
555
|
+
If ``--strict`` was explicitly set at the CLI level, it implies ``--validate``.
|
|
556
|
+
This prevents ``validate = false`` in config from silently neutralising a
|
|
557
|
+
CLI ``--strict`` flag.
|
|
558
|
+
"""
|
|
559
|
+
# CLI --validate flag wins if explicitly provided
|
|
560
|
+
if args.validate is not None:
|
|
561
|
+
return args.validate
|
|
562
|
+
|
|
563
|
+
# If --strict was explicitly passed via CLI, it implies validate=True
|
|
564
|
+
# even when config sets `validate = false`.
|
|
565
|
+
if args.strict is True:
|
|
566
|
+
return True
|
|
567
|
+
|
|
568
|
+
# Fallback to config
|
|
569
|
+
validate_attr = ConfKey.to_attr(ConfKey.VALIDATE)
|
|
570
|
+
if validate_attr in config:
|
|
571
|
+
return bool(config[validate_attr]) # type: ignore[literal-required]
|
|
572
|
+
|
|
573
|
+
return MISSING
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _resolve_strict(args: CLIArguments, config: Config) -> bool | MissingType:
|
|
577
|
+
"""Resolve strict setting from CLI or config."""
|
|
578
|
+
# CLI flag --strict always wins
|
|
579
|
+
if args.strict is not None:
|
|
580
|
+
return args.strict
|
|
581
|
+
|
|
582
|
+
# Fallback to config
|
|
583
|
+
attr = ConfKey.to_attr(ConfKey.STRICT)
|
|
584
|
+
if attr in config:
|
|
585
|
+
return bool(config[attr]) # type: ignore[literal-required]
|
|
586
|
+
|
|
587
|
+
return MISSING
|
|
588
|
+
|
|
589
|
+
|
|
503
590
|
def _resolve_args(args: CLIArguments, config: Config, initial_to: pathlib.Path) -> ResolvedArgs:
|
|
504
591
|
"""Resolve upstream, to, exclude, branch, and path from CLI and config."""
|
|
505
592
|
upstream = _resolve_upstream(args, config)
|
|
@@ -522,7 +609,7 @@ def _resolve_args(args: CLIArguments, config: Config, initial_to: pathlib.Path)
|
|
|
522
609
|
)
|
|
523
610
|
|
|
524
611
|
|
|
525
|
-
def _resolve_pre_commit(args: CLIArguments, config: Config) -> bool:
|
|
612
|
+
def _resolve_pre_commit(args: CLIArguments, config: Config) -> bool | MissingType:
|
|
526
613
|
"""Resolve pre-commit sync setting from CLI or config."""
|
|
527
614
|
if args.pre_commit is not None:
|
|
528
615
|
return args.pre_commit
|
|
@@ -531,7 +618,7 @@ def _resolve_pre_commit(args: CLIArguments, config: Config) -> bool:
|
|
|
531
618
|
if pre_commit_attr in config:
|
|
532
619
|
val = config.get(pre_commit_attr)
|
|
533
620
|
return bool(val)
|
|
534
|
-
return
|
|
621
|
+
return MISSING
|
|
535
622
|
|
|
536
623
|
|
|
537
624
|
def _detect_ci_provider() -> CIProvider | None:
|
|
@@ -599,28 +686,35 @@ def main() -> int:
|
|
|
599
686
|
initial_to = pathlib.Path(arg_to) if arg_to else pathlib.Path()
|
|
600
687
|
config: Config = get_config(initial_to)
|
|
601
688
|
|
|
602
|
-
|
|
603
|
-
|
|
689
|
+
# Resolve all arguments (honoring CLI overrides over config values)
|
|
690
|
+
resolved = _resolve_args(args, config, initial_to)
|
|
691
|
+
|
|
692
|
+
resolved_upstream = tuple(
|
|
693
|
+
resolve_raw_url(u, branch=resolved.branch, path=resolved.path) for u in resolved.upstream
|
|
694
|
+
)
|
|
604
695
|
|
|
605
|
-
|
|
696
|
+
# Resolve validation flags from CLI (with CLI --strict implying validate=True)
|
|
697
|
+
# and fall back to config. Bidirectional implications are handled by
|
|
698
|
+
# resolve_bool_flags() inside Arguments.resolve(), so we store raw sentinels here.
|
|
699
|
+
raw_validate = _resolve_validate(args, config)
|
|
700
|
+
raw_strict = _resolve_strict(args, config)
|
|
606
701
|
|
|
607
702
|
exec_args = Arguments(
|
|
608
703
|
command=args.command,
|
|
609
704
|
upstream=resolved_upstream,
|
|
610
|
-
to=
|
|
611
|
-
exclude=exclude,
|
|
705
|
+
to=resolved.to,
|
|
706
|
+
exclude=resolved.exclude,
|
|
612
707
|
verbose=args.verbose,
|
|
613
|
-
branch=branch,
|
|
614
|
-
path=path,
|
|
708
|
+
branch=resolved.branch,
|
|
709
|
+
path=resolved.path,
|
|
615
710
|
semantic=getattr(args, "semantic", False),
|
|
616
711
|
diff=getattr(args, "diff", True),
|
|
617
712
|
init=getattr(args, "init", False),
|
|
618
|
-
pre_commit=
|
|
713
|
+
pre_commit=_resolve_pre_commit(args, config),
|
|
619
714
|
save=getattr(args, "save", None),
|
|
620
|
-
output_format=output_format,
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
strict=getattr(args, "strict", False),
|
|
715
|
+
output_format=resolved.output_format,
|
|
716
|
+
validate=raw_validate,
|
|
717
|
+
strict=raw_strict,
|
|
624
718
|
)
|
|
625
719
|
|
|
626
720
|
# Warn if the specified output format doesn't match the current CI environment
|
ruff_sync/constants.py
CHANGED
|
@@ -81,6 +81,8 @@ class ConfKey(str, enum.Enum):
|
|
|
81
81
|
INIT = "init"
|
|
82
82
|
SAVE = "save"
|
|
83
83
|
VERBOSE = "verbose"
|
|
84
|
+
VALIDATE = "validate"
|
|
85
|
+
STRICT = "strict"
|
|
84
86
|
|
|
85
87
|
# Legacy / Alias Keys
|
|
86
88
|
SOURCE = "source" # Legacy for 'to'
|
|
@@ -127,13 +129,13 @@ class ConfKey(str, enum.Enum):
|
|
|
127
129
|
|
|
128
130
|
def resolve_defaults(
|
|
129
131
|
branch: str | MissingType,
|
|
130
|
-
path: str | MissingType,
|
|
132
|
+
path: str | None | MissingType,
|
|
131
133
|
exclude: Iterable[str] | MissingType,
|
|
132
134
|
) -> tuple[str, str | None, Iterable[str]]:
|
|
133
135
|
"""Resolve MISSING sentinel values to their effective defaults.
|
|
134
136
|
|
|
135
|
-
This is the single source of truth for MISSING → default resolution
|
|
136
|
-
|
|
137
|
+
This is the single source of truth for MISSING → default resolution for
|
|
138
|
+
URL-related parameters (branch, path, exclude).
|
|
137
139
|
|
|
138
140
|
Args:
|
|
139
141
|
branch: The resolved branch value or ``MISSING``.
|
|
@@ -151,4 +153,40 @@ def resolve_defaults(
|
|
|
151
153
|
# but explicit None is the canonical "root directory" sentinel.
|
|
152
154
|
eff_path: str | None = raw_path or None
|
|
153
155
|
eff_exclude = exclude if exclude is not MISSING else DEFAULT_EXCLUDE
|
|
156
|
+
|
|
154
157
|
return eff_branch, eff_path, eff_exclude
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def apply_bool_precedence(
|
|
161
|
+
validate: bool | MissingType, strict: bool | MissingType
|
|
162
|
+
) -> tuple[bool | MissingType, bool | MissingType]:
|
|
163
|
+
"""Apply precedence rules for validate and strict flags.
|
|
164
|
+
|
|
165
|
+
1. Disabling validation implicitly disables strict mode.
|
|
166
|
+
2. Strict mode implicitly enables validation.
|
|
167
|
+
"""
|
|
168
|
+
if validate is False:
|
|
169
|
+
strict = False
|
|
170
|
+
elif strict is True:
|
|
171
|
+
validate = True
|
|
172
|
+
return validate, strict
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def resolve_bool_flags(
|
|
176
|
+
validate: bool | MissingType = MISSING,
|
|
177
|
+
strict: bool | MissingType = MISSING,
|
|
178
|
+
pre_commit: bool | MissingType = MISSING,
|
|
179
|
+
) -> tuple[bool, bool, bool]:
|
|
180
|
+
"""Resolve MISSING sentinel values for boolean execution flags.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
A ``(validate, strict, pre_commit)`` tuple with defaults applied.
|
|
184
|
+
``strict=True`` implicitly enables ``validate``.
|
|
185
|
+
"""
|
|
186
|
+
validate, strict = apply_bool_precedence(validate, strict)
|
|
187
|
+
|
|
188
|
+
eff_validate = validate if validate is not MISSING else False
|
|
189
|
+
eff_strict = strict if strict is not MISSING else False
|
|
190
|
+
eff_pre_commit = pre_commit if pre_commit is not MISSING else False
|
|
191
|
+
|
|
192
|
+
return eff_validate, eff_strict, eff_pre_commit
|
ruff_sync/core.py
CHANGED
|
@@ -34,6 +34,7 @@ from ruff_sync.constants import (
|
|
|
34
34
|
DEFAULT_BRANCH,
|
|
35
35
|
DEFAULT_EXCLUDE,
|
|
36
36
|
DEFAULT_PATH,
|
|
37
|
+
MISSING,
|
|
37
38
|
ConfKey,
|
|
38
39
|
)
|
|
39
40
|
from ruff_sync.formatters import ResultFormatter, get_formatter
|
|
@@ -42,7 +43,7 @@ from ruff_sync.pre_commit import sync_pre_commit
|
|
|
42
43
|
if TYPE_CHECKING:
|
|
43
44
|
from tomlkit.container import Container
|
|
44
45
|
|
|
45
|
-
from ruff_sync.cli import Arguments
|
|
46
|
+
from ruff_sync.cli import Arguments, ExecutionArgs
|
|
46
47
|
|
|
47
48
|
__all__: Final[list[str]] = [
|
|
48
49
|
"Config",
|
|
@@ -123,6 +124,8 @@ class Config(TypedDict, total=False):
|
|
|
123
124
|
init: bool
|
|
124
125
|
pre_commit_version_sync: bool
|
|
125
126
|
output_format: str
|
|
127
|
+
validate: bool
|
|
128
|
+
strict: bool
|
|
126
129
|
|
|
127
130
|
|
|
128
131
|
def _resolve_upstream_target_path(path: str | None) -> str:
|
|
@@ -743,7 +746,7 @@ async def fetch_upstreams_concurrently(
|
|
|
743
746
|
async def _merge_multiple_upstreams(
|
|
744
747
|
target_doc: TOMLDocument,
|
|
745
748
|
is_target_ruff_toml: bool,
|
|
746
|
-
args:
|
|
749
|
+
args: ExecutionArgs,
|
|
747
750
|
client: httpx.AsyncClient,
|
|
748
751
|
) -> TOMLDocument:
|
|
749
752
|
"""Fetch and merge all upstreams into the target document.
|
|
@@ -751,10 +754,8 @@ async def _merge_multiple_upstreams(
|
|
|
751
754
|
Downloads are performed concurrently via fetch_upstreams_concurrently,
|
|
752
755
|
while merging remains sequential to preserve layering order.
|
|
753
756
|
"""
|
|
754
|
-
branch, path, exclude = args.branch, args.path, args.exclude
|
|
755
|
-
|
|
756
757
|
fetch_results = await fetch_upstreams_concurrently(
|
|
757
|
-
args.upstream, client, branch=branch, path=path
|
|
758
|
+
args.upstream, client, branch=args.branch, path=args.path
|
|
758
759
|
)
|
|
759
760
|
|
|
760
761
|
# Sequentially merge the results in the original order
|
|
@@ -767,7 +768,7 @@ async def _merge_multiple_upstreams(
|
|
|
767
768
|
fetch_result.buffer.read(),
|
|
768
769
|
is_ruff_toml=is_upstream_ruff_toml,
|
|
769
770
|
create_if_missing=False,
|
|
770
|
-
exclude=exclude,
|
|
771
|
+
exclude=args.exclude,
|
|
771
772
|
)
|
|
772
773
|
|
|
773
774
|
target_doc = merge_ruff_toml(
|
|
@@ -804,14 +805,14 @@ def _print_diff(
|
|
|
804
805
|
fmt.diff("".join(diff))
|
|
805
806
|
|
|
806
807
|
|
|
807
|
-
def _check_pre_commit_sync(
|
|
808
|
+
def _check_pre_commit_sync(pre_commit: bool, fmt: ResultFormatter) -> int | None:
|
|
808
809
|
"""Return exit code 3 if pre-commit hook version is out of sync, otherwise None.
|
|
809
810
|
|
|
810
811
|
Shared helper to avoid duplicating the pre-commit synchronization logic.
|
|
811
812
|
Exit code 3 is reserved for pre-commit hook drift to avoid collision with
|
|
812
813
|
argparse (2) and config drift (1).
|
|
813
814
|
"""
|
|
814
|
-
if
|
|
815
|
+
if pre_commit and not sync_pre_commit(pathlib.Path.cwd(), dry_run=True):
|
|
815
816
|
repo_root = pathlib.Path.cwd()
|
|
816
817
|
pre_commit_config = repo_root / ".pre-commit-config.yaml"
|
|
817
818
|
file_path = pre_commit_config if pre_commit_config.exists() else repo_root
|
|
@@ -944,6 +945,9 @@ async def check(
|
|
|
944
945
|
... )
|
|
945
946
|
>>> # asyncio.run(check(args))
|
|
946
947
|
"""
|
|
948
|
+
# Resolve defaults for execution logic
|
|
949
|
+
exec_args = args.resolve()
|
|
950
|
+
|
|
947
951
|
output_format = args.output_format
|
|
948
952
|
fmt: ResultFormatter = get_formatter(output_format)
|
|
949
953
|
try:
|
|
@@ -970,7 +974,7 @@ async def check(
|
|
|
970
974
|
merged_doc = await _merge_multiple_upstreams(
|
|
971
975
|
merged_doc,
|
|
972
976
|
is_target_ruff_toml=is_ruff_toml_file(_source_toml_path.name),
|
|
973
|
-
args=
|
|
977
|
+
args=exec_args,
|
|
974
978
|
client=client,
|
|
975
979
|
)
|
|
976
980
|
|
|
@@ -991,13 +995,13 @@ async def check(
|
|
|
991
995
|
|
|
992
996
|
if source_val == merged_val:
|
|
993
997
|
fmt.success("✅ Ruff configuration is semantically in sync.")
|
|
994
|
-
exit_code = _check_pre_commit_sync(
|
|
998
|
+
exit_code = _check_pre_commit_sync(exec_args.pre_commit, fmt)
|
|
995
999
|
if exit_code is not None:
|
|
996
1000
|
return exit_code
|
|
997
1001
|
return 0
|
|
998
1002
|
elif source_doc.as_string() == merged_doc.as_string():
|
|
999
1003
|
fmt.success("✅ Ruff configuration is in sync.")
|
|
1000
|
-
exit_code = _check_pre_commit_sync(
|
|
1004
|
+
exit_code = _check_pre_commit_sync(exec_args.pre_commit, fmt)
|
|
1001
1005
|
if exit_code is not None:
|
|
1002
1006
|
return exit_code
|
|
1003
1007
|
return 0
|
|
@@ -1096,8 +1100,15 @@ def serialize_ruff_sync_config(doc: TOMLDocument, args: Arguments) -> None:
|
|
|
1096
1100
|
if args.path != DEFAULT_PATH and args.path is not None:
|
|
1097
1101
|
ruff_sync_table[ConfKey.PATH] = args.path
|
|
1098
1102
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1103
|
+
# Simple boolean flags
|
|
1104
|
+
for key, attr in [
|
|
1105
|
+
(ConfKey.PRE_COMMIT_VERSION_SYNC, "pre_commit"),
|
|
1106
|
+
(ConfKey.VALIDATE, "validate"),
|
|
1107
|
+
(ConfKey.STRICT, "strict"),
|
|
1108
|
+
]:
|
|
1109
|
+
val = getattr(args, attr, MISSING)
|
|
1110
|
+
if val is not MISSING:
|
|
1111
|
+
ruff_sync_table[key] = val
|
|
1101
1112
|
|
|
1102
1113
|
|
|
1103
1114
|
async def pull(
|
|
@@ -1122,6 +1133,9 @@ async def pull(
|
|
|
1122
1133
|
... )
|
|
1123
1134
|
>>> # asyncio.run(pull(args))
|
|
1124
1135
|
"""
|
|
1136
|
+
# Resolve defaults for execution logic
|
|
1137
|
+
exec_args = args.resolve()
|
|
1138
|
+
|
|
1125
1139
|
output_format = args.output_format
|
|
1126
1140
|
fmt = get_formatter(output_format)
|
|
1127
1141
|
try:
|
|
@@ -1154,17 +1168,20 @@ async def pull(
|
|
|
1154
1168
|
source_doc = await _merge_multiple_upstreams(
|
|
1155
1169
|
source_doc,
|
|
1156
1170
|
is_target_ruff_toml=is_ruff_toml_file(_source_toml_path.name),
|
|
1157
|
-
args=
|
|
1171
|
+
args=exec_args,
|
|
1158
1172
|
client=client,
|
|
1159
1173
|
)
|
|
1160
1174
|
|
|
1161
1175
|
# Validation is opt-in — only run if --validate (or --strict) was passed
|
|
1162
|
-
if
|
|
1176
|
+
if exec_args.validate:
|
|
1163
1177
|
from ruff_sync.validation import validate_merged_config # noqa: PLC0415
|
|
1164
1178
|
|
|
1165
1179
|
is_ruff_toml = is_ruff_toml_file(_source_toml_path.name)
|
|
1166
1180
|
if not validate_merged_config(
|
|
1167
|
-
source_doc,
|
|
1181
|
+
source_doc,
|
|
1182
|
+
is_ruff_toml=is_ruff_toml,
|
|
1183
|
+
strict=exec_args.strict,
|
|
1184
|
+
exclude=exec_args.exclude,
|
|
1168
1185
|
):
|
|
1169
1186
|
fmt.error(
|
|
1170
1187
|
"❌ Merged config failed validation. Local file left unchanged.",
|
|
@@ -1190,7 +1207,7 @@ async def pull(
|
|
|
1190
1207
|
rel_path = _source_toml_path.resolve()
|
|
1191
1208
|
fmt.success(f"✅ Updated {rel_path}")
|
|
1192
1209
|
|
|
1193
|
-
if
|
|
1210
|
+
if exec_args.pre_commit:
|
|
1194
1211
|
sync_pre_commit(pathlib.Path.cwd(), dry_run=False)
|
|
1195
1212
|
|
|
1196
1213
|
return 0
|
ruff_sync/validation.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import functools
|
|
6
|
+
import json
|
|
5
7
|
import logging
|
|
6
8
|
import pathlib
|
|
7
9
|
import re
|
|
@@ -109,6 +111,97 @@ def check_python_version_consistency(
|
|
|
109
111
|
return True
|
|
110
112
|
|
|
111
113
|
|
|
114
|
+
_RULE_LIST_KEYS: frozenset[str] = frozenset({"select", "extend-select", "ignore", "extend-ignore"})
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@functools.lru_cache(maxsize=1)
|
|
118
|
+
def _get_deprecated_rule_codes() -> frozenset[str]:
|
|
119
|
+
"""Return the set of deprecated rule codes reported by the installed Ruff version.
|
|
120
|
+
|
|
121
|
+
Returns an empty frozenset if ruff is not found or output cannot be parsed.
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
result = subprocess.run(
|
|
125
|
+
["ruff", "rule", "--all", "--output-format", "json"], # noqa: S607
|
|
126
|
+
capture_output=True,
|
|
127
|
+
text=True,
|
|
128
|
+
timeout=30,
|
|
129
|
+
check=False,
|
|
130
|
+
)
|
|
131
|
+
if result.returncode != 0:
|
|
132
|
+
return frozenset()
|
|
133
|
+
rules = json.loads(result.stdout)
|
|
134
|
+
return frozenset(r["code"] for r in rules if r.get("deprecated") is True)
|
|
135
|
+
except (
|
|
136
|
+
FileNotFoundError,
|
|
137
|
+
subprocess.TimeoutExpired,
|
|
138
|
+
json.JSONDecodeError,
|
|
139
|
+
KeyError,
|
|
140
|
+
TypeError,
|
|
141
|
+
):
|
|
142
|
+
return frozenset()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def check_deprecated_rules(
|
|
146
|
+
doc: TOMLDocument,
|
|
147
|
+
is_ruff_toml: bool = False,
|
|
148
|
+
strict: bool = False,
|
|
149
|
+
_deprecated_codes: frozenset[str] | None = None,
|
|
150
|
+
exclude: Iterable[str] = (),
|
|
151
|
+
) -> bool:
|
|
152
|
+
"""Warn if the merged config references any deprecated Ruff rules.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
doc: The merged TOML document.
|
|
156
|
+
is_ruff_toml: True if the document is a standalone ruff.toml.
|
|
157
|
+
strict: If True, treat deprecated rules as a failure.
|
|
158
|
+
_deprecated_codes: Optional set of deprecated codes to use (for testing).
|
|
159
|
+
exclude: List of keys excluded from the ruff configuration.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if no deprecated rules are found, or if strict=False.
|
|
163
|
+
False if strict=True and deprecated rules are found.
|
|
164
|
+
"""
|
|
165
|
+
deprecated_codes = (
|
|
166
|
+
_deprecated_codes if _deprecated_codes is not None else _get_deprecated_rule_codes()
|
|
167
|
+
)
|
|
168
|
+
if not deprecated_codes:
|
|
169
|
+
return True # Nothing to check (ruff not found or returned no deprecated rules)
|
|
170
|
+
|
|
171
|
+
if is_ruff_toml:
|
|
172
|
+
lint_section = doc.get("lint", {})
|
|
173
|
+
else:
|
|
174
|
+
lint_section = doc.get("tool", {}).get("ruff", {}).get("lint", {})
|
|
175
|
+
|
|
176
|
+
if not isinstance(lint_section, dict):
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
found_deprecated = False
|
|
180
|
+
for key in _RULE_LIST_KEYS:
|
|
181
|
+
full_key = f"lint.{key}" if is_ruff_toml else f"tool.ruff.lint.{key}"
|
|
182
|
+
if key in exclude or full_key in exclude:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
rule_list = lint_section.get(key, [])
|
|
186
|
+
if not isinstance(rule_list, list):
|
|
187
|
+
continue
|
|
188
|
+
for rule_code in rule_list:
|
|
189
|
+
rule_str = str(rule_code).strip().upper()
|
|
190
|
+
if rule_str in deprecated_codes:
|
|
191
|
+
found_deprecated = True
|
|
192
|
+
msg = (
|
|
193
|
+
f"Upstream config uses deprecated rule '{rule_str}' "
|
|
194
|
+
f"(found in [{'lint' if is_ruff_toml else 'tool.ruff.lint'}].{key}). "
|
|
195
|
+
"This rule may be removed in a future Ruff version."
|
|
196
|
+
)
|
|
197
|
+
if strict:
|
|
198
|
+
LOGGER.error(f"❌ {msg}")
|
|
199
|
+
else:
|
|
200
|
+
LOGGER.warning(f"⚠️ {msg}")
|
|
201
|
+
|
|
202
|
+
return not (strict and found_deprecated)
|
|
203
|
+
|
|
204
|
+
|
|
112
205
|
def validate_toml_syntax(doc: TOMLDocument) -> bool:
|
|
113
206
|
"""Return True if the document serializes to valid TOML.
|
|
114
207
|
|
|
@@ -218,6 +311,13 @@ def validate_merged_config(
|
|
|
218
311
|
return False
|
|
219
312
|
if not validate_ruff_accepts_config(doc, is_ruff_toml=is_ruff_toml, strict=strict):
|
|
220
313
|
return False
|
|
314
|
+
|
|
315
|
+
version_ok = True
|
|
221
316
|
if not is_ruff_toml:
|
|
222
|
-
|
|
223
|
-
|
|
317
|
+
version_ok = check_python_version_consistency(doc, strict=strict, exclude=exclude)
|
|
318
|
+
|
|
319
|
+
deprecated_ok = check_deprecated_rules(
|
|
320
|
+
doc, is_ruff_toml=is_ruff_toml, strict=strict, exclude=exclude
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return version_ok and deprecated_ok
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ruff-sync
|
|
3
|
-
Version: 0.1.6.
|
|
3
|
+
Version: 0.1.6.dev4
|
|
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/
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
ruff_sync/__init__.py,sha256=M_1YK0BcPzlLlxlBOEPEBTCkAVm37r-yoQzGGZnfQ7Y,1206
|
|
2
2
|
ruff_sync/__main__.py,sha256=oMIbv9ywToleBKJrZ-GMo_KhM4lHg02nJe7t6neWzJE,179
|
|
3
|
-
ruff_sync/cli.py,sha256=
|
|
3
|
+
ruff_sync/cli.py,sha256=ieZVvMD2AlaHWSfXLidQhCRD87VmzcqV6J50tjnNRuc,25345
|
|
4
4
|
ruff_sync/config_io.py,sha256=3GlFjFlii_hBLaNnUJRfFI0iozkngTBbfqu4rLrpZKQ,4348
|
|
5
|
-
ruff_sync/constants.py,sha256=
|
|
6
|
-
ruff_sync/core.py,sha256=
|
|
5
|
+
ruff_sync/constants.py,sha256=g7ZU8mGdQ2682tKQuKkhRaTRRmg0K2vMVp3Ljs60D70,5822
|
|
6
|
+
ruff_sync/core.py,sha256=H2qJ_fn887ijlBh1Q4gh_24VHpzOqRjv5fvsNFhS0r0,44278
|
|
7
7
|
ruff_sync/dependencies.py,sha256=RpJXAsrB-Y-ol_ZH4U92XIuyq8_bWaYwp9sn9KpxraA,2096
|
|
8
8
|
ruff_sync/formatters.py,sha256=A4p2XAyc-xnteUmdtRXbp63fIjT9MdyKF6qKvcyNMho,29908
|
|
9
9
|
ruff_sync/pre_commit.py,sha256=0ltXUkmocbrSwwf6cPbNc_K5K9n-gvou98xJy9YUhr0,5303
|
|
10
10
|
ruff_sync/system.py,sha256=vF9Jjb-Z9MuLwo3wiOqcCDxniNtjvo3O-aFt4NJf-2Y,5715
|
|
11
11
|
ruff_sync/types_.py,sha256=BejR6sgctR0qr5fKgRTtgHp2TqnvT8zTRo1NJi2VHLo,824
|
|
12
|
-
ruff_sync/validation.py,sha256=
|
|
12
|
+
ruff_sync/validation.py,sha256=OatdkWUKEyxw7CYTOFNpH6IRsbVfU5EHjt6Lu6nOO9U,11585
|
|
13
13
|
ruff_sync/tui/__init__.py,sha256=DcwvE70ETjBcC6pQeHpjrYtkJp8z4StGM8L3EuaVcIo,635
|
|
14
14
|
ruff_sync/tui/app.py,sha256=soq0fkFsLeB7ZxJWNZM_H6bNj7H8p6nEYyirVcskN54,9317
|
|
15
15
|
ruff_sync/tui/constants.py,sha256=fEPyp2k817QLluyiVHqDObzDONlHz_L3Ec0SYAUzzMs,267
|
|
@@ -17,8 +17,8 @@ ruff_sync/tui/screens.py,sha256=NVIlqNIr64JrFFWeec7usqJCb1x41NEmSfRZGeEcwuo,5557
|
|
|
17
17
|
ruff_sync/tui/themes.py,sha256=-x9u_8iZPa1XZuo8O_tWVHyCImN9Qs9F7xPhl0VLwZw,654
|
|
18
18
|
ruff_sync/tui/types_.py,sha256=poNUBJotgUso2IkUi3nXUuxz6wv1goFWZimj4VttdJE,8271
|
|
19
19
|
ruff_sync/tui/widgets.py,sha256=2m3gyH4xDBpM0CK-5eQmmUCZXYKJujrbyVkFTxqmHcg,9760
|
|
20
|
-
ruff_sync-0.1.6.
|
|
21
|
-
ruff_sync-0.1.6.
|
|
22
|
-
ruff_sync-0.1.6.
|
|
23
|
-
ruff_sync-0.1.6.
|
|
24
|
-
ruff_sync-0.1.6.
|
|
20
|
+
ruff_sync-0.1.6.dev4.dist-info/METADATA,sha256=8Z7T_-halUxzrgamLZkseVji5ARHpLJMdYxPo8pg6HM,22706
|
|
21
|
+
ruff_sync-0.1.6.dev4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
22
|
+
ruff_sync-0.1.6.dev4.dist-info/entry_points.txt,sha256=QQ0pjxMIN0vp5EmNFkyb-1Yk8JOIkid8zAPN3IpAExA,78
|
|
23
|
+
ruff_sync-0.1.6.dev4.dist-info/licenses/LICENSE.md,sha256=RcCP8t8xyCx2cvMZSLjBrPqJfctciWC6q85ceQAcEfA,1080
|
|
24
|
+
ruff_sync-0.1.6.dev4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|