Skip to content

Commit 4927e0a

Browse files
881 usethis tool import linter doesnt give message for ignoring inp (#885)
* Clarify behaviour for duplicates in `KeyValueFileManager.extend_list` docstring * Give a message regarding the addition of INP per-file ignores when adding Import Linter. Also change the rule management methods on `Tool` to return `bool` for whether rules were added. * Reword comment for clarity and conciseness
1 parent b139e3a commit 4927e0a

5 files changed

Lines changed: 137 additions & 26 deletions

File tree

src/usethis/_io.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,10 @@ def set_value(
203203

204204
@abstractmethod
205205
def extend_list(self, *, keys: Sequence[Key], values: list[Any]) -> None:
206-
"""Extend a list in the configuration file."""
206+
"""Extend a list in the configuration file.
207+
208+
This method will always extend the list, even if it results in duplicates.
209+
"""
207210
raise NotImplementedError
208211

209212
@abstractmethod

src/usethis/_tool/base.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -567,18 +567,26 @@ def is_managed_rule(self, rule: Rule) -> bool:
567567
"""Determine if a rule is managed by this tool."""
568568
return False
569569

570-
def select_rules(self, rules: list[Rule]) -> None:
570+
def select_rules(self, rules: list[Rule]) -> bool:
571571
"""Select the rules managed by the tool.
572572
573573
These rules are not validated; it is assumed they are valid rules for the tool,
574574
and that the tool will be able to manage them.
575+
576+
Args:
577+
rules: The rules to select. If any of these rules are already selected, they
578+
will be skipped.
579+
580+
Returns:
581+
True if any rules were selected, False if no rules were selected.
575582
"""
583+
return False
576584

577585
def get_selected_rules(self) -> list[Rule]:
578586
"""Get the rules managed by the tool that are currently selected."""
579587
return []
580588

581-
def ignore_rules(self, rules: list[Rule]) -> None:
589+
def ignore_rules(self, rules: list[Rule]) -> bool:
582590
"""Ignore rules managed by the tool.
583591
584592
Ignoring a rule is different from deselecting it - it means that even if it
@@ -587,21 +595,45 @@ def ignore_rules(self, rules: list[Rule]) -> None:
587595
588596
These rules are not validated; it is assumed they are valid rules for the tool,
589597
and that the tool will be able to manage them.
598+
599+
Args:
600+
rules: The rules to ignore. If any of these rules are already ignored, they
601+
will be skipped.
602+
603+
Returns:
604+
True if any rules were ignored, False if no rules were ignored.
590605
"""
606+
return False
591607

592-
def unignore_rules(self, rules: list[str]) -> None:
608+
def unignore_rules(self, rules: list[str]) -> bool:
593609
"""Stop ignoring rules managed by the tool.
594610
595611
These rules are not validated; it is assumed they are valid rules for the tool,
596612
and that the tool will be able to manage them.
613+
614+
Args:
615+
rules: The rules to unignore. If any of these rules are not ignored, they
616+
will be skipped.
617+
618+
Returns:
619+
True if any rules were unignored, False if no rules were unignored.
597620
"""
621+
return False
598622

599623
def get_ignored_rules(self) -> list[Rule]:
600624
"""Get the ignored rules managed by the tool."""
601625
return []
602626

603-
def deselect_rules(self, rules: list[Rule]) -> None:
627+
def deselect_rules(self, rules: list[Rule]) -> bool:
604628
"""Deselect the rules managed by the tool.
605629
606630
Any rules that aren't already selected are ignored.
631+
632+
Args:
633+
rules: The rules to deselect. If any of these rules are not selected, they
634+
will be skipped.
635+
636+
Returns:
637+
True if any rules were deselected, False if no rules were deselected.
607638
"""
639+
return False

src/usethis/_tool/impl/deptry.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,11 @@ def get_bitbucket_steps(self) -> list[BitbucketStep]:
110110
def is_managed_rule(self, rule: Rule) -> bool:
111111
return rule.startswith("DEP") and rule[3:].isdigit()
112112

113-
def select_rules(self, rules: list[Rule]) -> None:
113+
def select_rules(self, rules: list[Rule]) -> bool:
114114
"""Does nothing for deptry - all rules are automatically enabled by default."""
115115
if rules:
116116
info_print(f"All {self.name} rules are always implicitly selected.")
117+
return False
117118

118119
def get_selected_rules(self) -> list[Rule]:
119120
"""No notion of selection for deptry.
@@ -123,14 +124,15 @@ def get_selected_rules(self) -> list[Rule]:
123124
"""
124125
return []
125126

126-
def deselect_rules(self, rules: list[Rule]) -> None:
127+
def deselect_rules(self, rules: list[Rule]) -> bool:
127128
"""Does nothing for deptry - all rules are automatically enabled by default."""
129+
return False
128130

129-
def ignore_rules(self, rules: list[Rule]) -> None:
131+
def ignore_rules(self, rules: list[Rule]) -> bool:
130132
rules = sorted(set(rules) - set(self.get_ignored_rules()))
131133

132134
if not rules:
133-
return
135+
return False
134136

135137
rules_str = ", ".join([f"'{rule}'" for rule in rules])
136138
s = "" if len(rules) == 1 else "s"
@@ -143,11 +145,13 @@ def ignore_rules(self, rules: list[Rule]) -> None:
143145
keys = self._get_ignore_keys(file_manager)
144146
file_manager.extend_list(keys=keys, values=rules)
145147

146-
def unignore_rules(self, rules: list[str]) -> None:
148+
return True
149+
150+
def unignore_rules(self, rules: list[str]) -> bool:
147151
rules = sorted(set(rules) & set(self.get_ignored_rules()))
148152

149153
if not rules:
150-
return
154+
return False
151155

152156
rules_str = ", ".join([f"'{rule}'" for rule in rules])
153157
s = "" if len(rules) == 1 else "s"
@@ -160,6 +164,8 @@ def unignore_rules(self, rules: list[str]) -> None:
160164
keys = self._get_ignore_keys(file_manager)
161165
file_manager.remove_from_list(keys=keys, values=rules)
162166

167+
return True
168+
163169
def get_ignored_rules(self) -> list[Rule]:
164170
(file_manager,) = self.get_active_config_file_managers()
165171
keys = self._get_ignore_keys(file_manager)

src/usethis/_tool/impl/ruff.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from typing_extensions import assert_never
77

8+
from usethis._config import usethis_config
89
from usethis._config_file import DotRuffTOMLManager, RuffTOMLManager
910
from usethis._console import box_print, tick_print
1011
from usethis._integrations.ci.bitbucket.anchor import (
@@ -297,12 +298,12 @@ def get_bitbucket_steps(self) -> list[BitbucketStep]:
297298

298299
return steps
299300

300-
def select_rules(self, rules: list[Rule]) -> None:
301+
def select_rules(self, rules: list[Rule]) -> bool:
301302
"""Add Ruff rules to the project."""
302303
rules = sorted(set(rules) - set(self.get_selected_rules()))
303304

304305
if not rules:
305-
return
306+
return False
306307

307308
rules_str = ", ".join([f"'{rule}'" for rule in rules])
308309
s = "" if len(rules) == 1 else "s"
@@ -315,12 +316,14 @@ def select_rules(self, rules: list[Rule]) -> None:
315316
keys = self._get_select_keys(file_manager)
316317
file_manager.extend_list(keys=keys, values=rules)
317318

318-
def ignore_rules(self, rules: list[Rule]) -> None:
319+
return True
320+
321+
def ignore_rules(self, rules: list[Rule]) -> bool:
319322
"""Ignore Ruff rules in the project."""
320323
rules = sorted(set(rules) - set(self.get_ignored_rules()))
321324

322325
if not rules:
323-
return
326+
return False
324327

325328
rules_str = ", ".join([f"'{rule}'" for rule in rules])
326329
s = "" if len(rules) == 1 else "s"
@@ -333,12 +336,14 @@ def ignore_rules(self, rules: list[Rule]) -> None:
333336
keys = self._get_ignore_keys(file_manager)
334337
file_manager.extend_list(keys=keys, values=rules)
335338

336-
def unignore_rules(self, rules: list[str]) -> None:
339+
return True
340+
341+
def unignore_rules(self, rules: list[str]) -> bool:
337342
"""Unignore Ruff rules in the project."""
338343
rules = list(set(rules) & set(self.get_ignored_rules()))
339344

340345
if not rules:
341-
return
346+
return False
342347

343348
rules_str = ", ".join([f"'{rule}'" for rule in rules])
344349
s = "" if len(rules) == 1 else "s"
@@ -350,13 +355,14 @@ def unignore_rules(self, rules: list[str]) -> None:
350355
)
351356
keys = self._get_ignore_keys(file_manager)
352357
file_manager.remove_from_list(keys=keys, values=rules)
358+
return True
353359

354-
def deselect_rules(self, rules: list[Rule]) -> None:
360+
def deselect_rules(self, rules: list[Rule]) -> bool:
355361
"""Ensure Ruff rules are not selected in the project."""
356362
rules = list(set(rules) & set(self.get_selected_rules()))
357363

358364
if not rules:
359-
return
365+
return False
360366

361367
rules_str = ", ".join([f"'{rule}'" for rule in rules])
362368
s = "" if len(rules) == 1 else "s"
@@ -368,6 +374,7 @@ def deselect_rules(self, rules: list[Rule]) -> None:
368374
)
369375
keys = self._get_select_keys(file_manager)
370376
file_manager.remove_from_list(keys=keys, values=rules)
377+
return True
371378

372379
def get_selected_rules(self) -> list[Rule]:
373380
"""Get the Ruff rules selected in the project."""
@@ -397,8 +404,14 @@ def ignore_rules_in_glob(self, rules: list[Rule], *, glob: str) -> None:
397404
if not rules:
398405
return
399406

407+
rules_str = ", ".join([f"'{rule}'" for rule in rules])
408+
s = "" if len(rules) == 1 else "s"
409+
400410
(file_manager,) = self.get_active_config_file_managers()
401411
ensure_managed_file_exists(file_manager)
412+
tick_print(
413+
f"Ignoring {self.name} rule{s} {rules_str} for '{glob}' in '{file_manager.name}'."
414+
)
402415
keys = self._get_per_file_ignore_keys(file_manager, glob=glob)
403416
file_manager.extend_list(keys=keys, values=rules)
404417

@@ -407,9 +420,19 @@ def apply_rule_config(self, rule_config: RuleConfig) -> None:
407420
408421
Note, this will add both managed and unmanaged config.
409422
"""
410-
self.select_rules(rule_config.get_all_selected())
411-
self.ignore_rules(rule_config.get_all_ignored())
412-
self.ignore_rules_in_glob(rule_config.tests_unmanaged_ignored, glob="tests/**")
423+
is_selected = self.select_rules(rule_config.get_all_selected())
424+
is_ignored = self.ignore_rules(rule_config.get_all_ignored())
425+
426+
# We don't want to spam the user with verbose messages about per-file ignores.
427+
# On the other hand, if we haven't displayed any messages at all, we need to
428+
# avoid a misleading silence, which would imply we haven't modified a file.
429+
# This is probably a workaround until there is more sophisticated support for
430+
# verbosity control.
431+
# https://github.com/usethis-python/usethis-python/issues/884
432+
with usethis_config.set(alert_only=is_selected or is_ignored):
433+
self.ignore_rules_in_glob(
434+
rule_config.tests_unmanaged_ignored, glob="tests/**"
435+
)
413436

414437
def remove_rule_config(self, rule_config: RuleConfig) -> None:
415438
"""Remove the Ruff rules associated with a rule config from the project.

tests/usethis/_core/test_core_tool.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,16 +1352,54 @@ def test_inp_rules_selected(self, tmp_path: Path):
13521352
assert "INP" in RuffTool().get_selected_rules()
13531353

13541354
@pytest.mark.usefixtures("_vary_network_conn")
1355-
def test_inp_rules_not_selected_for_tests_dir(self, tmp_path: Path):
1355+
def test_inp_rules_not_selected_for_tests_dir(
1356+
self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]
1357+
):
13561358
# Arrange
1357-
(tmp_path / "ruff.toml").touch()
1359+
(uv_init_dir / "ruff.toml").touch()
13581360

1359-
with change_cwd(tmp_path), files_manager():
1361+
with change_cwd(uv_init_dir), files_manager():
13601362
# Act
13611363
use_import_linter()
13621364

13631365
# Assert
1364-
contents = (tmp_path / "ruff.toml").read_text()
1366+
contents = (uv_init_dir / "ruff.toml").read_text()
1367+
assert (
1368+
contents
1369+
== """\
1370+
[lint]
1371+
select = ["INP"]
1372+
1373+
[lint.per-file-ignores]
1374+
"tests/**" = ["INP"]
1375+
"""
1376+
)
1377+
out, err = capfd.readouterr()
1378+
assert not err
1379+
assert out == (
1380+
"✔ Adding dependency 'import-linter' to the 'dev' group in 'pyproject.toml'.\n"
1381+
"☐ Install the dependency 'import-linter'.\n"
1382+
"✔ Adding Import Linter config to 'pyproject.toml'.\n"
1383+
"✔ Selecting Ruff rule 'INP' in 'ruff.toml'.\n"
1384+
"☐ Run 'uv run lint-imports' to run Import Linter.\n"
1385+
)
1386+
1387+
@pytest.mark.usefixtures("_vary_network_conn")
1388+
def test_message_for_already_selected_but_needs_ignoring(
1389+
self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]
1390+
):
1391+
# Arrange
1392+
(uv_init_dir / "ruff.toml").write_text("""\
1393+
[lint]
1394+
select = ["INP"]
1395+
""")
1396+
1397+
with change_cwd(uv_init_dir), files_manager():
1398+
# Act
1399+
use_import_linter()
1400+
1401+
# Assert
1402+
contents = (uv_init_dir / "ruff.toml").read_text()
13651403
assert (
13661404
contents
13671405
== """\
@@ -1372,6 +1410,15 @@ def test_inp_rules_not_selected_for_tests_dir(self, tmp_path: Path):
13721410
"tests/**" = ["INP"]
13731411
"""
13741412
)
1413+
out, err = capfd.readouterr()
1414+
assert not err
1415+
assert out == (
1416+
"✔ Adding dependency 'import-linter' to the 'dev' group in 'pyproject.toml'.\n"
1417+
"☐ Install the dependency 'import-linter'.\n"
1418+
"✔ Adding Import Linter config to 'pyproject.toml'.\n"
1419+
"✔ Ignoring Ruff rule 'INP' for 'tests/**' in 'ruff.toml'.\n"
1420+
"☐ Run 'uv run lint-imports' to run Import Linter.\n"
1421+
)
13751422

13761423
@pytest.mark.usefixtures("_vary_network_conn")
13771424
def test_inp_rules_dont_break_config(self, uv_init_dir: Path):

0 commit comments

Comments
 (0)