Skip to content

Commit 33ae98a

Browse files
Bugfix/ignore inp rules for tests dir (#757)
* Remove trace references to old repo location * Ignore INP fules for the tests dir when adding INP rules for Import Linter * Add missing `is_used` check
1 parent 59a585c commit 33ae98a

5 files changed

Lines changed: 161 additions & 34 deletions

File tree

src/usethis/_core/tool.py

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def use_import_linter(*, remove: bool = False, how: bool = False) -> None:
136136
tool.add_dev_deps()
137137
tool.add_configs()
138138
if RuffTool().is_used():
139-
RuffTool().select_rules(rule_config.get_all_selected())
139+
RuffTool().apply_rule_config(rule_config)
140140
if PreCommitTool().is_used():
141141
tool.add_pre_commit_config()
142142
else:
@@ -147,7 +147,7 @@ def use_import_linter(*, remove: bool = False, how: bool = False) -> None:
147147
tool.remove_pre_commit_repo_configs()
148148
tool.remove_bitbucket_steps()
149149
if RuffTool().is_used():
150-
RuffTool().deselect_rules(rule_config.selected)
150+
RuffTool().remove_rule_config(rule_config)
151151
tool.remove_configs()
152152
tool.remove_dev_deps()
153153
tool.remove_managed_files()
@@ -284,7 +284,7 @@ def use_pytest(*, remove: bool = False, how: bool = False) -> None:
284284
tool.add_test_deps()
285285
tool.add_configs()
286286
if RuffTool().is_used():
287-
RuffTool().select_rules(rule_config.get_all_selected())
287+
RuffTool().apply_rule_config(rule_config)
288288

289289
# deptry currently can't scan the tests folder for dev deps
290290
# https://github.com/fpgmaas/deptry/issues/302
@@ -300,7 +300,7 @@ def use_pytest(*, remove: bool = False, how: bool = False) -> None:
300300
PytestTool().remove_bitbucket_steps()
301301

302302
if RuffTool().is_used():
303-
RuffTool().deselect_rules(rule_config.selected)
303+
RuffTool().remove_rule_config(rule_config)
304304
tool.remove_configs()
305305
tool.remove_test_deps()
306306
remove_pytest_dir() # Last, since this is a manual step
@@ -401,6 +401,10 @@ def use_ruff(
401401
or not tool.get_selected_rules()
402402
):
403403
rule_config = _get_basic_rule_config()
404+
for _tool in ALL_TOOLS:
405+
tool_rule_config = _tool.get_rule_config()
406+
if not tool_rule_config.empty and _tool.is_used():
407+
rule_config |= tool_rule_config
404408
else:
405409
rule_config = RuleConfig()
406410

@@ -439,28 +443,27 @@ def use_ruff(
439443

440444
def _get_basic_rule_config() -> RuleConfig:
441445
"""Get the basic rule config for Ruff."""
442-
selected = [
443-
"A",
444-
"C4",
445-
"E4",
446-
"E7",
447-
"E9",
448-
"F",
449-
"FLY",
450-
"FURB",
451-
"I",
452-
"PLE",
453-
"PLR",
454-
"RUF",
455-
"SIM",
456-
"UP",
457-
]
458-
for _tool in ALL_TOOLS:
459-
additional_selected = _tool.get_rule_config().get_all_selected()
460-
if additional_selected and _tool.is_used():
461-
selected += additional_selected
462-
ignored = [
463-
"PLR2004", # https://github.com/usethis-python/usethis-python/issues/105
464-
"SIM108", # https://github.com/usethis-python/usethis-python/issues/118
465-
]
466-
return RuleConfig(selected=selected, ignored=ignored)
446+
rule_config = RuleConfig(
447+
selected=[
448+
"A",
449+
"C4",
450+
"E4",
451+
"E7",
452+
"E9",
453+
"F",
454+
"FLY",
455+
"FURB",
456+
"I",
457+
"PLE",
458+
"PLR",
459+
"RUF",
460+
"SIM",
461+
"UP",
462+
],
463+
ignored=[
464+
"PLR2004", # https://github.com/usethis-python/usethis-python/issues/105
465+
"SIM108", # https://github.com/usethis-python/usethis-python/issues/118
466+
],
467+
)
468+
469+
return rule_config

src/usethis/_tool/impl/import_linter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ def get_bitbucket_steps(self) -> list[BitbucketStep]:
357357
]
358358

359359
def get_rule_config(self) -> RuleConfig:
360-
return RuleConfig(unmanaged_selected=["INP"])
360+
return RuleConfig(unmanaged_selected=["INP"], tests_unmanaged_ignored=["INP"])
361361

362362

363363
@functools.cache

src/usethis/_tool/impl/ruff.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
if TYPE_CHECKING:
4040
from usethis._integrations.ci.bitbucket.schema import Pipe as BitbucketPipe
4141
from usethis._io import KeyValueFileManager
42-
from usethis._tool.rule import Rule
42+
from usethis._tool.rule import Rule, RuleConfig
4343

4444

4545
class RuffTool(Tool):
@@ -374,6 +374,35 @@ def get_ignored_rules(self) -> list[Rule]:
374374

375375
return rules
376376

377+
def ignore_rules_in_glob(self, rules: list[Rule], *, glob: str) -> None:
378+
"""Ignore Ruff rules in the project for a specific glob pattern."""
379+
if not rules:
380+
return
381+
382+
(file_manager,) = self.get_active_config_file_managers()
383+
ensure_file_manager_exists(file_manager)
384+
keys = self._get_per_file_ignore_keys(file_manager, glob=glob)
385+
file_manager.extend_list(keys=keys, values=rules)
386+
387+
def apply_rule_config(self, rule_config: RuleConfig) -> None:
388+
"""Apply the Ruff rules associated with a rule config to the project.
389+
390+
Note, this will add both managed and unmanaged config.
391+
"""
392+
self.select_rules(rule_config.get_all_selected())
393+
self.ignore_rules(rule_config.get_all_ignored())
394+
self.ignore_rules_in_glob(
395+
rule_config.tests_unmanaged_ignored, glob="*/tests/**"
396+
)
397+
398+
def remove_rule_config(self, rule_config: RuleConfig) -> None:
399+
"""Remove the Ruff rules associated with a rule config from the project.
400+
401+
Note, this will not modify unmanaged config.
402+
"""
403+
self.deselect_rules(rule_config.selected)
404+
self.unignore_rules(rule_config.ignored)
405+
377406
def set_docstyle(self, style: Literal["numpy", "google", "pep257"]) -> None:
378407
(file_manager,) = self.get_active_config_file_managers()
379408

@@ -442,6 +471,21 @@ def _get_ignore_keys(self, file_manager: KeyValueFileManager) -> list[str]:
442471
)
443472
raise NotImplementedError(msg)
444473

474+
def _get_per_file_ignore_keys(
475+
self, file_manager: KeyValueFileManager, *, glob: str
476+
) -> list[str]:
477+
"""Get the keys for the per-file ignored rules in the given file manager."""
478+
if isinstance(file_manager, PyprojectTOMLManager):
479+
return ["tool", "ruff", "lint", "per-file-ignores", glob]
480+
elif isinstance(file_manager, RuffTOMLManager | DotRuffTOMLManager):
481+
return ["lint", "per-file-ignores", glob]
482+
else:
483+
msg = (
484+
f"Unknown location for per-file ignored {self.name} rules for file manager "
485+
f"'{file_manager.name}' of type {file_manager.__class__.__name__}."
486+
)
487+
raise NotImplementedError(msg)
488+
445489
def _get_docstyle_keys(self, file_manager: KeyValueFileManager) -> list[str]:
446490
"""Get the keys for the docstyle rules in the given file manager."""
447491
if isinstance(file_manager, PyprojectTOMLManager):

src/usethis/_tool/rule.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from __future__ import annotations
22

3-
from typing import TypeAlias
3+
from typing import TYPE_CHECKING, TypeAlias
44

55
from pydantic import BaseModel, Field
66

7+
if TYPE_CHECKING:
8+
from typing_extensions import Self
9+
710
Rule: TypeAlias = str
811

912

@@ -26,17 +29,72 @@ class RuleConfig(BaseModel):
2629
ignored: Managed ignored rules.
2730
unmanaged_selected: Unmanaged selected rules.
2831
unmanaged_ignored: Unmanaged ignored rules.
32+
tests_unmanaged_ignored: Unmanaged cases of rules ignored for specifically the
33+
tests directory.
2934
"""
3035

3136
selected: list[Rule] = Field(default_factory=list)
3237
ignored: list[Rule] = Field(default_factory=list)
3338
unmanaged_selected: list[Rule] = Field(default_factory=list)
3439
unmanaged_ignored: list[Rule] = Field(default_factory=list)
40+
tests_unmanaged_ignored: list[Rule] = Field(default_factory=list)
3541

3642
def get_all_selected(self) -> list[Rule]:
37-
"""Get all selected rules."""
43+
"""Get all (project-scope) selected rules."""
3844
return self.selected + self.unmanaged_selected
3945

4046
def get_all_ignored(self) -> list[Rule]:
41-
"""Get all ignored rules."""
47+
"""Get all (project-scope) ignored rules."""
4248
return self.ignored + self.unmanaged_ignored
49+
50+
@property
51+
def empty(self) -> bool:
52+
"""Check if the rule config is empty."""
53+
return (
54+
not self.selected
55+
and not self.ignored
56+
and not self.unmanaged_selected
57+
and not self.unmanaged_ignored
58+
and not self.tests_unmanaged_ignored
59+
)
60+
61+
def __repr__(self) -> str:
62+
"""Representation which omits empty-list fields."""
63+
args = []
64+
if self.selected:
65+
args.append(f"selected={self.selected}")
66+
if self.ignored:
67+
args.append(f"ignored={self.ignored}")
68+
if self.unmanaged_selected:
69+
args.append(f"unmanaged_selected={self.unmanaged_selected}")
70+
if self.unmanaged_ignored:
71+
args.append(f"unmanaged_ignored={self.unmanaged_ignored}")
72+
if self.tests_unmanaged_ignored:
73+
args.append(f"tests_unmanaged_ignored={self.tests_unmanaged_ignored}")
74+
arg_str = ", ".join(args)
75+
return f"RuleConfig({arg_str})"
76+
77+
def __or__(self, other: Self) -> Self:
78+
"""Merge multiple rule configs together.
79+
80+
Examples:
81+
>>> RuleConfig(selected=["A"]) | RuleConfig(selected=["B"])
82+
RuleConfig(selected=['A', 'B'])
83+
>>> RuleConfig(selected=["A"]) | RuleConfig(ignored=["B"])
84+
RuleConfig(selected=['A'], ignored=['B'])
85+
"""
86+
if not isinstance(other, self.__class__):
87+
msg = (
88+
f"Cannot merge '{self.__class__.__name__}' with "
89+
f"'{other.__class__.__name__}'"
90+
)
91+
raise NotImplementedError(msg)
92+
93+
return type(self)(
94+
selected=self.selected + other.selected,
95+
ignored=self.ignored + other.ignored,
96+
unmanaged_selected=self.unmanaged_selected + other.unmanaged_selected,
97+
unmanaged_ignored=self.unmanaged_ignored + other.unmanaged_ignored,
98+
tests_unmanaged_ignored=self.tests_unmanaged_ignored
99+
+ other.tests_unmanaged_ignored,
100+
)

tests/usethis/_core/test_core_tool.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,28 @@ def test_inp_rules_selected(self, tmp_path: Path):
13391339
# Assert
13401340
assert "INP" in RuffTool().get_selected_rules()
13411341

1342+
@pytest.mark.usefixtures("_vary_network_conn")
1343+
def test_inp_rules_not_selected_for_tests_dir(self, tmp_path: Path):
1344+
# Arrange
1345+
(tmp_path / "ruff.toml").touch()
1346+
1347+
with change_cwd(tmp_path), files_manager():
1348+
# Act
1349+
use_import_linter()
1350+
1351+
# Assert
1352+
contents = (tmp_path / "ruff.toml").read_text()
1353+
assert (
1354+
contents
1355+
== """\
1356+
[lint]
1357+
select = ["INP"]
1358+
1359+
[lint.per-file-ignores]
1360+
"*/tests/**" = ["INP"]
1361+
"""
1362+
)
1363+
13421364
class TestRemove:
13431365
def test_config_file(self, uv_init_repo_dir: Path):
13441366
# Arrange

0 commit comments

Comments
 (0)