Skip to content

Commit bf22248

Browse files
Fix bugs in handling of ruff config and rules (#421)
* Fix bugs in handling of ruff config and rules * Fix incorrect section name in test * Fix bug in deselection
1 parent ff62dc0 commit bf22248

File tree

4 files changed

+131
-48
lines changed

4 files changed

+131
-48
lines changed

src/usethis/_core/tool.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -304,29 +304,36 @@ def use_ruff(*, remove: bool = False) -> None:
304304

305305
ensure_pyproject_toml()
306306

307-
rules = [
308-
"A",
309-
"C4",
310-
"E4",
311-
"E7",
312-
"E9",
313-
"EM",
314-
"F",
315-
"FURB",
316-
"I",
317-
"PLE",
318-
"PLR",
319-
"RUF",
320-
"SIM",
321-
"UP",
322-
]
323-
for _tool in ALL_TOOLS:
324-
if _tool.is_used():
325-
rules += _tool.get_associated_ruff_rules()
326-
ignored_rules = [
327-
"PLR2004", # https://github.com/nathanjmcdougall/usethis-python/issues/105
328-
"SIM108", # https://github.com/nathanjmcdougall/usethis-python/issues/118
329-
]
307+
# Only add ruff rules if the user doesn't already have a select/ignore list.
308+
# Otherwise, we should leave them alone.
309+
310+
if not RuffTool().get_rules() and not RuffTool().get_ignored_rules():
311+
rules = [
312+
"A",
313+
"C4",
314+
"E4",
315+
"E7",
316+
"E9",
317+
"EM",
318+
"F",
319+
"FURB",
320+
"I",
321+
"PLE",
322+
"PLR",
323+
"RUF",
324+
"SIM",
325+
"UP",
326+
]
327+
for _tool in ALL_TOOLS:
328+
if _tool.is_used():
329+
rules += _tool.get_associated_ruff_rules()
330+
ignored_rules = [
331+
"PLR2004", # https://github.com/nathanjmcdougall/usethis-python/issues/105
332+
"SIM108", # https://github.com/nathanjmcdougall/usethis-python/issues/118
333+
]
334+
else:
335+
rules = []
336+
ignored_rules = []
330337

331338
if not remove:
332339
tool.add_dev_deps()

src/usethis/_tool.py

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ def add_configs(self) -> None:
385385

386386
shared_keys = []
387387
for key in entry.keys:
388-
shared_keys += key
388+
shared_keys.append(key)
389389
new_file_managers = [
390390
file_manager
391391
for file_manager in file_managers
@@ -397,12 +397,12 @@ def add_configs(self) -> None:
397397

398398
# Now, use the highest-prority file manager to add the config
399399
(used_file_manager,) = file_managers
400-
used_file_manager[entry.keys] = entry.value
401400
if first_addition:
402401
tick_print(
403402
f"Adding {self.name} config to '{used_file_manager.relative_path}'."
404403
)
405404
first_addition = False
405+
used_file_manager[entry.keys] = entry.value
406406

407407
def remove_configs(self) -> None:
408408
"""Remove the tool's configuration sections.
@@ -1075,8 +1075,7 @@ def get_dev_deps(self, *, unconditional: bool = False) -> list[Dependency]:
10751075
def get_config_spec(self) -> ConfigSpec:
10761076
# https://docs.astral.sh/ruff/configuration/#config-file-discovery
10771077

1078-
root_value = {"line-length": 88}
1079-
lint_value = {"select": []}
1078+
line_length = 88
10801079

10811080
return ConfigSpec.from_flat(
10821081
file_managers=[
@@ -1089,22 +1088,23 @@ def get_config_spec(self) -> ConfigSpec:
10891088
ConfigItem(
10901089
description="Overall config",
10911090
root={
1092-
Path(".ruff.toml"): ConfigEntry(keys=[], value=root_value),
1093-
Path("ruff.toml"): ConfigEntry(keys=[], value=root_value),
1094-
Path("pyproject.toml"): ConfigEntry(
1095-
keys=["tool", "ruff"], value=root_value
1096-
),
1091+
Path(".ruff.toml"): ConfigEntry(keys=[]),
1092+
Path("ruff.toml"): ConfigEntry(keys=[]),
1093+
Path("pyproject.toml"): ConfigEntry(keys=["tool", "ruff"]),
10971094
},
10981095
),
10991096
ConfigItem(
1100-
description="Lint config",
1097+
description="Line length",
11011098
root={
11021099
Path(".ruff.toml"): ConfigEntry(
1103-
keys=["lint"], value=lint_value
1100+
keys=["line-length"], value=line_length
1101+
),
1102+
Path("ruff.toml"): ConfigEntry(
1103+
keys=["line-length"], value=line_length
11041104
),
1105-
Path("ruff.toml"): ConfigEntry(keys=["lint"], value=lint_value),
11061105
Path("pyproject.toml"): ConfigEntry(
1107-
keys=["tool", "ruff", "lint"], value=lint_value
1106+
keys=["tool", "ruff", "line-length"],
1107+
value=line_length,
11081108
),
11091109
},
11101110
),
@@ -1174,10 +1174,11 @@ def select_rules(self, rules: list[str]) -> None:
11741174

11751175
rules_str = ", ".join([f"'{rule}'" for rule in rules])
11761176
s = "" if len(rules) == 1 else "s"
1177-
tick_print(f"Enabling Ruff rule{s} {rules_str} in 'pyproject.toml'.")
11781177

11791178
(file_manager,) = self.get_active_config_file_managers()
1180-
file_manager.extend_list(keys=["tool", "ruff", "lint", "select"], values=rules)
1179+
tick_print(f"Enabling Ruff rule{s} {rules_str} in '{file_manager.name}'.")
1180+
keys = self._get_select_keys(file_manager)
1181+
file_manager.extend_list(keys=keys, values=rules)
11811182

11821183
def ignore_rules(self, rules: list[str]) -> None:
11831184
"""Ignore Ruff rules in the project."""
@@ -1188,10 +1189,11 @@ def ignore_rules(self, rules: list[str]) -> None:
11881189

11891190
rules_str = ", ".join([f"'{rule}'" for rule in rules])
11901191
s = "" if len(rules) == 1 else "s"
1191-
tick_print(f"Ignoring Ruff rule{s} {rules_str} in 'pyproject.toml'.")
11921192

11931193
(file_manager,) = self.get_active_config_file_managers()
1194-
file_manager.extend_list(keys=["tool", "ruff", "lint", "ignore"], values=rules)
1194+
tick_print(f"Ignoring Ruff rule{s} {rules_str} in '{file_manager.name}'.")
1195+
keys = self._get_ignore_keys(file_manager)
1196+
file_manager.extend_list(keys=keys, values=rules)
11951197

11961198
def deselect_rules(self, rules: list[str]) -> None:
11971199
"""Ensure Ruff rules are not selected in the project."""
@@ -1202,18 +1204,18 @@ def deselect_rules(self, rules: list[str]) -> None:
12021204

12031205
rules_str = ", ".join([f"'{rule}'" for rule in rules])
12041206
s = "" if len(rules) == 1 else "s"
1205-
tick_print(f"Disabling Ruff rule{s} {rules_str} in 'pyproject.toml'.")
12061207

12071208
(file_manager,) = self.get_active_config_file_managers()
1208-
file_manager.remove_from_list(
1209-
keys=["tool", "ruff", "lint", "select"], values=rules
1210-
)
1209+
tick_print(f"Disabling Ruff rule{s} {rules_str} in '{file_manager.name}'.")
1210+
keys = self._get_select_keys(file_manager)
1211+
file_manager.remove_from_list(keys=keys, values=rules)
12111212

12121213
def get_rules(self) -> list[str]:
12131214
"""Get the Ruff rules selected in the project."""
12141215
(file_manager,) = self.get_active_config_file_managers()
1216+
keys = self._get_select_keys(file_manager)
12151217
try:
1216-
rules: list[str] = file_manager[["tool", "ruff", "lint", "select"]]
1218+
rules: list[str] = file_manager[keys]
12171219
except KeyError:
12181220
rules = []
12191221

@@ -1222,13 +1224,42 @@ def get_rules(self) -> list[str]:
12221224
def get_ignored_rules(self) -> list[str]:
12231225
"""Get the Ruff rules ignored in the project."""
12241226
(file_manager,) = self.get_active_config_file_managers()
1227+
keys = self._get_ignore_keys(file_manager)
12251228
try:
1226-
rules: list[str] = file_manager[["tool", "ruff", "lint", "ignore"]]
1229+
rules: list[str] = file_manager[keys]
12271230
except KeyError:
12281231
rules = []
12291232

12301233
return rules
12311234

1235+
@staticmethod
1236+
def _get_select_keys(file_manager: KeyValueFileManager) -> list[str]:
1237+
"""Get the keys for the select rules in the given file manager."""
1238+
if isinstance(file_manager, PyprojectTOMLManager):
1239+
return ["tool", "ruff", "lint", "select"]
1240+
elif isinstance(file_manager, RuffTOMLManager | DotRuffTOMLManager):
1241+
return ["lint", "select"]
1242+
else:
1243+
msg = (
1244+
f"Unknown location for selected ruff rules for file manager "
1245+
f"'{file_manager.name}' of type {file_manager.__class__.__name__}."
1246+
)
1247+
raise NotImplementedError(msg)
1248+
1249+
@staticmethod
1250+
def _get_ignore_keys(file_manager: KeyValueFileManager) -> list[str]:
1251+
"""Get the keys for the ignored rules in the given file manager."""
1252+
if isinstance(file_manager, PyprojectTOMLManager):
1253+
return ["tool", "ruff", "lint", "ignore"]
1254+
elif isinstance(file_manager, RuffTOMLManager | DotRuffTOMLManager):
1255+
return ["lint", "ignore"]
1256+
else:
1257+
msg = (
1258+
f"Unknown location for ignored ruff rules for file manager "
1259+
f"'{file_manager.name}' of type {file_manager.__class__.__name__}."
1260+
)
1261+
raise NotImplementedError(msg)
1262+
12321263

12331264
ALL_TOOLS: list[Tool] = [
12341265
CodespellTool(),

tests/usethis/_core/test_core_tool.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,6 +1694,51 @@ def test_creates_pyproject_toml(
16941694
assert not err
16951695
assert out.startswith("✔ Writing 'pyproject.toml'.\n")
16961696

1697+
def test_existing_ruff_toml(
1698+
self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]
1699+
):
1700+
# https://github.com/nathanjmcdougall/usethis-python/issues/420
1701+
1702+
# Arrange
1703+
(uv_init_dir / "ruff.toml").write_text("""\
1704+
namespace-packages = ["src/usethis/namespace"]
1705+
1706+
[lint]
1707+
select = [ "ALL" ]
1708+
ignore = [ "EM", "T20", "TRY003", "S603" ]
1709+
1710+
[lint.extend-per-file-ignores]
1711+
"__main__.py" = [ "BLE001" ]
1712+
""")
1713+
1714+
# Act
1715+
with change_cwd(uv_init_dir), files_manager():
1716+
use_ruff()
1717+
1718+
# Assert
1719+
out, err = capfd.readouterr()
1720+
assert not err
1721+
assert out == (
1722+
"✔ Adding dependency 'ruff' to the 'dev' group in 'pyproject.toml'.\n"
1723+
"☐ Install the dependency 'ruff'.\n"
1724+
"✔ Adding Ruff config to 'ruff.toml'.\n"
1725+
"☐ Run 'ruff check --fix' to run the Ruff linter with autofixes.\n"
1726+
"☐ Run 'ruff format' to run the Ruff formatter.\n"
1727+
)
1728+
assert (uv_init_dir / "ruff.toml").read_text() == (
1729+
"""\
1730+
namespace-packages = ["src/usethis/namespace"]
1731+
line-length = 88
1732+
1733+
[lint]
1734+
select = [ "ALL" ]
1735+
ignore = [ "EM", "T20", "TRY003", "S603" ]
1736+
1737+
[lint.extend-per-file-ignores]
1738+
"__main__.py" = [ "BLE001" ]
1739+
"""
1740+
)
1741+
16971742
class TestRemove:
16981743
@pytest.mark.usefixtures("_vary_network_conn")
16991744
def test_config_file(self, uv_init_dir: Path):

tests/usethis/test_usethis_tool.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -966,7 +966,7 @@ def test_ruff_toml(self, tmp_path: Path):
966966
# Arrange
967967
(tmp_path / "ruff.toml").write_text(
968968
"""
969-
[tool.ruff.lint]
969+
[lint]
970970
select = ["A", "B"]
971971
"""
972972
)
@@ -1053,7 +1053,7 @@ def test_ruff_toml(self, tmp_path: Path):
10531053
# Arrange
10541054
(tmp_path / ".ruff.toml").write_text(
10551055
"""\
1056-
[tool.ruff.lint]
1056+
[lint]
10571057
select = ["A", "B"]
10581058
"""
10591059
)

0 commit comments

Comments
 (0)