Skip to content

Commit 2ae04db

Browse files
Move config_spec into ToolSpec (#1430)
* Initial plan * Move config_spec from Tool to ToolSpec and refactor is_likely_used - Add config_spec() method to ToolSpec protocol in spec.py - Move config_spec() implementations from impl/base/ to impl/spec/ for: codespell, coverage_py, deptry, mkdocs, pyproject_fmt, pytest - Keep config_spec() override on impl/base/ for ruff (uses instance attrs) and import_linter (uses Tool methods) - Refactor is_likely_used() to only take ToolSpec (removed config_spec param) - Update Tool.is_used() to call simplified is_likely_used() - Update tests accordingly Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> * Apply formatting fixes from static checks Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> * Move __init__, config_spec, get_active_config_file_managers to ToolSpec - Move RuffTool.__init__ to RuffToolSpec so linter_detection/formatter_detection attrs are available at the spec level - Move config_spec from RuffTool to RuffToolSpec - Move config_spec and helpers (_are_active_ini_contracts, _is_root_package_singular) from ImportLinterTool to ImportLinterToolSpec - Move get_active_config_file_managers and _get_active_config_file_managers_from_resolution from Tool to ToolSpec Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> Co-authored-by: Nathan McDougall <nathan.j.mcdougall@gmail.com>
1 parent 966a19b commit 2ae04db

20 files changed

Lines changed: 882 additions & 891 deletions

src/usethis/_tool/base.py

Lines changed: 3 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
hook_ids_are_equivalent,
2929
remove_hook,
3030
)
31-
from usethis._tool.config import ConfigSpec, NoConfigValue, ensure_managed_file_exists
31+
from usethis._tool.config import NoConfigValue, ensure_managed_file_exists
3232
from usethis._tool.heuristics import is_likely_used
3333
from usethis._tool.spec import ToolMeta, ToolSpec
3434
from usethis._types.backend import BackendEnum
@@ -38,10 +38,8 @@
3838
)
3939

4040
if TYPE_CHECKING:
41-
from pathlib import Path
42-
4341
from usethis._io import KeyValueFileManager
44-
from usethis._tool.config import ConfigItem, ResolutionT
42+
from usethis._tool.config import ConfigItem
4543
from usethis._tool.rule import Rule
4644

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

124122
return hook_id
125123

126-
def config_spec(self) -> ConfigSpec:
127-
"""Get the configuration specification for this tool.
128-
129-
This can be dynamically determined, e.g. based on the source directory structure
130-
of the current project.
131-
132-
This includes the file managers and resolution methodology.
133-
"""
134-
return ConfigSpec.empty()
135-
136124
def is_used(self) -> bool:
137125
"""Whether the tool is being used in the current project.
138126
@@ -142,7 +130,7 @@ def is_used(self) -> bool:
142130
3. Whether any of the tool's managed config file sections are present.
143131
4. Whether any of the tool's characteristic pre-commit hooks are present.
144132
"""
145-
return is_likely_used(self, self.config_spec())
133+
return is_likely_used(self)
146134

147135
def add_dev_deps(self) -> None:
148136
add_deps_to_group(self.dev_deps(), "dev")
@@ -235,72 +223,6 @@ def migrate_config_from_pre_commit(self) -> None:
235223
if pre_commit_config.inform_how_to_use_on_migrate:
236224
self.print_how_to_use()
237225

238-
def get_active_config_file_managers(self) -> set[KeyValueFileManager[object]]:
239-
"""Get file managers for all active configuration files.
240-
241-
Active configuration files are just those that we expect to use based on our
242-
strategy for deciding on relevant files: this is a combination of the resolution
243-
methodology associated with the tool, and hard-coded preferences for certain
244-
files.
245-
246-
Most commonly, this will just be a single file manager. The active config files
247-
themselves do not necessarily exist yet.
248-
"""
249-
config_spec = self.config_spec()
250-
resolution = config_spec.resolution
251-
return self._get_active_config_file_managers_from_resolution(
252-
resolution,
253-
file_manager_by_relative_path=config_spec.file_manager_by_relative_path,
254-
)
255-
256-
def _get_active_config_file_managers_from_resolution(
257-
self,
258-
resolution: ResolutionT,
259-
*,
260-
file_manager_by_relative_path: dict[Path, KeyValueFileManager[object]],
261-
) -> set[KeyValueFileManager[object]]:
262-
if resolution == "first":
263-
# N.B. keep this roughly in sync with the bespoke logic for pytest
264-
# since that logic is based on this logic.
265-
for (
266-
relative_path,
267-
file_manager,
268-
) in file_manager_by_relative_path.items():
269-
path = usethis_config.cpd() / relative_path
270-
if path.exists() and path.is_file():
271-
return {file_manager}
272-
elif resolution == "first_content":
273-
config_spec = self.config_spec()
274-
for relative_path, file_manager in file_manager_by_relative_path.items():
275-
path = usethis_config.cpd() / relative_path
276-
if path.exists() and path.is_file():
277-
# We check whether any of the managed config exists
278-
for config_item in config_spec.config_items:
279-
if config_item.root[relative_path].keys in file_manager:
280-
return {file_manager}
281-
elif resolution == "bespoke":
282-
msg = (
283-
"The bespoke resolution method is not yet implemented for the tool "
284-
f"{self.name}."
285-
)
286-
raise NotImplementedError(msg)
287-
else:
288-
assert_never(resolution)
289-
290-
file_managers = file_manager_by_relative_path.values()
291-
if not file_managers:
292-
return set()
293-
294-
preferred_file_manager = self.preferred_file_manager()
295-
if preferred_file_manager not in file_managers:
296-
msg = (
297-
f"The preferred file manager '{preferred_file_manager}' is not "
298-
f"among the file managers '{file_managers}' for the tool "
299-
f"'{self.name}'."
300-
)
301-
raise NotImplementedError(msg)
302-
return {preferred_file_manager}
303-
304226
def is_config_present(self) -> bool:
305227
"""Whether any of the tool's managed config sections are present."""
306228
return self.config_spec().is_present()

src/usethis/_tool/heuristics.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
from usethis.errors import FileConfigError
77

88
if TYPE_CHECKING:
9-
from usethis._tool.config import ConfigSpec
109
from usethis._tool.spec import ToolSpec
1110

1211

13-
def is_likely_used(tool_spec: ToolSpec, config_spec: ConfigSpec) -> bool:
12+
def is_likely_used(tool_spec: ToolSpec) -> bool:
1413
"""Determine whether a tool is likely used in the current project.
1514
1615
Four heuristics are used:
@@ -21,7 +20,6 @@ def is_likely_used(tool_spec: ToolSpec, config_spec: ConfigSpec) -> bool:
2120
2221
Args:
2322
tool_spec: The tool specification to check.
24-
config_spec: The configuration specification for the tool.
2523
2624
Returns:
2725
True if the tool is likely used, False otherwise.
@@ -39,7 +37,7 @@ def is_likely_used(tool_spec: ToolSpec, config_spec: ConfigSpec) -> bool:
3937

4038
if not _is_used:
4139
try:
42-
_is_used = config_spec.is_present()
40+
_is_used = tool_spec.config_spec().is_present()
4341
except FileConfigError as err:
4442
decode_err_by_name[err.name] = err
4543

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,10 @@
11
from __future__ import annotations
22

3-
from pathlib import Path
4-
5-
from usethis._config_file import DotCodespellRCManager
63
from usethis._console import how_print
7-
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
8-
from usethis._file.setup_cfg.io_ import SetupCFGManager
94
from usethis._tool.base import Tool
10-
from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec
115
from usethis._tool.impl.spec.codespell import CodespellToolSpec
126

137

148
class CodespellTool(CodespellToolSpec, Tool):
15-
def config_spec(self) -> ConfigSpec:
16-
# https://github.com/codespell-project/codespell?tab=readme-ov-file#using-a-config-file
17-
18-
return ConfigSpec.from_flat(
19-
file_managers=[
20-
DotCodespellRCManager(),
21-
SetupCFGManager(),
22-
PyprojectTOMLManager(),
23-
],
24-
resolution="first_content",
25-
config_items=[
26-
ConfigItem(
27-
description="Overall config",
28-
root={
29-
Path(".codespellrc"): ConfigEntry(keys=[]),
30-
Path("setup.cfg"): ConfigEntry(keys=["codespell"]),
31-
Path("pyproject.toml"): ConfigEntry(keys=["tool", "codespell"]),
32-
},
33-
),
34-
ConfigItem(
35-
description="Ignore long base64 strings",
36-
root={
37-
Path(".codespellrc"): ConfigEntry(
38-
keys=["codespell", "ignore-regex"],
39-
get_value=lambda: "[A-Za-z0-9+/]{100,}",
40-
),
41-
Path("setup.cfg"): ConfigEntry(
42-
keys=["codespell", "ignore-regex"],
43-
get_value=lambda: "[A-Za-z0-9+/]{100,}",
44-
),
45-
Path("pyproject.toml"): ConfigEntry(
46-
keys=["tool", "codespell", "ignore-regex"],
47-
get_value=lambda: ["[A-Za-z0-9+/]{100,}"],
48-
),
49-
},
50-
),
51-
ConfigItem(
52-
description="Ignore Words List",
53-
root={
54-
Path(".codespellrc"): ConfigEntry(
55-
keys=["codespell", "ignore-words-list"],
56-
get_value=lambda: "...",
57-
),
58-
Path("setup.cfg"): ConfigEntry(
59-
keys=["codespell", "ignore-words-list"],
60-
get_value=lambda: "...",
61-
),
62-
Path("pyproject.toml"): ConfigEntry(
63-
keys=["tool", "codespell", "ignore-words-list"],
64-
get_value=lambda: ["..."],
65-
),
66-
},
67-
),
68-
],
69-
)
70-
719
def print_how_to_use(self) -> None:
7210
how_print(f"Run '{self.how_to_use_cmd()}' to run the {self.name} spellchecker.")

0 commit comments

Comments
 (0)