|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | | -import re |
4 | | -from pathlib import Path |
5 | 3 | from typing import TYPE_CHECKING |
6 | 4 |
|
7 | 5 | from usethis._config import usethis_config |
8 | | -from usethis._config_file import DotImportLinterManager |
9 | 6 | 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 |
13 | 7 | from usethis._tool.base import Tool |
14 | | -from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec, NoConfigValue |
15 | 8 | from usethis._tool.impl.base.ruff import RuffTool |
16 | 9 | from usethis._tool.impl.spec.import_linter import ImportLinterToolSpec |
17 | 10 |
|
18 | 11 | if TYPE_CHECKING: |
19 | 12 | from usethis._tool.rule import Rule |
20 | 13 |
|
21 | | -IMPORT_LINTER_CONTRACT_MIN_MODULE_COUNT = 3 |
22 | | - |
23 | 14 |
|
24 | 15 | 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 | | - |
209 | 16 | def is_used(self) -> bool: |
210 | 17 | """Check if the Import Linter tool is used in the project.""" |
211 | 18 | # We suppress the warning about assumptions regarding the package name. |
|
0 commit comments