Skip to content

Commit b247646

Browse files
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>
1 parent 9a580f9 commit b247646

6 files changed

Lines changed: 387 additions & 388 deletions

File tree

src/usethis/_tool/base.py

Lines changed: 1 addition & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -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"]
@@ -225,72 +223,6 @@ def migrate_config_from_pre_commit(self) -> None:
225223
if pre_commit_config.inform_how_to_use_on_migrate:
226224
self.print_how_to_use()
227225

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

src/usethis/_tool/impl/base/import_linter.py

Lines changed: 0 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -1,211 +1,18 @@
11
from __future__ import annotations
22

3-
import re
4-
from pathlib import Path
53
from typing import TYPE_CHECKING
64

75
from usethis._config import usethis_config
8-
from usethis._config_file import DotImportLinterManager
96
from usethis._console import info_print
10-
from usethis._file.ini.io_ import INIFileManager
11-
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
12-
from usethis._file.setup_cfg.io_ import SetupCFGManager
137
from usethis._tool.base import Tool
14-
from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec, NoConfigValue
158
from usethis._tool.impl.base.ruff import RuffTool
169
from usethis._tool.impl.spec.import_linter import ImportLinterToolSpec
1710

1811
if TYPE_CHECKING:
1912
from usethis._tool.rule import Rule
2013

21-
IMPORT_LINTER_CONTRACT_MIN_MODULE_COUNT = 3
22-
2314

2415
class ImportLinterTool(ImportLinterToolSpec, Tool):
25-
def config_spec(self) -> ConfigSpec:
26-
# https://import-linter.readthedocs.io/en/stable/usage.html
27-
28-
layered_architecture_by_module_by_root_package = (
29-
self._get_layered_architecture_by_module_by_root_package()
30-
)
31-
32-
min_depth = min(
33-
(
34-
module.count(".")
35-
for layered_architecture_by_module in layered_architecture_by_module_by_root_package.values()
36-
for module in layered_architecture_by_module
37-
if any(
38-
layered_architecture.module_count()
39-
>= IMPORT_LINTER_CONTRACT_MIN_MODULE_COUNT
40-
for layered_architecture in layered_architecture_by_module.values()
41-
)
42-
),
43-
default=0,
44-
)
45-
46-
contracts: list[dict[str, bool | str | list[str]]] = []
47-
for (
48-
layered_architecture_by_module
49-
) in layered_architecture_by_module_by_root_package.values():
50-
for module, layered_architecture in layered_architecture_by_module.items():
51-
# We only skip if we have at least one contract.
52-
if len(contracts) > 0 and (
53-
(
54-
# Skip if the contract isn't big enough to be notable.
55-
layered_architecture.module_count()
56-
< IMPORT_LINTER_CONTRACT_MIN_MODULE_COUNT
57-
)
58-
and
59-
# We have waited until we have finished a complete depth level
60-
# (e.g. we have done all of a.b, a.c, and a.d so we won't go on to
61-
# a.b.e)
62-
module.count(".") > min_depth
63-
):
64-
continue
65-
66-
layers: list[str] = []
67-
for layer in layered_architecture.layers:
68-
layers.append(" | ".join(sorted(layer)))
69-
70-
contract = {
71-
"name": module,
72-
"type": "layers",
73-
"layers": layers,
74-
"containers": [module],
75-
"exhaustive": True,
76-
}
77-
78-
if layered_architecture.excluded:
79-
contract["exhaustive_ignores"] = sorted(
80-
layered_architecture.excluded
81-
)
82-
83-
contracts.append(contract)
84-
85-
if not contracts:
86-
raise AssertionError
87-
88-
def get_root_packages() -> list[str] | NoConfigValue:
89-
# There are two configuration items which are very similar:
90-
# root_packages = ["usethis"] # noqa: ERA001
91-
# root_package = "usethis" # noqa: ERA001
92-
# Maybe at a later point we can abstract this case of variant config
93-
# into ConfigEntry but it seems premautre, so for now for Import Linter
94-
# we manually check this case. This might give somewhat reduced performance,
95-
# perhaps.
96-
if self._is_root_package_singular():
97-
return NoConfigValue()
98-
return list(layered_architecture_by_module_by_root_package.keys())
99-
100-
# We're only going to add the INI contracts if there aren't already any
101-
# contracts, so we need to check if there are any contracts.
102-
are_active_ini_contracts = self._are_active_ini_contracts()
103-
104-
ini_contracts_config_items = []
105-
for idx, contract in enumerate(contracts):
106-
if are_active_ini_contracts:
107-
continue
108-
109-
# Cast bools to strings for INI files
110-
ini_contract = contract.copy()
111-
ini_contract["exhaustive"] = str(ini_contract["exhaustive"])
112-
113-
ini_contracts_config_items.append(
114-
ConfigItem(
115-
description=f"Itemized Contract {idx} (INI)",
116-
root={
117-
Path("setup.cfg"): ConfigEntry(
118-
keys=[f"importlinter:contract:{idx}"],
119-
get_value=lambda c=ini_contract: c,
120-
),
121-
Path(".importlinter"): ConfigEntry(
122-
keys=[f"importlinter:contract:{idx}"],
123-
get_value=lambda c=ini_contract: c,
124-
),
125-
},
126-
applies_to_all=False,
127-
)
128-
)
129-
130-
return ConfigSpec(
131-
file_manager_by_relative_path=self._get_file_manager_by_relative_path(),
132-
resolution=self._get_resolution(),
133-
config_items=[
134-
ConfigItem(
135-
description="Overall config",
136-
root={
137-
Path("setup.cfg"): ConfigEntry(keys=["importlinter"]),
138-
Path(".importlinter"): ConfigEntry(keys=["importlinter"]),
139-
Path("pyproject.toml"): ConfigEntry(
140-
keys=["tool", "importlinter"]
141-
),
142-
},
143-
),
144-
ConfigItem(
145-
description="Root packages",
146-
root={
147-
Path("setup.cfg"): ConfigEntry(
148-
keys=["importlinter", "root_packages"],
149-
get_value=get_root_packages,
150-
),
151-
Path(".importlinter"): ConfigEntry(
152-
keys=["importlinter", "root_packages"],
153-
get_value=get_root_packages,
154-
),
155-
Path("pyproject.toml"): ConfigEntry(
156-
keys=["tool", "importlinter", "root_packages"],
157-
get_value=get_root_packages,
158-
),
159-
},
160-
),
161-
ConfigItem(
162-
description="Listed Contracts",
163-
root={
164-
Path("pyproject.toml"): ConfigEntry(
165-
keys=["tool", "importlinter", "contracts"],
166-
get_value=lambda: contracts,
167-
),
168-
# N.B. these INI sections are added via
169-
# `ini_contracts_config_items`
170-
# but there might be others so we still need to declare they
171-
# are associated with this tool based on regex.
172-
Path(".importlinter"): ConfigEntry(
173-
keys=[re.compile("importlinter:contract:.*")]
174-
),
175-
Path(".importlinter"): ConfigEntry(
176-
keys=[re.compile("importlinter:contract:.*")]
177-
),
178-
},
179-
applies_to_all=False,
180-
),
181-
*ini_contracts_config_items,
182-
],
183-
)
184-
185-
def _are_active_ini_contracts(self) -> bool:
186-
# Consider active config manager, and see if there's a matching regex
187-
# for the contract in the INI file.
188-
(file_manager,) = self._get_active_config_file_managers_from_resolution(
189-
self._get_resolution(),
190-
file_manager_by_relative_path=self._get_file_manager_by_relative_path(),
191-
)
192-
if not isinstance(file_manager, INIFileManager):
193-
return False
194-
return [re.compile("importlinter:contract:.*")] in file_manager
195-
196-
def _is_root_package_singular(self) -> bool:
197-
(file_manager,) = self._get_active_config_file_managers_from_resolution(
198-
self._get_resolution(),
199-
file_manager_by_relative_path=self._get_file_manager_by_relative_path(),
200-
)
201-
if isinstance(file_manager, PyprojectTOMLManager):
202-
return ["tool", "importlinter", "root_package"] in file_manager
203-
elif isinstance(file_manager, SetupCFGManager | DotImportLinterManager):
204-
return ["importlinter", "root_package"] in file_manager
205-
else:
206-
msg = f"Unsupported file manager: '{file_manager}'."
207-
raise NotImplementedError(msg)
208-
20916
def is_used(self) -> bool:
21017
"""Check if the Import Linter tool is used in the project."""
21118
# We suppress the warning about assumptions regarding the package name.

0 commit comments

Comments
 (0)