Skip to content
Closed
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
33 changes: 30 additions & 3 deletions src/usethis/_tool/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
"""Base classes for tool implementations."""
"""Base classes for tool implementations.

Tool extends ToolSpec with opinionated, heuristic operations that manage a tool's
presence in a project: adding/removing configuration and dependencies, detecting usage,
and providing how-to-use instructions. These involve side-effects and pragmatic decisions.
"""

from __future__ import annotations

Expand Down Expand Up @@ -40,6 +45,19 @@


class Tool(ToolSpec, Protocol):
"""Opinionated operations for managing a third-party tool in a project.

Tool extends ToolSpec with heuristic and pragmatic methods that consider real-world
usage patterns: adding and removing dependencies, managing configuration files,
detecting whether a tool is in use, and presenting how-to-use instructions. These
aspects involve side-effects (mutating project state) and opinionated decisions (e.g.
which heuristics determine tool usage, how to migrate configuration), making them
potentially less stable than the factual ToolSpec layer.

Contrast with `ToolSpec`, which captures only the non-opinionated, factual information
about the tool itself.
"""

def print_how_to_use(self) -> None:
"""Print instructions for using the tool.

Expand Down Expand Up @@ -90,7 +108,12 @@ def how_to_use_cmd(self) -> str:
assert_never(install_method)

def how_to_use_pre_commit_hook_id(self) -> str:
"""The pre-commit hook ID to use when explaining how to run via pre-commit."""
"""The pre-commit hook ID to use when explaining how to run via pre-commit.

Note: this extracts factual information from the pre-commit config and could
arguably live in ToolSpec, but is placed here since it is specific to
how-to-use instruction logic.
"""
pre_commit_repos = self.get_pre_commit_repos()
try:
(pre_commit_repo,) = pre_commit_repos
Expand Down Expand Up @@ -221,7 +244,11 @@ def migrate_config_from_pre_commit(self) -> None:
self.print_how_to_use()

def is_config_present(self) -> bool:
"""Whether any of the tool's managed config sections are present."""
"""Whether any of the tool's managed config sections are present.

Note: this is a read-only query and could arguably live in ToolSpec, but is
placed here since it is primarily used by Tool's mutating operations.
"""
return self.config_spec().is_present()

def add_configs(self) -> None:
Expand Down
26 changes: 23 additions & 3 deletions src/usethis/_tool/spec.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
"""Abstract tool specification base classes."""
"""Abstract tool specification base classes.

ToolSpec captures the factual, non-opinionated aspects of a third-party tool: its
metadata, dependencies, configuration locations, and pre-commit definitions. These are
stable and inherent to the tool itself.
"""

from __future__ import annotations

Expand Down Expand Up @@ -45,6 +50,19 @@ class ToolMeta:


class ToolSpec(Protocol, metaclass=ABCMeta):
"""Factual specification of a third-party tool.

ToolSpec captures non-opinionated, stable information about how a third-party tool
works: its name, managed files, dependencies, configuration file locations, and
pre-commit hook definitions. Implementations should stick to describing what is
inherently true about the tool itself, independent of any pragmatic decisions about
how to best use it in a project.

Contrast with `Tool`, which extends ToolSpec and adds opinionated, heuristic, and
potentially less stable aspects — such as how to decide whether the tool is "in use",
how to add or remove configuration, and how to present instructions to users.
"""

@property
@abstractmethod
def meta(self) -> ToolMeta: ...
Expand Down Expand Up @@ -73,8 +91,10 @@ def managed_files(self) -> list[Path]:
def rule_config(self) -> RuleConfig:
"""Get the linter rule configuration associated with this tool.

This is a static, opinionated configuration which usethis uses when adding the
tool (and managing this and other tools when adding and removing, etc.).
This captures which rule codes are inherently associated with the tool and
which usethis manages when the tool is added or removed. This is part of the
tool's factual specification, although the choice of which rules to select and
ignore is an opinionated decision by usethis.
"""
return self.meta.rule_config

Expand Down