diff --git a/README.md b/README.md index a07d77c3..065507c5 100644 --- a/README.md +++ b/README.md @@ -416,7 +416,6 @@ Will not show up if no `.readthedocs.yml`/`.readthedocs.yaml` file is present. - [`RF002`](https://learn.scientific-python.org/development/guides/style#RF002): Target version must be set - [`RF003`](https://learn.scientific-python.org/development/guides/style#RF003): src directory doesn't need to be specified anymore (0.6+) - [`RF101`](https://learn.scientific-python.org/development/guides/style#RF101): Bugbear must be selected -- [`RF102`](https://learn.scientific-python.org/development/guides/style#RF102): isort must be selected - [`RF103`](https://learn.scientific-python.org/development/guides/style#RF103): pyupgrade must be selected - `RF201`: Avoid using deprecated config settings - `RF202`: Use (new) lint config section diff --git a/docs/guides/style.md b/docs/guides/style.md index 5580f062..61722473 100644 --- a/docs/guides/style.md +++ b/docs/guides/style.md @@ -276,7 +276,6 @@ won't tell you what or why it fixed things. [tool.ruff.lint] extend-select = [ "B", # flake8-bugbear - "I", # isort "RUF", # Ruff-specific "UP", # pyupgrade ] @@ -290,16 +289,11 @@ extend-select = [ extend-select = [ "ARG", # flake8-unused-arguments "B", # flake8-bugbear - "BLE", # flake8-blind-except "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez "EM", # flake8-errmsg "EXE", # flake8-executable - "FA", # flake8-future-annotations - "FLY", # flynt "FURB", # refurb "G", # flake8-logging-format - "I", # isort "ICN", # flake8-import-conventions "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging @@ -307,7 +301,6 @@ extend-select = [ "PD", # pandas-vet "PERF", # perflint "PGH", # pygrep-hooks - "PIE", # flake8-pie "PL", # pylint "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib @@ -323,7 +316,6 @@ extend-select = [ "TC", # flake8-type-checking "TRY", # tryceratops "UP", # pyupgrade - "YTT", # flake8-2020 ] ignore = [ "PLR09", # Too many <...> @@ -407,11 +399,10 @@ Here are some good error codes to enable on most (but not all!) projects: stood the test of time. Not required if you use `extend-select` (`W` not needed if you use a formatter) - `B`: This finds patterns that are very bug-prone. {rr}`RF101` -- `I`: This sorts your includes. There are multiple benefits, such as smaller - diffs, fewer conflicts, a way to auto-inject `__future__` imports, and easier - for readers to tell what's built-in, third-party, and local. It has a lot of - configuration options, but defaults to a Black-compatible style. - {rr}`RF102` +- `I`: This sorts your includes (on by default since Ruff 0.16). Select this + group only if you also want `I002`, which can auto-inject imports, such as + `from __future__ import annotations`; it needs the + `lint.isort.required-imports` setting. - `ARG`: This looks for unused arguments. You might need to `# noqa: ARG001` occasionally, but it's overall pretty useful. - `C4`: This looks for places that could use comprehensions, and can autofix diff --git a/src/sp_repo_review/checks/ruff.py b/src/sp_repo_review/checks/ruff.py index 69eb71ec..af3c1347 100644 --- a/src/sp_repo_review/checks/ruff.py +++ b/src/sp_repo_review/checks/ruff.py @@ -157,13 +157,6 @@ class RF101(RF1xx): name = "flake8-bugbear" -class RF102(RF1xx): - "isort must be selected" - - code = "I" - name = "isort" - - class RF103(RF1xx): "pyupgrade must be selected" diff --git a/src/sp_repo_review/ruff_checks/__main__.py b/src/sp_repo_review/ruff_checks/__main__.py index c883f11b..3b294162 100644 --- a/src/sp_repo_review/ruff_checks/__main__.py +++ b/src/sp_repo_review/ruff_checks/__main__.py @@ -36,6 +36,8 @@ select_info = json.load(f) LIBS = frozenset(select_info["libs"]) SPECIALTY = frozenset(r["name"] for r in select_info["specialty"]) +# Selected by Ruff without configuration (0.16+) +DEFAULTS = {r["name"]: r["reason"] for r in select_info["defaults"]} with RESOURCE_DIR.joinpath("ignore.json").open(encoding="utf-8") as f: IGNORE_INFO = json.load(f) @@ -105,6 +107,7 @@ def _print_output_rich( libs_items: dict[str, str], spec_items: dict[str, str], unselected_items: dict[str, str], + default_items: dict[str, str], ) -> None: """Print rich formatted output.""" import rich.columns @@ -128,6 +131,10 @@ def _print_output_rich( uns = "\n".join(_print_each_rich(unselected_items)) rich.print(rich.columns.Columns([panel_sel, panel_lib, panel_spec])) + if default_items: + rich.print("[yellow]Selected, but on by default [dim](safe to remove)") + for item in _print_each_rich(default_items): + rich.print(item) if uns: rich.print("[red]Unselected [dim](copy and paste ready)") rich.print(uns) @@ -138,12 +145,18 @@ def _print_output_plain( libs_items: dict[str, str], spec_items: dict[str, str], unselected_items: dict[str, str], + default_items: dict[str, str], ) -> None: """Print plain formatted output.""" print("Selected:") for item in _print_each_plain(selected_items): print(item) + if default_items: + print("\nSelected, but on by default (safe to remove):") + for item in _print_each_plain(default_items): + print(item) + if libs_items: print("\nLibrary specific:") for item in _print_each_plain(libs_items): @@ -236,8 +249,15 @@ def process_dir(path: Path, format: str = "auto") -> None: _handle_all_selected(fmt, ruff_config) return - selected_items = {k: v for k, v in LINT_INFO.items() if k in selected} - all_uns_items = {k: v for k, v in LINT_INFO.items() if k not in selected} + default_items = { + k: DEFAULTS[k] for k in LINT_INFO if k in selected & DEFAULTS.keys() + } + selected_items = { + k: v for k, v in LINT_INFO.items() if k in selected and k not in DEFAULTS + } + all_uns_items = { + k: v for k, v in LINT_INFO.items() if k not in selected and k not in DEFAULTS + } unselected_items = { k: v for k, v in all_uns_items.items() if k not in LIBS | SPECIALTY } @@ -245,9 +265,13 @@ def process_dir(path: Path, format: str = "auto") -> None: spec_items = {k: v for k, v in all_uns_items.items() if k in SPECIALTY} if fmt == "rich": - _print_output_rich(selected_items, libs_items, spec_items, unselected_items) + _print_output_rich( + selected_items, libs_items, spec_items, unselected_items, default_items + ) else: - _print_output_plain(selected_items, libs_items, spec_items, unselected_items) + _print_output_plain( + selected_items, libs_items, spec_items, unselected_items, default_items + ) def main() -> None: diff --git a/src/sp_repo_review/ruff_checks/select.json b/src/sp_repo_review/ruff_checks/select.json index c747bed5..1fd69a8f 100644 --- a/src/sp_repo_review/ruff_checks/select.json +++ b/src/sp_repo_review/ruff_checks/select.json @@ -1,5 +1,15 @@ { - "libs": ["AIR", "ASYNC", "DJ", "FAST", "INT", "NPY", "PD"], + "libs": ["AIR", "ASYNC", "DJ", "FAST", "NPY", "PD"], + "defaults": [ + { "name": "BLE", "reason": "on by default" }, + { "name": "DTZ", "reason": "on by default" }, + { "name": "FA", "reason": "on by default" }, + { "name": "FLY", "reason": "on by default" }, + { "name": "I", "reason": "on by default, except I002, which needs config" }, + { "name": "INT", "reason": "on by default" }, + { "name": "PIE", "reason": "on by default" }, + { "name": "YTT", "reason": "on by default" } + ], "specialty": [ { "name": "CPY", "reason": "preview only" }, { "name": "A", "reason": "Naming related" }, diff --git a/tests/test_ruff.py b/tests/test_ruff.py index 16c3191b..89bdcd56 100644 --- a/tests/test_ruff.py +++ b/tests/test_ruff.py @@ -68,15 +68,7 @@ def test_rf101_selected(): def test_rf101_missing(): - assert not compute_check("RF101", ruff={"lint": {"select": ["I"]}}).result - - -def test_rf102_selected(): - assert compute_check("RF102", ruff={"lint": {"select": ["I"]}}).result - - -def test_rf102_missing(): - assert not compute_check("RF102", ruff={"lint": {"select": ["UP"]}}).result + assert not compute_check("RF101", ruff={"lint": {"select": ["UP"]}}).result def test_rf103_selected(): diff --git a/tests/test_ruff_checks_cli.py b/tests/test_ruff_checks_cli.py index fec23e9a..a2acd5c2 100644 --- a/tests/test_ruff_checks_cli.py +++ b/tests/test_ruff_checks_cli.py @@ -40,3 +40,22 @@ def test_plain_format_has_quotes_and_comma(monkeypatch, tmp_path, capsys): ruff_checks.process_dir(tmp_path, format="plain") captured = capsys.readouterr() assert '"A",' in captured.out + + +def test_plain_format_default_rules(monkeypatch, tmp_path, capsys): + """Rules Ruff selects itself are reported as removable, not as missing.""" + monkeypatch.setattr(ruff_checks, "ruff", lambda *_a, **_k: {"tool": "ruff"}) + monkeypatch.setattr(ruff_checks, "get_rule_selection", lambda *_a, **_k: {"A", "B"}) + monkeypatch.setattr(ruff_checks, "LINT_INFO", {"A": "Rule A", "B": "Rule B"}) + monkeypatch.setattr(ruff_checks, "LIBS", frozenset()) + monkeypatch.setattr(ruff_checks, "SPECIALTY", frozenset()) + monkeypatch.setattr( + ruff_checks, "DEFAULTS", {"A": "on by default", "C": "likewise"} + ) + + ruff_checks.process_dir(tmp_path, format="plain") + captured = capsys.readouterr() + selected, defaults = captured.out.split("Selected, but on by default") + assert '"B",' in selected + assert '"A",' in defaults + assert '"C",' not in captured.out diff --git a/{{cookiecutter.project_name}}/pyproject.toml b/{{cookiecutter.project_name}}/pyproject.toml index 7be19002..3a1013e8 100644 --- a/{{cookiecutter.project_name}}/pyproject.toml +++ b/{{cookiecutter.project_name}}/pyproject.toml @@ -288,16 +288,11 @@ show-fixes = true extend-select = [ "ARG", # flake8-unused-arguments "B", # flake8-bugbear - "BLE", # flake8-blind-except "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez "EM", # flake8-errmsg "EXE", # flake8-executable - "FA", # flake8-future-annotations - "FLY", # flynt "FURB", # refurb "G", # flake8-logging-format - "I", # isort "ICN", # flake8-import-conventions "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging @@ -305,7 +300,6 @@ extend-select = [ "PD", # pandas-vet "PERF", # perflint "PGH", # pygrep-hooks - "PIE", # flake8-pie "PL", # pylint "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib @@ -321,7 +315,6 @@ extend-select = [ "TC", # flake8-type-checking "TRY", # tryceratops "UP", # pyupgrade - "YTT", # flake8-2020 ] ignore = [ "PLR09", # Too many <...>