ruff-sync 0.1.5.dev2__py3-none-any.whl → 0.1.5.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/__init__.py +2 -0
- ruff_sync/cli.py +25 -9
- ruff_sync/core.py +45 -36
- ruff_sync/dependencies.py +37 -11
- ruff_sync/system.py +138 -5
- ruff_sync/tui/__init__.py +26 -0
- ruff_sync/tui/app.py +263 -0
- ruff_sync/tui/constants.py +9 -0
- ruff_sync/tui/screens.py +187 -0
- ruff_sync/tui/themes.py +22 -0
- ruff_sync/tui/types_.py +263 -0
- ruff_sync/tui/widgets.py +270 -0
- ruff_sync/types_.py +31 -0
- {ruff_sync-0.1.5.dev2.dist-info → ruff_sync-0.1.5.dev4.dist-info}/METADATA +1 -1
- ruff_sync-0.1.5.dev4.dist-info/RECORD +23 -0
- {ruff_sync-0.1.5.dev2.dist-info → ruff_sync-0.1.5.dev4.dist-info}/entry_points.txt +1 -0
- ruff_sync-0.1.5.dev2.dist-info/RECORD +0 -15
- {ruff_sync-0.1.5.dev2.dist-info → ruff_sync-0.1.5.dev4.dist-info}/WHEEL +0 -0
- {ruff_sync-0.1.5.dev2.dist-info → ruff_sync-0.1.5.dev4.dist-info}/licenses/LICENSE.md +0 -0
ruff_sync/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from .cli import (
|
|
|
9
9
|
Arguments,
|
|
10
10
|
__version__,
|
|
11
11
|
get_config,
|
|
12
|
+
inspect,
|
|
12
13
|
main,
|
|
13
14
|
)
|
|
14
15
|
from .config_io import (
|
|
@@ -46,6 +47,7 @@ __all__ = [
|
|
|
46
47
|
"get_formatter",
|
|
47
48
|
"get_ruff_config",
|
|
48
49
|
"get_ruff_tool_table",
|
|
50
|
+
"inspect",
|
|
49
51
|
"is_ruff_toml_file",
|
|
50
52
|
"load_local_ruff_config",
|
|
51
53
|
"main",
|
ruff_sync/cli.py
CHANGED
|
@@ -42,6 +42,7 @@ from ruff_sync.core import (
|
|
|
42
42
|
pull,
|
|
43
43
|
resolve_raw_url,
|
|
44
44
|
)
|
|
45
|
+
from ruff_sync.dependencies import DependencyError
|
|
45
46
|
|
|
46
47
|
if TYPE_CHECKING:
|
|
47
48
|
from collections.abc import Iterable, Mapping
|
|
@@ -378,6 +379,9 @@ def _resolve_upstream(args: CLIArguments, config: Config) -> tuple[URL, ...]:
|
|
|
378
379
|
f"got {type(config_upstream).__name__}"
|
|
379
380
|
)
|
|
380
381
|
|
|
382
|
+
if args.command == "inspect":
|
|
383
|
+
return ()
|
|
384
|
+
|
|
381
385
|
PARSER.error(
|
|
382
386
|
"❌ the following arguments are required: upstream "
|
|
383
387
|
f"(or define it in [tool.ruff-sync] in {RuffConfigFileName.PYPROJECT_TOML}) 💥"
|
|
@@ -599,25 +603,37 @@ def main() -> int:
|
|
|
599
603
|
|
|
600
604
|
try:
|
|
601
605
|
if exec_args.command == "inspect":
|
|
602
|
-
from ruff_sync.
|
|
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
|
|
606
|
+
from ruff_sync.tui import get_tui_app # noqa: PLC0415
|
|
609
607
|
|
|
610
|
-
|
|
611
|
-
return
|
|
608
|
+
app_class = get_tui_app()
|
|
609
|
+
return app_class(exec_args).run() or 0
|
|
612
610
|
|
|
613
611
|
if exec_args.command == "check":
|
|
614
612
|
return asyncio.run(check(exec_args))
|
|
615
613
|
return asyncio.run(pull(exec_args))
|
|
614
|
+
except DependencyError as e:
|
|
615
|
+
LOGGER.error(f"❌ {e}") # noqa: TRY400
|
|
616
|
+
return 1
|
|
616
617
|
except UpstreamError as e:
|
|
617
618
|
for url, err in e.errors:
|
|
618
619
|
LOGGER.error(f"❌ Failed to fetch {url}: {err}") # noqa: TRY400
|
|
619
620
|
return 4
|
|
620
621
|
|
|
621
622
|
|
|
623
|
+
def inspect() -> int:
|
|
624
|
+
"""Entry point for the ruff-inspect console script."""
|
|
625
|
+
# Handle optional subcommands/args if user passed any to ruff-inspect
|
|
626
|
+
# but primarily ensure "inspect" is the command.
|
|
627
|
+
if len(sys.argv) > 1 and sys.argv[1] not in ("-h", "--help", "--version"):
|
|
628
|
+
# If they passed args but no command, insert 'inspect'
|
|
629
|
+
if sys.argv[1] not in ("pull", "check", "inspect"):
|
|
630
|
+
sys.argv.insert(1, "inspect")
|
|
631
|
+
else:
|
|
632
|
+
# Default to 'inspect' if no args or just flags
|
|
633
|
+
sys.argv.insert(1, "inspect")
|
|
634
|
+
|
|
635
|
+
return main()
|
|
636
|
+
|
|
637
|
+
|
|
622
638
|
if __name__ == "__main__":
|
|
623
639
|
sys.exit(main())
|
ruff_sync/core.py
CHANGED
|
@@ -11,11 +11,10 @@ import re
|
|
|
11
11
|
import subprocess
|
|
12
12
|
import sys
|
|
13
13
|
import tempfile
|
|
14
|
-
from collections.abc import Iterable, Mapping
|
|
14
|
+
from collections.abc import Iterable, Mapping, MutableMapping
|
|
15
15
|
from io import StringIO
|
|
16
16
|
from typing import (
|
|
17
17
|
TYPE_CHECKING,
|
|
18
|
-
Any,
|
|
19
18
|
Final,
|
|
20
19
|
Literal,
|
|
21
20
|
NamedTuple,
|
|
@@ -27,7 +26,7 @@ import httpx
|
|
|
27
26
|
import tomlkit
|
|
28
27
|
from httpx import URL
|
|
29
28
|
from tomlkit import TOMLDocument, table
|
|
30
|
-
from tomlkit.items import Table
|
|
29
|
+
from tomlkit.items import Item, Table
|
|
31
30
|
from tomlkit.toml_file import TOMLFile
|
|
32
31
|
|
|
33
32
|
from ruff_sync.config_io import RuffConfigFileName, is_ruff_toml_file, resolve_target_path
|
|
@@ -41,6 +40,8 @@ from ruff_sync.formatters import ResultFormatter, get_formatter
|
|
|
41
40
|
from ruff_sync.pre_commit import sync_pre_commit
|
|
42
41
|
|
|
43
42
|
if TYPE_CHECKING:
|
|
43
|
+
from tomlkit.container import Container
|
|
44
|
+
|
|
44
45
|
from ruff_sync.cli import Arguments
|
|
45
46
|
|
|
46
47
|
__all__: Final[list[str]] = [
|
|
@@ -103,8 +104,8 @@ class DiffContext(NamedTuple):
|
|
|
103
104
|
source_toml_path: pathlib.Path
|
|
104
105
|
source_doc: TOMLDocument
|
|
105
106
|
merged_doc: TOMLDocument
|
|
106
|
-
source_val:
|
|
107
|
-
merged_val:
|
|
107
|
+
source_val: object
|
|
108
|
+
merged_val: object
|
|
108
109
|
|
|
109
110
|
|
|
110
111
|
class Config(TypedDict, total=False):
|
|
@@ -552,16 +553,19 @@ def _apply_exclusions(tbl: Table | TOMLDocument, exclude: Iterable[str]) -> None
|
|
|
552
553
|
"""
|
|
553
554
|
for key_path in exclude:
|
|
554
555
|
parts = key_path.split(".")
|
|
555
|
-
target:
|
|
556
|
+
target: Table | TOMLDocument | Item | Container | None = tbl
|
|
556
557
|
for part in parts[:-1]:
|
|
557
|
-
|
|
558
|
-
|
|
558
|
+
if isinstance(target, Mapping):
|
|
559
|
+
target = target.get(part)
|
|
560
|
+
else:
|
|
561
|
+
target = None
|
|
559
562
|
break
|
|
560
|
-
if
|
|
563
|
+
if isinstance(target, Mapping):
|
|
561
564
|
leaf = parts[-1]
|
|
562
565
|
if leaf in target:
|
|
563
566
|
LOGGER.info(f"Excluding `{key_path}` from upstream ruff config.")
|
|
564
|
-
target
|
|
567
|
+
if hasattr(target, "pop"):
|
|
568
|
+
target.pop(leaf)
|
|
565
569
|
|
|
566
570
|
|
|
567
571
|
def toml_ruff_parse(toml_s: str, exclude: Iterable[str]) -> TOMLDocument:
|
|
@@ -573,9 +577,11 @@ def toml_ruff_parse(toml_s: str, exclude: Iterable[str]) -> TOMLDocument:
|
|
|
573
577
|
return ruff_toml
|
|
574
578
|
|
|
575
579
|
|
|
576
|
-
def _recursive_update(
|
|
580
|
+
def _recursive_update(
|
|
581
|
+
source_table: MutableMapping[str, object], upstream: Mapping[str, object]
|
|
582
|
+
) -> None:
|
|
577
583
|
"""Recursively update a TOML table, preserving formatting of existing keys."""
|
|
578
|
-
if
|
|
584
|
+
if isinstance(upstream, Mapping) or hasattr(upstream, "items"):
|
|
579
585
|
items = upstream.items()
|
|
580
586
|
else:
|
|
581
587
|
return
|
|
@@ -588,26 +594,32 @@ def _recursive_update(source_table: Any, upstream: Any) -> None:
|
|
|
588
594
|
# Structural fix: if the target is a proxy (dotted key),
|
|
589
595
|
# and we are adding NEW keys to it, we must convert it to a real
|
|
590
596
|
# table to ensure children get correct headers.
|
|
591
|
-
|
|
592
|
-
|
|
597
|
+
source_key_val = source_table[key]
|
|
598
|
+
source_sub_keys = (
|
|
599
|
+
set(source_key_val.keys()) if isinstance(source_key_val, Mapping) else set()
|
|
600
|
+
)
|
|
601
|
+
upstream_sub_keys = set(value.keys()) if isinstance(value, Mapping) else set()
|
|
593
602
|
if not upstream_sub_keys.issubset(source_sub_keys):
|
|
594
|
-
current_val =
|
|
603
|
+
current_val = getattr(
|
|
604
|
+
source_table[key], "unwrap", lambda k=key: source_table[k]
|
|
605
|
+
)()
|
|
595
606
|
# DELETE PROXY FIRST to avoid structural doubling
|
|
596
607
|
del source_table[key]
|
|
597
608
|
# ADD AS REAL TABLE
|
|
598
|
-
source_table
|
|
599
|
-
|
|
600
|
-
|
|
609
|
+
if hasattr(source_table, "add"):
|
|
610
|
+
source_table.add(key, current_val)
|
|
611
|
+
else:
|
|
612
|
+
source_table[key] = current_val
|
|
613
|
+
|
|
614
|
+
child_source = source_table[key]
|
|
615
|
+
if isinstance(child_source, MutableMapping) and isinstance(value, Mapping):
|
|
616
|
+
_recursive_update(child_source, value)
|
|
601
617
|
else:
|
|
602
618
|
# Overwrite existing leaf value only if it's semantically different.
|
|
603
619
|
# Compare unwrapped values, but assign the raw tomlkit Item to
|
|
604
620
|
# preserve inline comments attached to the upstream value.
|
|
605
|
-
current_val = (
|
|
606
|
-
|
|
607
|
-
if hasattr(source_table[key], "unwrap")
|
|
608
|
-
else source_table[key]
|
|
609
|
-
)
|
|
610
|
-
new_val_unwrapped = value.unwrap() if hasattr(value, "unwrap") else value
|
|
621
|
+
current_val = getattr(source_table[key], "unwrap", lambda k=key: source_table[k])()
|
|
622
|
+
new_val_unwrapped = getattr(value, "unwrap", lambda v=value: v)()
|
|
611
623
|
if current_val != new_val_unwrapped:
|
|
612
624
|
source_table[key] = value
|
|
613
625
|
else:
|
|
@@ -813,8 +825,8 @@ def _check_pre_commit_sync(args: Arguments, fmt: ResultFormatter) -> int | None:
|
|
|
813
825
|
|
|
814
826
|
|
|
815
827
|
def _find_changed_keys(
|
|
816
|
-
source:
|
|
817
|
-
merged:
|
|
828
|
+
source: object,
|
|
829
|
+
merged: object,
|
|
818
830
|
prefix: str = "",
|
|
819
831
|
) -> list[str]:
|
|
820
832
|
"""Return a list of dotted TOML keys that differ between *source* and *merged*.
|
|
@@ -834,12 +846,9 @@ def _find_changed_keys(
|
|
|
834
846
|
"""
|
|
835
847
|
changed: list[str] = []
|
|
836
848
|
|
|
837
|
-
|
|
838
|
-
merged_is_mapping = isinstance(merged, Mapping)
|
|
839
|
-
|
|
840
|
-
if source_is_mapping and merged_is_mapping:
|
|
849
|
+
if isinstance(source, Mapping) and isinstance(merged, Mapping):
|
|
841
850
|
for key, merged_val in merged.items():
|
|
842
|
-
full_key = f"{prefix}.{key}" if prefix else key
|
|
851
|
+
full_key = f"{prefix}.{key}" if prefix else str(key)
|
|
843
852
|
source_val = source.get(key)
|
|
844
853
|
if source_val is None and key not in source:
|
|
845
854
|
# Key is new (only in merged)
|
|
@@ -859,7 +868,7 @@ def _find_changed_keys(
|
|
|
859
868
|
changed.extend(nested)
|
|
860
869
|
elif src_unwrapped != mrg_unwrapped:
|
|
861
870
|
changed.append(full_key)
|
|
862
|
-
elif
|
|
871
|
+
elif isinstance(source, Mapping) != isinstance(merged, Mapping):
|
|
863
872
|
# Structural type mismatch (table vs scalar or vice-versa): treat the
|
|
864
873
|
# whole node as changed rather than attempting a meaningless value
|
|
865
874
|
# comparison between incompatible TOML node types.
|
|
@@ -889,8 +898,8 @@ def _report_drift(
|
|
|
889
898
|
generic error.
|
|
890
899
|
"""
|
|
891
900
|
if is_ruff_toml:
|
|
892
|
-
source_section:
|
|
893
|
-
merged_section:
|
|
901
|
+
source_section: object = source_doc
|
|
902
|
+
merged_section: object = merged_doc
|
|
894
903
|
else:
|
|
895
904
|
source_section = source_doc.get("tool", {}).get("ruff") or {}
|
|
896
905
|
merged_section = merged_doc.get("tool", {}).get("ruff") or {}
|
|
@@ -966,8 +975,8 @@ async def check(
|
|
|
966
975
|
)
|
|
967
976
|
|
|
968
977
|
is_source_ruff_toml = is_ruff_toml_file(_source_toml_path.name)
|
|
969
|
-
source_val:
|
|
970
|
-
merged_val:
|
|
978
|
+
source_val: object = None
|
|
979
|
+
merged_val: object = None
|
|
971
980
|
if args.semantic:
|
|
972
981
|
if is_source_ruff_toml:
|
|
973
982
|
source_ruff = source_doc
|
ruff_sync/dependencies.py
CHANGED
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import importlib
|
|
5
6
|
import importlib.util
|
|
6
|
-
from typing import Final
|
|
7
|
+
from typing import TYPE_CHECKING, Final
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
__all__: Final[list[str]] = ["DependencyError", "is_installed", "require_dependency"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DependencyError(Exception):
|
|
17
|
+
"""Raised when a required optional dependency is missing or broken."""
|
|
9
18
|
|
|
10
19
|
|
|
11
20
|
def is_installed(package_name: str) -> bool:
|
|
@@ -20,19 +29,36 @@ def is_installed(package_name: str) -> bool:
|
|
|
20
29
|
return importlib.util.find_spec(package_name) is not None
|
|
21
30
|
|
|
22
31
|
|
|
23
|
-
def require_dependency(
|
|
24
|
-
|
|
32
|
+
def require_dependency(
|
|
33
|
+
package_name: str,
|
|
34
|
+
extra_name: str,
|
|
35
|
+
*,
|
|
36
|
+
_is_installed: Callable[[str], bool] = is_installed,
|
|
37
|
+
_import_module: Callable[[str], Any] = importlib.import_module,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Ensure a dependency is installed and importable, or raise DependencyError.
|
|
25
40
|
|
|
26
41
|
Args:
|
|
27
42
|
package_name: The name of the required package.
|
|
28
43
|
extra_name: The name of the ruff-sync extra that provides this package.
|
|
44
|
+
_is_installed: Internal use only for testing. Function to check if a package is installed.
|
|
45
|
+
_import_module: Internal use only for testing. Function to import a module.
|
|
29
46
|
|
|
30
47
|
Raises:
|
|
31
|
-
|
|
48
|
+
DependencyError: If the package is not installed or raises an error during import.
|
|
32
49
|
"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
msg = (
|
|
51
|
+
f"The '{package_name}' package is required for this feature. "
|
|
52
|
+
f"Install it with: pip install 'ruff-sync[{extra_name}]'"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# 1. Fast check (dry) to see if it exists at all
|
|
56
|
+
if not _is_installed(package_name):
|
|
57
|
+
raise DependencyError(msg)
|
|
58
|
+
|
|
59
|
+
# 2. Wet check (real import) to ensure it's functional
|
|
60
|
+
try:
|
|
61
|
+
_import_module(package_name)
|
|
62
|
+
except (ImportError, ModuleNotFoundError) as e:
|
|
63
|
+
# If it failed here, it's installed but BROKEN (e.g. missing sub-dependencies)
|
|
64
|
+
raise DependencyError(msg) from e
|
ruff_sync/system.py
CHANGED
|
@@ -3,8 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import json
|
|
6
7
|
import logging
|
|
7
|
-
from typing import Final
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
|
|
13
|
+
from ruff_sync.types_ import RuffLinter, RuffRule
|
|
8
14
|
|
|
9
15
|
LOGGER = logging.getLogger(__name__)
|
|
10
16
|
|
|
@@ -20,6 +26,132 @@ async def get_ruff_rule_markdown(rule_code: str) -> str | None:
|
|
|
20
26
|
or the rule is not found.
|
|
21
27
|
"""
|
|
22
28
|
cmd: Final[list[str]] = ["ruff", "rule", rule_code]
|
|
29
|
+
return await _run_ruff_command(cmd, f"ruff rule {rule_code}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def get_ruff_config_markdown(setting_path: str) -> str | None:
|
|
33
|
+
"""Execute `ruff config <SETTING>` and return the Markdown documentation.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
setting_path: The Ruff configuration setting path (e.g., 'lint.select').
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The Markdown documentation for the setting, or None if the execution fails.
|
|
40
|
+
"""
|
|
41
|
+
# Strip 'tool.ruff.' prefix if present as 'ruff config' expects relative paths
|
|
42
|
+
clean_path = setting_path.removeprefix("tool.ruff.")
|
|
43
|
+
if not clean_path or clean_path == "tool.ruff":
|
|
44
|
+
return None
|
|
45
|
+
cmd: Final[list[str]] = ["ruff", "config", clean_path]
|
|
46
|
+
return await _run_ruff_command(cmd, f"ruff config {clean_path}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def get_all_ruff_rules() -> list[RuffRule]:
|
|
50
|
+
"""Execute `ruff rule --all --output-format json` and return the parsed rules.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A list of dictionaries representing all supported Ruff rules.
|
|
54
|
+
"""
|
|
55
|
+
cmd: Final[list[str]] = ["ruff", "rule", "--all", "--output-format", "json"]
|
|
56
|
+
output = await _run_ruff_command(cmd, "ruff rule --all")
|
|
57
|
+
if not output:
|
|
58
|
+
return []
|
|
59
|
+
try:
|
|
60
|
+
data = json.loads(output)
|
|
61
|
+
except json.JSONDecodeError:
|
|
62
|
+
LOGGER.exception("Failed to parse Ruff rules JSON.")
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
if isinstance(data, list):
|
|
66
|
+
return data
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def get_ruff_linters() -> list[RuffLinter]:
|
|
71
|
+
"""Execute `ruff linter --output-format json` and return the parsed linters.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
A list of dictionaries representing Ruff linter categories.
|
|
75
|
+
"""
|
|
76
|
+
cmd: Final[list[str]] = ["ruff", "linter", "--output-format", "json"]
|
|
77
|
+
output = await _run_ruff_command(cmd, "ruff linter")
|
|
78
|
+
if not output:
|
|
79
|
+
return []
|
|
80
|
+
try:
|
|
81
|
+
data = json.loads(output)
|
|
82
|
+
except json.JSONDecodeError:
|
|
83
|
+
LOGGER.exception("Failed to parse Ruff linters JSON.")
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
if isinstance(data, list):
|
|
87
|
+
return data
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def compute_effective_rules(
|
|
92
|
+
all_rules: list[RuffRule], toml_config: Mapping[str, Any]
|
|
93
|
+
) -> list[RuffRule]:
|
|
94
|
+
"""Determine the status (Enabled, Ignored, Disabled) for each rule.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
all_rules: The list of all supported rules.
|
|
98
|
+
toml_config: The local configuration dictionary.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The list of rules enriched with a 'status' key.
|
|
102
|
+
"""
|
|
103
|
+
# The config may be "wrapped" (top-level 'tool' key) or
|
|
104
|
+
# "unwrapped" (direct Ruff config as returned by load_local_ruff_config).
|
|
105
|
+
lint = toml_config.get("lint")
|
|
106
|
+
if lint is None:
|
|
107
|
+
lint = toml_config.get("tool", {}).get("ruff", {}).get("lint", {})
|
|
108
|
+
|
|
109
|
+
select = set(lint.get("select", [])) | set(lint.get("extend-select", []))
|
|
110
|
+
ignore = set(lint.get("ignore", [])) | set(lint.get("extend-ignore", []))
|
|
111
|
+
|
|
112
|
+
# If no select/extend-select is provided, Ruff defaults to E and F
|
|
113
|
+
if not lint.get("select") and not lint.get("extend-select"):
|
|
114
|
+
select.update(["E", "F"])
|
|
115
|
+
|
|
116
|
+
enriched: list[RuffRule] = []
|
|
117
|
+
for rule in all_rules:
|
|
118
|
+
code = rule["code"]
|
|
119
|
+
|
|
120
|
+
# Find longest matching select prefix
|
|
121
|
+
best_select_len = -1
|
|
122
|
+
for s in select:
|
|
123
|
+
if code.startswith(s):
|
|
124
|
+
best_select_len = max(best_select_len, len(s))
|
|
125
|
+
|
|
126
|
+
# Find longest matching ignore prefix
|
|
127
|
+
best_ignore_len = -1
|
|
128
|
+
for i in ignore:
|
|
129
|
+
if code.startswith(i):
|
|
130
|
+
best_ignore_len = max(best_ignore_len, len(i))
|
|
131
|
+
|
|
132
|
+
status = "Disabled"
|
|
133
|
+
if best_select_len > best_ignore_len:
|
|
134
|
+
status = "Enabled"
|
|
135
|
+
elif best_ignore_len >= best_select_len and best_ignore_len != -1:
|
|
136
|
+
status = "Ignored"
|
|
137
|
+
|
|
138
|
+
rule_with_status = dict(rule)
|
|
139
|
+
rule_with_status["status"] = status
|
|
140
|
+
enriched.append(rule_with_status) # type: ignore[arg-type]
|
|
141
|
+
|
|
142
|
+
return enriched
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def _run_ruff_command(cmd: list[str], description: str) -> str | None:
|
|
146
|
+
"""Execute a ruff command and return the decoded output.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
cmd: The command to execute.
|
|
150
|
+
description: A human-readable description for logging.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The decoded stdout, or None if the command fails.
|
|
154
|
+
"""
|
|
23
155
|
LOGGER.debug(f"Executing system command: {' '.join(cmd)}")
|
|
24
156
|
|
|
25
157
|
try:
|
|
@@ -32,14 +164,15 @@ async def get_ruff_rule_markdown(rule_code: str) -> str | None:
|
|
|
32
164
|
|
|
33
165
|
if process.returncode != 0:
|
|
34
166
|
msg = stderr.decode().strip()
|
|
35
|
-
LOGGER.warning(f"
|
|
167
|
+
LOGGER.warning(f"Command '{description}' failed with code {process.returncode}: {msg}")
|
|
36
168
|
return None
|
|
37
169
|
|
|
38
|
-
|
|
39
|
-
|
|
170
|
+
output = stdout.decode().strip()
|
|
40
171
|
except FileNotFoundError:
|
|
41
172
|
LOGGER.exception("Ruff executable not found in PATH.")
|
|
42
173
|
return None
|
|
43
174
|
except Exception:
|
|
44
|
-
LOGGER.exception(f"Unexpected error executing
|
|
175
|
+
LOGGER.exception(f"Unexpected error executing '{description}'")
|
|
45
176
|
return None
|
|
177
|
+
else:
|
|
178
|
+
return output or None
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""TUI package for ruff-sync."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ruff_sync.tui.app import RuffSyncApp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_tui_app() -> type[RuffSyncApp]:
|
|
12
|
+
"""Lazy-load the TUI and return the RuffSyncApp class.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
The RuffSyncApp class.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
DependencyError: If 'textual' is not installed or the TUI cannot be loaded.
|
|
19
|
+
"""
|
|
20
|
+
from ruff_sync.dependencies import require_dependency # noqa: PLC0415
|
|
21
|
+
|
|
22
|
+
require_dependency("textual", extra_name="tui")
|
|
23
|
+
|
|
24
|
+
from ruff_sync.tui.app import RuffSyncApp # noqa: PLC0415
|
|
25
|
+
|
|
26
|
+
return RuffSyncApp
|