Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
08d0965
Add draft rule interface
nathanjmcdougall Mar 25, 2025
8abf9b2
Merge branch 'main' into 311-implement-a-rule-management-interface
nathanjmcdougall Mar 30, 2025
b4672f7
Merge branch 'main' into 311-implement-a-rule-management-interface
nathanjmcdougall Mar 31, 2025
1e59ee8
Add uv badge and badge tests
nathanjmcdougall Apr 3, 2025
83491ac
Add test coverage for adding uv badge in `usethis readme`
nathanjmcdougall Apr 3, 2025
1d2a31b
Merge branch '458-implement-usethis-badge-uv' into 311-implement-a-ru…
nathanjmcdougall Apr 3, 2025
99cc343
Merge branch 'main' into 311-implement-a-rule-management-interface
nathanjmcdougall Apr 3, 2025
587fd96
Merge branch 'main' into 311-implement-a-rule-management-interface
nathanjmcdougall Apr 21, 2025
a745814
Address TODOs and add tests
nathanjmcdougall Apr 22, 2025
e32e054
ignore setup-uv cache directory in coverage
nathanjmcdougall Apr 22, 2025
d41e7fb
Merge branch 'main' into 311-implement-a-rule-management-interface
nathanjmcdougall Apr 22, 2025
556985f
Merge branch 'main' into 311-implement-a-rule-management-interface
nathanjmcdougall Apr 24, 2025
9003519
Revert "ignore setup-uv cache directory in coverage"
nathanjmcdougall Apr 24, 2025
0235631
Merge branch 'main' into 311-implement-a-rule-management-interface
nathanjmcdougall Apr 24, 2025
1658baa
Make a distinction between deselecting and ignoring rules
nathanjmcdougall Apr 24, 2025
98f1152
Add docs in README
nathanjmcdougall Apr 24, 2025
2e101b8
Add explicit `unignore_rules` abstract method
nathanjmcdougall Apr 24, 2025
866e293
Add a message to inform the user how selection is handled in deptry, …
nathanjmcdougall Apr 24, 2025
3ad3a7f
Rename use_rules -> select_rules
nathanjmcdougall Apr 24, 2025
8b6d305
Merge branch 'main' into 311-implement-a-rule-management-interface
nathanjmcdougall Apr 26, 2025
f9b0ae6
Fix broken imports follwoing merge
nathanjmcdougall Apr 26, 2025
50d72ab
Only display info message about deptry rules always being sleected if…
nathanjmcdougall Apr 26, 2025
51ade51
Add test of unignoring deptry rules
nathanjmcdougall Apr 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 43 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ Alternatively, run in isolation, using `uvx` or `pipx`.

### Configuration

- [`usethis readme`](#usethis-readme)
- [`usethis author`](#usethis-author)
- [`usethis badge`](#usethis-badge)
- [`usethis rule`](#usethis-rule-rulecode)
- [`usethis docstyle`](#usethis-docstyle)
- [`usethis readme`](#usethis-readme)
- [`usethis author`](#usethis-author)

### Information

Expand All @@ -82,7 +83,7 @@ $ uvx usethis tool ruff
✔ Writing 'pyproject.toml'.
✔ Adding dependency 'ruff' to the 'dev' group in 'pyproject.toml'.
✔ Adding Ruff config to 'pyproject.toml'.
Enabling Ruff rules 'A', 'C4', 'E4', 'E7', 'E9', 'F', 'FLY', 'FURB', 'I', 'PLE', 'PLR', 'RUF', 'SIM', 'UP' in 'pyproject.toml'.
Selecting Ruff rules 'A', 'C4', 'E4', 'E7', 'E9', 'F', 'FLY', 'FURB', 'I', 'PLE', 'PLR', 'RUF', 'SIM', 'UP' in 'pyproject.toml'.
✔ Ignoring Ruff rules 'PLR2004', 'SIM108' in 'pyproject.toml'.
☐ Run 'uv run ruff check --fix' to run the Ruff linter with autofixes.
☐ Run 'uv run ruff format' to run the Ruff formatter.
Expand All @@ -94,7 +95,7 @@ To use pytest, run:
$ uvx usethis tool pytest
✔ Adding dependency 'pytest' to the 'test' group in 'pyproject.toml'.
✔ Adding pytest config to 'pyproject.toml'.
Enabling Ruff rule 'PT' in 'pyproject.toml'.
Selecting Ruff rule 'PT' in 'pyproject.toml'.
✔ Creating '/tests'.
✔ Writing '/tests/conftest.py'.
☐ Add test files to the '/tests' directory with the format 'test_*.py'.
Expand Down Expand Up @@ -159,29 +160,6 @@ Supported options:
- `--offline` to disable network access and rely on caches
- `--quiet` to suppress output

### `usethis readme`

Add a README.md file to the project.

Supported options:

- `--quiet` to suppress output
- `--badges` to also add badges to the README.md file

### `usethis author`

Set new author information for the project.

Required options:

- `--name` for the new author's name

Other supported options:

- `--email` to set the author email address
- `--overwrite` to overwrite all existing author information
- `--quiet` to suppress output

### `usethis badge`

Add badges to README.md.
Expand All @@ -200,6 +178,21 @@ Supported options:
- `--offline` to disable network access and rely on caches
- `--quiet` to suppress output

### `usethis rule <rulecode>`

Add (or manage configuration) of Ruff and Deptry rules in `pyproject.toml`.

Example:

`usethis rule RUF001`

Supported options:

- `--remove` to remove the rule selection or ignore status.
- `--ignore` to add the rule to the ignore list (or remove it if --remove is specified).
- `--offline` to disable network access and rely on caches
- `--quiet` to suppress output

### `usethis docstyle`

Set a docstring style convention for the project, and enforce it with Ruff.
Expand All @@ -214,6 +207,29 @@ Supported options:

- `--quiet` to suppress output

### `usethis readme`

Add a README.md file to the project.

Supported options:

- `--quiet` to suppress output
- `--badges` to also add badges to the README.md file

### `usethis author`

Set new author information for the project.

Required options:

- `--name` for the new author's name

Other supported options:

- `--email` to set the author email address
- `--overwrite` to overwrite all existing author information
- `--quiet` to suppress output

### `usethis list`

Display a table of all available tools and their current usage status.
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ name = "usethis._interface"
type = "layers"
layers = [
# Note; if you're adding an interface, make sure it's in the README too.
"author | badge | browse | ci | docstyle | list | readme | show | tool | version",
"author | badge | browse | ci | docstyle | list | readme | rule | show | tool | version",
]
containers = [ "usethis._interface" ]
exhaustive = true
Expand All @@ -190,7 +190,7 @@ name = "usethis._core"
type = "layers"
layers = [
# docstyle uses (Ruff) tool, badge uses readme
"badge | docstyle | list",
"badge | docstyle | list | rule",
"author | browse | ci | readme | show | tool",
]
containers = [ "usethis._core" ]
Expand Down
6 changes: 6 additions & 0 deletions src/usethis/_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import usethis._interface.docstyle
import usethis._interface.list
import usethis._interface.readme
import usethis._interface.rule
import usethis._interface.show
import usethis._interface.tool
import usethis._interface.version
Expand All @@ -32,6 +33,11 @@
app.command(help="Enforce a docstring style.", rich_help_panel=rich_help_panel)(
usethis._interface.docstyle.docstyle,
)
app.command(
help="Enable a lint rule for the project.", rich_help_panel=rich_help_panel
)(
usethis._interface.rule.rule,
)

rich_help_panel = "Manage README"
app.add_typer(
Expand Down
50 changes: 50 additions & 0 deletions src/usethis/_core/rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from pydantic import BaseModel

from usethis._core.tool import use_deptry, use_ruff
from usethis._tool.impl.deptry import DeptryTool
from usethis._tool.impl.ruff import RuffTool


class RulesMapping(BaseModel):
ruff_rules: list[str]
deptry_rules: list[str]


def select_rules(rules: list[str]) -> None:
rules_mapping = get_rules_mapping(rules)

if rules_mapping.deptry_rules and not DeptryTool().is_used():
use_deptry()
if rules_mapping.ruff_rules and not RuffTool().is_used():
use_ruff()

DeptryTool().select_rules(rules_mapping.deptry_rules)
RuffTool().select_rules(rules_mapping.ruff_rules)


def deselect_rules(rules: list[str]) -> None:
rules_mapping = get_rules_mapping(rules)

DeptryTool().deselect_rules(rules_mapping.deptry_rules)
RuffTool().deselect_rules(rules_mapping.ruff_rules)


def ignore_rules(rules: list[str]) -> None:
rules_mapping = get_rules_mapping(rules)

DeptryTool().ignore_rules(rules_mapping.deptry_rules)
RuffTool().ignore_rules(rules_mapping.ruff_rules)


def unignore_rules(rules: list[str]) -> None:
rules_mapping = get_rules_mapping(rules)

DeptryTool().unignore_rules(rules_mapping.deptry_rules)
RuffTool().unignore_rules(rules_mapping.ruff_rules)


def get_rules_mapping(rules: list[str]) -> RulesMapping:
deptry_rules = [rule for rule in rules if rule.startswith("DEP")]
ruff_rules = [rule for rule in rules if rule not in deptry_rules]

return RulesMapping(ruff_rules=ruff_rules, deptry_rules=deptry_rules)
35 changes: 35 additions & 0 deletions src/usethis/_interface/rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import typer

from usethis._config import offline_opt, quiet_opt, usethis_config
from usethis._config_file import files_manager
from usethis._core.rule import (
deselect_rules,
ignore_rules,
select_rules,
unignore_rules,
)

remove_opt = typer.Option(
False, "--remove", help="Remove the rule selection or ignore status."
)
ignore_opt = typer.Option(
False, "--ignore", help="Add (or remove) the rule to (or from) the ignore list"
)


def rule(
rules: list[str],
remove: bool = remove_opt,
ignore: bool = ignore_opt,
offline: bool = offline_opt,
quiet: bool = quiet_opt,
) -> None:
with usethis_config.set(offline=offline, quiet=quiet), files_manager():
if remove and not ignore:
deselect_rules(rules)
elif ignore and not remove:
ignore_rules(rules)
elif remove and ignore:
unignore_rules(rules)
else:
select_rules(rules)
7 changes: 7 additions & 0 deletions src/usethis/_tool/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,13 @@ def ignore_rules(self, rules: list[Rule]) -> None:
and that the tool will be able to manage them.
"""

def unignore_rules(self, rules: list[str]) -> None:
"""Stop ignoring rules managed by the tool.

These rules are not validated; it is assumed they are valid rules for the tool,
and that the tool will be able to manage them.
"""

def get_ignored_rules(self) -> list[Rule]:
"""Get the ignored rules managed by the tool."""
return []
Expand Down
21 changes: 20 additions & 1 deletion src/usethis/_tool/impl/deptry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pathlib import Path
from typing import TYPE_CHECKING

from usethis._console import box_print, tick_print
from usethis._console import box_print, info_print, tick_print
from usethis._integrations.ci.bitbucket.anchor import (
ScriptItemAnchor as BitbucketScriptItemAnchor,
)
Expand Down Expand Up @@ -101,6 +101,8 @@ def is_managed_rule(self, rule: Rule) -> bool:

def select_rules(self, rules: list[Rule]) -> None:
"""Does nothing for deptry - all rules are automatically enabled by default."""
if rules:
info_print(f"All {self.name} rules are always implicitly selected.")

def get_selected_rules(self) -> list[Rule]:
"""No notion of selection for deptry.
Expand Down Expand Up @@ -130,6 +132,23 @@ def ignore_rules(self, rules: list[Rule]) -> None:
keys = self._get_ignore_keys(file_manager)
file_manager.extend_list(keys=keys, values=rules)

def unignore_rules(self, rules: list[str]) -> None:
rules = sorted(set(rules) & set(self.get_ignored_rules()))

if not rules:
return

rules_str = ", ".join([f"'{rule}'" for rule in rules])
s = "" if len(rules) == 1 else "s"

(file_manager,) = self.get_active_config_file_managers()
ensure_file_manager_exists(file_manager)
tick_print(
f"No longer ignoring {self.name} rule{s} {rules_str} in '{file_manager.name}'."
)
keys = self._get_ignore_keys(file_manager)
file_manager.remove_from_list(keys=keys, values=rules)

def get_ignored_rules(self) -> list[Rule]:
(file_manager,) = self.get_active_config_file_managers()
keys = self._get_ignore_keys(file_manager)
Expand Down
22 changes: 20 additions & 2 deletions src/usethis/_tool/impl/ruff.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def select_rules(self, rules: list[Rule]) -> None:
(file_manager,) = self.get_active_config_file_managers()
ensure_file_manager_exists(file_manager)
tick_print(
f"Enabling {self.name} rule{s} {rules_str} in '{file_manager.name}'."
f"Selecting {self.name} rule{s} {rules_str} in '{file_manager.name}'."
)
keys = self._get_select_keys(file_manager)
file_manager.extend_list(keys=keys, values=rules)
Expand All @@ -187,6 +187,24 @@ def ignore_rules(self, rules: list[Rule]) -> None:
keys = self._get_ignore_keys(file_manager)
file_manager.extend_list(keys=keys, values=rules)

def unignore_rules(self, rules: list[str]) -> None:
"""Unignore Ruff rules in the project."""
rules = list(set(rules) & set(self.get_ignored_rules()))

if not rules:
return

rules_str = ", ".join([f"'{rule}'" for rule in rules])
s = "" if len(rules) == 1 else "s"

(file_manager,) = self.get_active_config_file_managers()
ensure_file_manager_exists(file_manager)
tick_print(
f"No longer ignoring {self.name} rule{s} {rules_str} in '{file_manager.name}'."
)
keys = self._get_ignore_keys(file_manager)
file_manager.remove_from_list(keys=keys, values=rules)

def deselect_rules(self, rules: list[Rule]) -> None:
"""Ensure Ruff rules are not selected in the project."""
rules = list(set(rules) & set(self.get_selected_rules()))
Expand All @@ -200,7 +218,7 @@ def deselect_rules(self, rules: list[Rule]) -> None:
(file_manager,) = self.get_active_config_file_managers()
ensure_file_manager_exists(file_manager)
tick_print(
f"Disabling {self.name} rule{s} {rules_str} in '{file_manager.name}'."
f"Deselecting {self.name} rule{s} {rules_str} in '{file_manager.name}'."
)
keys = self._get_select_keys(file_manager)
file_manager.remove_from_list(keys=keys, values=rules)
Expand Down
4 changes: 2 additions & 2 deletions tests/usethis/_core/test_core_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2237,7 +2237,7 @@ def test_message(
assert (
out
== """\
Disabling Ruff rule 'PT' in 'pyproject.toml'.
Deselecting Ruff rule 'PT' in 'pyproject.toml'.
"""
)

Expand Down Expand Up @@ -2606,7 +2606,7 @@ def test_stdout(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]):
"✔ Adding dependency 'ruff' to the 'dev' group in 'pyproject.toml'.\n"
"☐ Install the dependency 'ruff'.\n"
"✔ Adding Ruff config to 'pyproject.toml'.\n"
"✔ Enabling Ruff rules 'A', 'C4', 'E4', 'E7', 'E9', 'F', 'FLY', 'FURB', 'I', \n'PLE', 'PLR', 'RUF', 'SIM', 'UP' in 'pyproject.toml'.\n"
"✔ Selecting Ruff rules 'A', 'C4', 'E4', 'E7', 'E9', 'F', 'FLY', 'FURB', 'I', \n'PLE', 'PLR', 'RUF', 'SIM', 'UP' in 'pyproject.toml'.\n"
"✔ Ignoring Ruff rules 'PLR2004', 'SIM108' in 'pyproject.toml'.\n"
"☐ Run 'ruff check --fix' to run the Ruff linter with autofixes.\n"
"☐ Run 'ruff format' to run the Ruff formatter.\n"
Expand Down
2 changes: 1 addition & 1 deletion tests/usethis/_core/test_docstyle.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def test_google(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]):
assert not err
assert out == (
"✔ Setting docstring style to 'google' in 'ruff.toml'.\n"
"✔ Enabling Ruff rules 'D2', 'D3', 'D4' in 'ruff.toml'.\n"
"✔ Selecting Ruff rules 'D2', 'D3', 'D4' in 'ruff.toml'.\n"
)

def test_pep257(self, tmp_path: Path):
Expand Down
Loading