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
84 changes: 3 additions & 81 deletions src/usethis/_tool/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
hook_ids_are_equivalent,
remove_hook,
)
from usethis._tool.config import ConfigSpec, NoConfigValue, ensure_managed_file_exists
from usethis._tool.config import NoConfigValue, ensure_managed_file_exists
from usethis._tool.heuristics import is_likely_used
from usethis._tool.spec import ToolMeta, ToolSpec
from usethis._types.backend import BackendEnum
Expand All @@ -38,10 +38,8 @@
)

if TYPE_CHECKING:
from pathlib import Path

from usethis._io import KeyValueFileManager
from usethis._tool.config import ConfigItem, ResolutionT
from usethis._tool.config import ConfigItem
from usethis._tool.rule import Rule

__all__ = ["Tool", "ToolMeta", "ToolSpec"]
Expand Down Expand Up @@ -123,16 +121,6 @@ def how_to_use_pre_commit_hook_id(self) -> str:

return hook_id

def config_spec(self) -> ConfigSpec:
"""Get the configuration specification for this tool.

This can be dynamically determined, e.g. based on the source directory structure
of the current project.

This includes the file managers and resolution methodology.
"""
return ConfigSpec.empty()

def is_used(self) -> bool:
"""Whether the tool is being used in the current project.

Expand All @@ -142,7 +130,7 @@ def is_used(self) -> bool:
3. Whether any of the tool's managed config file sections are present.
4. Whether any of the tool's characteristic pre-commit hooks are present.
"""
return is_likely_used(self, self.config_spec())
return is_likely_used(self)

def add_dev_deps(self) -> None:
add_deps_to_group(self.dev_deps(), "dev")
Expand Down Expand Up @@ -235,72 +223,6 @@ def migrate_config_from_pre_commit(self) -> None:
if pre_commit_config.inform_how_to_use_on_migrate:
self.print_how_to_use()

def get_active_config_file_managers(self) -> set[KeyValueFileManager[object]]:
"""Get file managers for all active configuration files.

Active configuration files are just those that we expect to use based on our
strategy for deciding on relevant files: this is a combination of the resolution
methodology associated with the tool, and hard-coded preferences for certain
files.

Most commonly, this will just be a single file manager. The active config files
themselves do not necessarily exist yet.
"""
config_spec = self.config_spec()
resolution = config_spec.resolution
return self._get_active_config_file_managers_from_resolution(
resolution,
file_manager_by_relative_path=config_spec.file_manager_by_relative_path,
)

def _get_active_config_file_managers_from_resolution(
self,
resolution: ResolutionT,
*,
file_manager_by_relative_path: dict[Path, KeyValueFileManager[object]],
) -> set[KeyValueFileManager[object]]:
if resolution == "first":
# N.B. keep this roughly in sync with the bespoke logic for pytest
# since that logic is based on this logic.
for (
relative_path,
file_manager,
) in file_manager_by_relative_path.items():
path = usethis_config.cpd() / relative_path
if path.exists() and path.is_file():
return {file_manager}
elif resolution == "first_content":
config_spec = self.config_spec()
for relative_path, file_manager in file_manager_by_relative_path.items():
path = usethis_config.cpd() / relative_path
if path.exists() and path.is_file():
# We check whether any of the managed config exists
for config_item in config_spec.config_items:
if config_item.root[relative_path].keys in file_manager:
return {file_manager}
elif resolution == "bespoke":
msg = (
"The bespoke resolution method is not yet implemented for the tool "
f"{self.name}."
)
raise NotImplementedError(msg)
else:
assert_never(resolution)

file_managers = file_manager_by_relative_path.values()
if not file_managers:
return set()

preferred_file_manager = self.preferred_file_manager()
if preferred_file_manager not in file_managers:
msg = (
f"The preferred file manager '{preferred_file_manager}' is not "
f"among the file managers '{file_managers}' for the tool "
f"'{self.name}'."
)
raise NotImplementedError(msg)
return {preferred_file_manager}

def is_config_present(self) -> bool:
"""Whether any of the tool's managed config sections are present."""
return self.config_spec().is_present()
Expand Down
6 changes: 2 additions & 4 deletions src/usethis/_tool/heuristics.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
from usethis.errors import FileConfigError

if TYPE_CHECKING:
from usethis._tool.config import ConfigSpec
from usethis._tool.spec import ToolSpec


def is_likely_used(tool_spec: ToolSpec, config_spec: ConfigSpec) -> bool:
def is_likely_used(tool_spec: ToolSpec) -> bool:
"""Determine whether a tool is likely used in the current project.

Four heuristics are used:
Expand All @@ -21,7 +20,6 @@ def is_likely_used(tool_spec: ToolSpec, config_spec: ConfigSpec) -> bool:

Args:
tool_spec: The tool specification to check.
config_spec: The configuration specification for the tool.

Returns:
True if the tool is likely used, False otherwise.
Expand All @@ -39,7 +37,7 @@ def is_likely_used(tool_spec: ToolSpec, config_spec: ConfigSpec) -> bool:

if not _is_used:
try:
_is_used = config_spec.is_present()
_is_used = tool_spec.config_spec().is_present()
except FileConfigError as err:
decode_err_by_name[err.name] = err

Expand Down
62 changes: 0 additions & 62 deletions src/usethis/_tool/impl/base/codespell.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,10 @@
from __future__ import annotations

from pathlib import Path

from usethis._config_file import DotCodespellRCManager
from usethis._console import how_print
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._file.setup_cfg.io_ import SetupCFGManager
from usethis._tool.base import Tool
from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec
from usethis._tool.impl.spec.codespell import CodespellToolSpec


class CodespellTool(CodespellToolSpec, Tool):
def config_spec(self) -> ConfigSpec:
# https://github.com/codespell-project/codespell?tab=readme-ov-file#using-a-config-file

return ConfigSpec.from_flat(
file_managers=[
DotCodespellRCManager(),
SetupCFGManager(),
PyprojectTOMLManager(),
],
resolution="first_content",
config_items=[
ConfigItem(
description="Overall config",
root={
Path(".codespellrc"): ConfigEntry(keys=[]),
Path("setup.cfg"): ConfigEntry(keys=["codespell"]),
Path("pyproject.toml"): ConfigEntry(keys=["tool", "codespell"]),
},
),
ConfigItem(
description="Ignore long base64 strings",
root={
Path(".codespellrc"): ConfigEntry(
keys=["codespell", "ignore-regex"],
get_value=lambda: "[A-Za-z0-9+/]{100,}",
),
Path("setup.cfg"): ConfigEntry(
keys=["codespell", "ignore-regex"],
get_value=lambda: "[A-Za-z0-9+/]{100,}",
),
Path("pyproject.toml"): ConfigEntry(
keys=["tool", "codespell", "ignore-regex"],
get_value=lambda: ["[A-Za-z0-9+/]{100,}"],
),
},
),
ConfigItem(
description="Ignore Words List",
root={
Path(".codespellrc"): ConfigEntry(
keys=["codespell", "ignore-words-list"],
get_value=lambda: "...",
),
Path("setup.cfg"): ConfigEntry(
keys=["codespell", "ignore-words-list"],
get_value=lambda: "...",
),
Path("pyproject.toml"): ConfigEntry(
keys=["tool", "codespell", "ignore-words-list"],
get_value=lambda: ["..."],
),
},
),
],
)

def print_how_to_use(self) -> None:
how_print(f"Run '{self.how_to_use_cmd()}' to run the {self.name} spellchecker.")
Loading
Loading