Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ The following checks run automatically via prek (pre-commit framework). All must
9. **deptry** - Checks for missing/unused dependencies (`uv run deptry src`)
10. **codespell** - Spell checker
11. **import-linter** - Enforces architecture constraints (`uv run lint-imports`)
12. **ty** - Type checker (`uv run ty`)
12. **ty** - Type checker (`uv run ty check`)

**To run all checks manually:**
```bash
Expand All @@ -91,7 +91,7 @@ uv run ruff format # Formatter
uv run deptry src # Dependency checker
uv run codespell # Spell checker
uv run lint-imports # Architecture constraints
uv run ty # Type checker
uv run ty check # Type checker
```

## Architecture & Project Structure
Expand Down Expand Up @@ -270,7 +270,7 @@ These instructions have been validated by running actual commands and inspecting
| Run all checks | `uv run prek run --all-files` |
| Format code | `uv run ruff format` |
| Lint code | `uv run ruff check --fix` |
| Check types | `uv run ty` |
| Check types | `uv run ty check` |
| Check dependencies | `uv run deptry src` |
| Check architecture | `uv run lint-imports` |
| Serve docs | `uv run mkdocs serve` |
Expand Down
2 changes: 1 addition & 1 deletion src/usethis/_integrations/backend/uv/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from usethis._integrations.backend.uv.call import call_uv_subprocess
from usethis._integrations.backend.uv.errors import UVSubprocessFailedError

FALLBACK_UV_VERSION = "0.9.20"
FALLBACK_UV_VERSION = "0.9.21"


def get_uv_version() -> str:
Expand Down
119 changes: 113 additions & 6 deletions src/usethis/_tool/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@
hook_ids_are_equivalent,
remove_hook,
)
from usethis._tool.config import ConfigSpec, NoConfigValue
from usethis._tool.config import ConfigSpec, NoConfigValue, ensure_managed_file_exists
from usethis._tool.pre_commit import PreCommitConfig
from usethis._tool.rule import RuleConfig
from usethis._types.backend import BackendEnum
from usethis.errors import FileConfigError, NoDefaultToolCommand
from usethis.errors import (
FileConfigError,
NoDefaultToolCommand,
UnhandledConfigEntryError,
)

if TYPE_CHECKING:
from pathlib import Path
Expand Down Expand Up @@ -682,6 +686,27 @@ def is_managed_rule(self, rule: Rule) -> bool:
"""Determine if a rule is managed by this tool."""
return False

def _get_select_keys(self, file_manager: KeyValueFileManager) -> list[str]:
"""Get the configuration keys for selected rules.

This is optional - tools that don't support rule selection can leave this
unimplemented and will get an UnhandledConfigEntryError if selection is attempted.

Args:
file_manager: The file manager being used.

Returns:
List of keys to access the selected rules config section.

Raises:
UnhandledConfigEntryError: If the file manager type is not supported.
"""
msg = (
f"Unknown location for selected {self.name} rules for file manager "
f"'{file_manager.name}' of type '{file_manager.__class__.__name__}'."
)
raise UnhandledConfigEntryError(msg)

def select_rules(self, rules: list[Rule]) -> bool:
"""Select the rules managed by the tool.

Expand All @@ -695,12 +720,46 @@ def select_rules(self, rules: list[Rule]) -> bool:
Returns:
True if any rules were selected, False if no rules were selected.
"""
return False
rules = sorted(set(rules) - set(self.get_selected_rules()))

if not rules:
return False

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_managed_file_exists(file_manager)
tick_print(
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)

return True

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

def _get_ignore_keys(self, file_manager: KeyValueFileManager) -> list[str]:
"""Get the configuration keys for ignored rules.

Args:
file_manager: The file manager being used.

Returns:
List of keys to access the ignored rules config section.

Raises:
UnhandledConfigEntryError: If the file manager type is not supported.
"""
msg = (
f"Unknown location for ignored {self.name} rules for file manager "
f"'{file_manager.name}' of type '{file_manager.__class__.__name__}'."
)
raise UnhandledConfigEntryError(msg)

def ignore_rules(self, rules: list[Rule]) -> bool:
"""Ignore rules managed by the tool.

Expand All @@ -718,7 +777,23 @@ def ignore_rules(self, rules: list[Rule]) -> bool:
Returns:
True if any rules were ignored, False if no rules were ignored.
"""
return False
rules = sorted(set(rules) - set(self.get_ignored_rules()))

if not rules:
return False

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_managed_file_exists(file_manager)
tick_print(
f"Ignoring {self.name} rule{s} {rules_str} in '{file_manager.name}'."
)
keys = self._get_ignore_keys(file_manager)
file_manager.extend_list(keys=keys, values=rules)

return True

def unignore_rules(self, rules: list[str]) -> bool:
"""Stop ignoring rules managed by the tool.
Expand All @@ -733,7 +808,23 @@ def unignore_rules(self, rules: list[str]) -> bool:
Returns:
True if any rules were unignored, False if no rules were unignored.
"""
return False
rules = sorted(set(rules) & set(self.get_ignored_rules()))

if not rules:
return False

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_managed_file_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)

return True

def get_ignored_rules(self) -> list[Rule]:
"""Get the ignored rules managed by the tool."""
Expand All @@ -751,4 +842,20 @@ def deselect_rules(self, rules: list[Rule]) -> bool:
Returns:
True if any rules were deselected, False if no rules were deselected.
"""
return False
rules = sorted(set(rules) & set(self.get_selected_rules()))

if not rules:
return False

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_managed_file_exists(file_manager)
tick_print(
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)

return True
47 changes: 2 additions & 45 deletions src/usethis/_tool/impl/deptry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from typing_extensions import assert_never

from usethis._console import how_print, info_print, tick_print
from usethis._console import how_print, info_print
from usethis._integrations.backend.dispatch import get_backend
from usethis._integrations.backend.uv.used import is_uv_used
from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager
Expand All @@ -17,7 +17,6 @@
ConfigEntry,
ConfigItem,
ConfigSpec,
ensure_managed_file_exists,
)
from usethis._tool.pre_commit import PreCommitConfig
from usethis._types.backend import BackendEnum
Expand Down Expand Up @@ -154,44 +153,6 @@ def deselect_rules(self, rules: list[Rule]) -> bool:
"""Does nothing for deptry - all rules are automatically enabled by default."""
return False

def ignore_rules(self, rules: list[Rule]) -> bool:
rules = sorted(set(rules) - set(self.get_ignored_rules()))

if not rules:
return False

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_managed_file_exists(file_manager)
tick_print(
f"Ignoring {self.name} rule{s} {rules_str} in '{file_manager.name}'."
)
keys = self._get_ignore_keys(file_manager)
file_manager.extend_list(keys=keys, values=rules)

return True

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

if not rules:
return False

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_managed_file_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)

return True

def get_ignored_rules(self) -> list[Rule]:
(file_manager,) = self.get_active_config_file_managers()
keys = self._get_ignore_keys(file_manager)
Expand All @@ -207,8 +168,4 @@ def _get_ignore_keys(self, file_manager: KeyValueFileManager) -> list[str]:
if isinstance(file_manager, PyprojectTOMLManager):
return ["tool", "deptry", "ignore"]
else:
msg = (
f"Unknown location for ignored {self.name} rules for file manager "
f"'{file_manager.name}' of type {file_manager.__class__.__name__}."
)
raise NotImplementedError(msg)
return super()._get_ignore_keys(file_manager)
90 changes: 2 additions & 88 deletions src/usethis/_tool/impl/ruff.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,84 +334,6 @@ def get_bitbucket_steps(self, *, matrix_python: bool = True) -> list[BitbucketSt

return steps

def select_rules(self, rules: list[Rule]) -> bool:
"""Add Ruff rules to the project."""
rules = sorted(set(rules) - set(self.get_selected_rules()))

if not rules:
return False

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_managed_file_exists(file_manager)
tick_print(
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)

return True

def ignore_rules(self, rules: list[Rule]) -> bool:
"""Ignore Ruff rules in the project."""
rules = sorted(set(rules) - set(self.get_ignored_rules()))

if not rules:
return False

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_managed_file_exists(file_manager)
tick_print(
f"Ignoring {self.name} rule{s} {rules_str} in '{file_manager.name}'."
)
keys = self._get_ignore_keys(file_manager)
file_manager.extend_list(keys=keys, values=rules)

return True

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

if not rules:
return False

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_managed_file_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)
return True

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

if not rules:
return False

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_managed_file_exists(file_manager)
tick_print(
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)
return True

def get_selected_rules(self) -> list[Rule]:
"""Get the Ruff rules selected in the project."""
(file_manager,) = self.get_active_config_file_managers()
Expand Down Expand Up @@ -548,11 +470,7 @@ def _get_select_keys(self, file_manager: KeyValueFileManager) -> list[str]:
elif isinstance(file_manager, RuffTOMLManager | DotRuffTOMLManager):
return ["lint", "select"]
else:
msg = (
f"Unknown location for selected {self.name} rules for file manager "
f"'{file_manager.name}' of type '{file_manager.__class__.__name__}'."
)
raise NotImplementedError(msg)
return super()._get_select_keys(file_manager)

def _get_ignore_keys(self, file_manager: KeyValueFileManager) -> list[str]:
"""Get the keys for the ignored rules in the given file manager."""
Expand All @@ -561,11 +479,7 @@ def _get_ignore_keys(self, file_manager: KeyValueFileManager) -> list[str]:
elif isinstance(file_manager, RuffTOMLManager | DotRuffTOMLManager):
return ["lint", "ignore"]
else:
msg = (
f"Unknown location for ignored {self.name} rules for file manager "
f"'{file_manager.name}' of type '{file_manager.__class__.__name__}'."
)
raise NotImplementedError(msg)
return super()._get_ignore_keys(file_manager)

def _get_per_file_ignore_keys(
self, file_manager: KeyValueFileManager, *, glob: str
Expand Down
4 changes: 4 additions & 0 deletions src/usethis/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ class DepGroupError(UsethisError):

class NoDefaultToolCommand(UsethisError):
"""Raised when a tool does not have a default command."""


class UnhandledConfigEntryError(UsethisError):
"""Raised when a tool encounters an unknown file manager type for config entries."""
Loading