grimp 3.2__cp312-none-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- grimp/__init__.py +17 -0
- grimp/_rustgrimp.cp312-win32.pyd +0 -0
- grimp/adaptors/__init__.py +0 -0
- grimp/adaptors/_layers.py +174 -0
- grimp/adaptors/caching.py +286 -0
- grimp/adaptors/filesystem.py +46 -0
- grimp/adaptors/graph.py +463 -0
- grimp/adaptors/importscanner.py +390 -0
- grimp/adaptors/modulefinder.py +109 -0
- grimp/adaptors/packagefinder.py +53 -0
- grimp/adaptors/timing.py +8 -0
- grimp/algorithms/__init__.py +0 -0
- grimp/algorithms/shortest_path.py +143 -0
- grimp/application/__init__.py +0 -0
- grimp/application/config.py +22 -0
- grimp/application/ports/__init__.py +0 -0
- grimp/application/ports/caching.py +61 -0
- grimp/application/ports/filesystem.py +82 -0
- grimp/application/ports/graph.py +330 -0
- grimp/application/ports/importscanner.py +46 -0
- grimp/application/ports/modulefinder.py +39 -0
- grimp/application/ports/packagefinder.py +11 -0
- grimp/application/ports/timing.py +46 -0
- grimp/application/usecases.py +174 -0
- grimp/domain/__init__.py +0 -0
- grimp/domain/analysis.py +100 -0
- grimp/domain/valueobjects.py +106 -0
- grimp/exceptions.py +63 -0
- grimp/helpers.py +6 -0
- grimp/main.py +21 -0
- grimp/py.typed +0 -0
- grimp-3.2.dist-info/METADATA +157 -0
- grimp-3.2.dist-info/RECORD +36 -0
- grimp-3.2.dist-info/WHEEL +4 -0
- grimp-3.2.dist-info/license_files/AUTHORS.rst +9 -0
- grimp-3.2.dist-info/license_files/LICENSE +21 -0
grimp/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__version__ = "3.2"
|
|
2
|
+
|
|
3
|
+
from .application.ports.graph import DetailedImport, ImportGraph
|
|
4
|
+
from .domain.analysis import PackageDependency, Route
|
|
5
|
+
from .domain.valueobjects import DirectImport, Module, Layer
|
|
6
|
+
from .main import build_graph
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Module",
|
|
10
|
+
"DetailedImport",
|
|
11
|
+
"DirectImport",
|
|
12
|
+
"ImportGraph",
|
|
13
|
+
"PackageDependency",
|
|
14
|
+
"Route",
|
|
15
|
+
"build_graph",
|
|
16
|
+
"Layer",
|
|
17
|
+
]
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Iterator, Sequence, TypedDict
|
|
5
|
+
|
|
6
|
+
from grimp import Route
|
|
7
|
+
from grimp import _rustgrimp as rust # type: ignore[attr-defined]
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from grimp.adaptors.graph import ImportGraph
|
|
11
|
+
|
|
12
|
+
from grimp.domain.analysis import PackageDependency
|
|
13
|
+
from grimp.exceptions import NoSuchContainer
|
|
14
|
+
from grimp.domain.valueobjects import Layer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_layers(layers: Sequence[Layer | str | set[str]]) -> tuple[Layer, ...]:
|
|
18
|
+
"""
|
|
19
|
+
Convert the passed raw `layers` into `Layer`s.
|
|
20
|
+
"""
|
|
21
|
+
out_layers = []
|
|
22
|
+
for layer in layers:
|
|
23
|
+
if isinstance(layer, Layer):
|
|
24
|
+
out_layers.append(layer)
|
|
25
|
+
elif isinstance(layer, str):
|
|
26
|
+
out_layers.append(Layer(layer, independent=True))
|
|
27
|
+
else:
|
|
28
|
+
out_layers.append(Layer(*tuple(layer), independent=True))
|
|
29
|
+
return tuple(out_layers)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_illegal_dependencies(
|
|
33
|
+
graph: ImportGraph,
|
|
34
|
+
layers: Sequence[Layer],
|
|
35
|
+
containers: set[str],
|
|
36
|
+
) -> set[PackageDependency]:
|
|
37
|
+
"""
|
|
38
|
+
Find dependencies that don't conform to the supplied layered architecture.
|
|
39
|
+
|
|
40
|
+
See ImportGraph.find_illegal_dependencies_for_layers.
|
|
41
|
+
|
|
42
|
+
The only difference between this and the method is that the containers passed in
|
|
43
|
+
is already a (potentially empty) set.
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
rust_package_dependency_tuple = rust.find_illegal_dependencies(
|
|
47
|
+
levels=tuple(
|
|
48
|
+
{"layers": layer.module_tails, "independent": layer.independent}
|
|
49
|
+
for layer in layers
|
|
50
|
+
),
|
|
51
|
+
containers=set(containers),
|
|
52
|
+
importeds_by_importer=graph._importeds_by_importer,
|
|
53
|
+
)
|
|
54
|
+
except rust.NoSuchContainer as e:
|
|
55
|
+
raise NoSuchContainer(str(e))
|
|
56
|
+
|
|
57
|
+
rust_package_dependencies = _dependencies_from_tuple(rust_package_dependency_tuple)
|
|
58
|
+
return rust_package_dependencies
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _RustRoute(TypedDict):
|
|
62
|
+
heads: frozenset[str]
|
|
63
|
+
middle: tuple[str, ...]
|
|
64
|
+
tails: frozenset[str]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _RustPackageDependency(TypedDict):
|
|
68
|
+
importer: str
|
|
69
|
+
imported: str
|
|
70
|
+
routes: tuple[_RustRoute, ...]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _dependencies_from_tuple(
|
|
74
|
+
rust_package_dependency_tuple: tuple[_RustPackageDependency, ...]
|
|
75
|
+
) -> set[PackageDependency]:
|
|
76
|
+
return {
|
|
77
|
+
PackageDependency(
|
|
78
|
+
imported=dep_dict["imported"],
|
|
79
|
+
importer=dep_dict["importer"],
|
|
80
|
+
routes=frozenset(
|
|
81
|
+
{
|
|
82
|
+
Route(
|
|
83
|
+
heads=route_dict["heads"],
|
|
84
|
+
middle=route_dict["middle"],
|
|
85
|
+
tails=route_dict["tails"],
|
|
86
|
+
)
|
|
87
|
+
for route_dict in dep_dict["routes"]
|
|
88
|
+
}
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
for dep_dict in rust_package_dependency_tuple
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class _Module:
|
|
96
|
+
"""
|
|
97
|
+
A Python module.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(self, name: str) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Args:
|
|
103
|
+
name: The fully qualified name of a Python module, e.g. 'package.foo.bar'.
|
|
104
|
+
"""
|
|
105
|
+
self.name = name
|
|
106
|
+
|
|
107
|
+
def __str__(self) -> str:
|
|
108
|
+
return self.name
|
|
109
|
+
|
|
110
|
+
def __eq__(self, other: Any) -> bool:
|
|
111
|
+
if isinstance(other, self.__class__):
|
|
112
|
+
return hash(self) == hash(other)
|
|
113
|
+
else:
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def __hash__(self) -> int:
|
|
117
|
+
return hash(str(self))
|
|
118
|
+
|
|
119
|
+
def is_descendant_of(self, module: "_Module") -> bool:
|
|
120
|
+
return self.name.startswith(f"{module.name}.")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class _Link:
|
|
125
|
+
importer: str
|
|
126
|
+
imported: str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# A chain of modules, each of which imports the next.
|
|
130
|
+
if TYPE_CHECKING:
|
|
131
|
+
# TODO: remove TYPE_CHECKING conditional once on Python 3.9.
|
|
132
|
+
_Chain = tuple[str, ...]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _generate_module_permutations(
|
|
136
|
+
graph: ImportGraph,
|
|
137
|
+
layers: Sequence[str],
|
|
138
|
+
containers: set[str],
|
|
139
|
+
) -> Iterator[tuple[_Module, _Module, str | None]]:
|
|
140
|
+
"""
|
|
141
|
+
Return all possible combinations of higher level and lower level modules, in pairs.
|
|
142
|
+
|
|
143
|
+
Each pair of modules consists of immediate children of two different layers. The first
|
|
144
|
+
module is in a layer higher than the layer of the second module. This means the first
|
|
145
|
+
module is allowed to import the second, but not the other way around.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
module_in_higher_layer, module_in_lower_layer, container
|
|
149
|
+
"""
|
|
150
|
+
# If there are no containers, we still want to run the loop once.
|
|
151
|
+
quasi_containers = containers or [None]
|
|
152
|
+
|
|
153
|
+
for container in quasi_containers:
|
|
154
|
+
for index, higher_layer in enumerate(layers):
|
|
155
|
+
higher_layer_module = _module_from_layer(higher_layer, container)
|
|
156
|
+
|
|
157
|
+
if higher_layer_module.name not in graph.modules:
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
for lower_layer in layers[index + 1 :]:
|
|
161
|
+
lower_layer_module = _module_from_layer(lower_layer, container)
|
|
162
|
+
|
|
163
|
+
if lower_layer_module.name not in graph.modules:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
yield higher_layer_module, lower_layer_module, container
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _module_from_layer(layer: str, container: str | None = None) -> _Module:
|
|
170
|
+
if container:
|
|
171
|
+
name = ".".join([container, layer])
|
|
172
|
+
else:
|
|
173
|
+
name = layer
|
|
174
|
+
return _Module(name)
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, List, Optional, Set, Tuple, Type
|
|
5
|
+
|
|
6
|
+
from grimp.application.ports.filesystem import AbstractFileSystem
|
|
7
|
+
from grimp.application.ports.modulefinder import FoundPackage, ModuleFile
|
|
8
|
+
from grimp.domain.valueobjects import DirectImport, Module
|
|
9
|
+
|
|
10
|
+
from ..application.ports.caching import Cache as AbstractCache
|
|
11
|
+
from ..application.ports.caching import CacheMiss
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
PrimitiveFormat = Dict[str, List[Tuple[str, Optional[int], str]]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CacheFileNamer:
|
|
18
|
+
@classmethod
|
|
19
|
+
def make_meta_file_name(cls, found_package: FoundPackage) -> str:
|
|
20
|
+
return f"{found_package.name}.meta.json"
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def make_data_file_name(
|
|
24
|
+
cls,
|
|
25
|
+
found_packages: Set[FoundPackage],
|
|
26
|
+
include_external_packages: bool,
|
|
27
|
+
exclude_type_checking_imports: bool,
|
|
28
|
+
) -> str:
|
|
29
|
+
identifier = cls.make_data_file_unique_string(
|
|
30
|
+
found_packages, include_external_packages, exclude_type_checking_imports
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
bytes_identifier = identifier.encode()
|
|
34
|
+
# Use a hash algorithm with a limited size to avoid cache filenames that are too long
|
|
35
|
+
# the filesystem, which can happen if there are more than a few root packages
|
|
36
|
+
# being analyzed.
|
|
37
|
+
safe_unicode_identifier = hashlib.blake2b(bytes_identifier, digest_size=20).hexdigest()
|
|
38
|
+
return f"{safe_unicode_identifier}.data.json"
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def make_data_file_unique_string(
|
|
42
|
+
cls,
|
|
43
|
+
found_packages: Set[FoundPackage],
|
|
44
|
+
include_external_packages: bool,
|
|
45
|
+
exclude_type_checking_imports: bool,
|
|
46
|
+
) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Construct a unique string that identifies the analysis parameters.
|
|
49
|
+
|
|
50
|
+
Doesn't need to be safe to use for a filename.
|
|
51
|
+
"""
|
|
52
|
+
package_names = (p.name for p in found_packages)
|
|
53
|
+
csv_packages = ",".join(sorted(package_names))
|
|
54
|
+
include_external_packages_option = ":external" if include_external_packages else ""
|
|
55
|
+
exclude_type_checking_imports_option = (
|
|
56
|
+
":no_type_checking" if exclude_type_checking_imports else ""
|
|
57
|
+
)
|
|
58
|
+
return (
|
|
59
|
+
csv_packages + include_external_packages_option + exclude_type_checking_imports_option
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Cache(AbstractCache):
|
|
64
|
+
DEFAULT_CACHE_DIR = ".grimp_cache"
|
|
65
|
+
|
|
66
|
+
def __init__(self, *args, namer: Type[CacheFileNamer], **kwargs) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Don't instantiate Cache directly; use Cache.setup().
|
|
69
|
+
"""
|
|
70
|
+
super().__init__(*args, **kwargs)
|
|
71
|
+
self._mtime_map: Dict[str, float] = {}
|
|
72
|
+
self._data_map: Dict[Module, Set[DirectImport]] = {}
|
|
73
|
+
self._namer = namer
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def setup(
|
|
77
|
+
cls,
|
|
78
|
+
file_system: AbstractFileSystem,
|
|
79
|
+
found_packages: Set[FoundPackage],
|
|
80
|
+
include_external_packages: bool,
|
|
81
|
+
exclude_type_checking_imports: bool = False,
|
|
82
|
+
cache_dir: Optional[str] = None,
|
|
83
|
+
namer: Type[CacheFileNamer] = CacheFileNamer,
|
|
84
|
+
) -> "Cache":
|
|
85
|
+
cache = cls(
|
|
86
|
+
file_system=file_system,
|
|
87
|
+
found_packages=found_packages,
|
|
88
|
+
include_external_packages=include_external_packages,
|
|
89
|
+
exclude_type_checking_imports=exclude_type_checking_imports,
|
|
90
|
+
cache_dir=cls.cache_dir_or_default(cache_dir),
|
|
91
|
+
namer=namer,
|
|
92
|
+
)
|
|
93
|
+
cache._build_mtime_map()
|
|
94
|
+
cache._build_data_map()
|
|
95
|
+
assert cache.cache_dir
|
|
96
|
+
return cache
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def cache_dir_or_default(cls, cache_dir: Optional[str]) -> str:
|
|
100
|
+
return cache_dir or cls.DEFAULT_CACHE_DIR
|
|
101
|
+
|
|
102
|
+
def read_imports(self, module_file: ModuleFile) -> Set[DirectImport]:
|
|
103
|
+
try:
|
|
104
|
+
cached_mtime = self._mtime_map[module_file.module.name]
|
|
105
|
+
except KeyError:
|
|
106
|
+
raise CacheMiss
|
|
107
|
+
if cached_mtime != module_file.mtime:
|
|
108
|
+
raise CacheMiss
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
return self._data_map[module_file.module]
|
|
112
|
+
except KeyError:
|
|
113
|
+
# While we would expect the module to be in here,
|
|
114
|
+
# there's no point in crashing if, for some reason, it's not.
|
|
115
|
+
raise CacheMiss
|
|
116
|
+
|
|
117
|
+
def write(
|
|
118
|
+
self,
|
|
119
|
+
imports_by_module: Dict[Module, Set[DirectImport]],
|
|
120
|
+
) -> None:
|
|
121
|
+
self._write_marker_files_if_not_already_there()
|
|
122
|
+
# Write data file.
|
|
123
|
+
primitives_map: PrimitiveFormat = {}
|
|
124
|
+
for found_package in self.found_packages:
|
|
125
|
+
primitives_map_for_found_package: PrimitiveFormat = {
|
|
126
|
+
module_file.module.name: [
|
|
127
|
+
(
|
|
128
|
+
direct_import.imported.name,
|
|
129
|
+
direct_import.line_number,
|
|
130
|
+
direct_import.line_contents,
|
|
131
|
+
)
|
|
132
|
+
for direct_import in imports_by_module[module_file.module]
|
|
133
|
+
]
|
|
134
|
+
for module_file in found_package.module_files
|
|
135
|
+
}
|
|
136
|
+
primitives_map.update(primitives_map_for_found_package)
|
|
137
|
+
|
|
138
|
+
serialized = json.dumps(primitives_map)
|
|
139
|
+
data_cache_filename = self.file_system.join(
|
|
140
|
+
self.cache_dir,
|
|
141
|
+
self._namer.make_data_file_name(
|
|
142
|
+
found_packages=self.found_packages,
|
|
143
|
+
include_external_packages=self.include_external_packages,
|
|
144
|
+
exclude_type_checking_imports=self.exclude_type_checking_imports,
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
self.file_system.write(data_cache_filename, serialized)
|
|
148
|
+
logger.info(f"Wrote data cache file {data_cache_filename}.")
|
|
149
|
+
|
|
150
|
+
# Write meta files.
|
|
151
|
+
for found_package in self.found_packages:
|
|
152
|
+
meta_filename = self.file_system.join(
|
|
153
|
+
self.cache_dir, self._namer.make_meta_file_name(found_package)
|
|
154
|
+
)
|
|
155
|
+
mtime_map = {
|
|
156
|
+
module_file.module.name: module_file.mtime
|
|
157
|
+
for module_file in found_package.module_files
|
|
158
|
+
}
|
|
159
|
+
serialized_meta = json.dumps(mtime_map)
|
|
160
|
+
self.file_system.write(meta_filename, serialized_meta)
|
|
161
|
+
logger.info(f"Wrote meta cache file {meta_filename}.")
|
|
162
|
+
|
|
163
|
+
def _build_mtime_map(self) -> None:
|
|
164
|
+
self._mtime_map = self._read_mtime_map_files()
|
|
165
|
+
|
|
166
|
+
def _read_mtime_map_files(self) -> Dict[str, float]:
|
|
167
|
+
all_mtimes: Dict[str, float] = {}
|
|
168
|
+
for found_package in self.found_packages:
|
|
169
|
+
all_mtimes.update(self._read_mtime_map_file(found_package))
|
|
170
|
+
return all_mtimes
|
|
171
|
+
|
|
172
|
+
def _read_mtime_map_file(self, found_package: FoundPackage) -> Dict[str, float]:
|
|
173
|
+
meta_cache_filename = self.file_system.join(
|
|
174
|
+
self.cache_dir, self._namer.make_meta_file_name(found_package)
|
|
175
|
+
)
|
|
176
|
+
try:
|
|
177
|
+
serialized = self.file_system.read(meta_cache_filename)
|
|
178
|
+
except FileNotFoundError:
|
|
179
|
+
logger.info(f"No cache file: {meta_cache_filename}.")
|
|
180
|
+
return {}
|
|
181
|
+
try:
|
|
182
|
+
deserialized = json.loads(serialized)
|
|
183
|
+
logger.info(f"Used cache meta file {meta_cache_filename}.")
|
|
184
|
+
return deserialized
|
|
185
|
+
except json.JSONDecodeError:
|
|
186
|
+
logger.warning(f"Could not use corrupt cache file {meta_cache_filename}.")
|
|
187
|
+
return {}
|
|
188
|
+
|
|
189
|
+
def _build_data_map(self) -> None:
|
|
190
|
+
self._data_map = self._read_data_map_file()
|
|
191
|
+
|
|
192
|
+
def _read_data_map_file(self) -> Dict[Module, Set[DirectImport]]:
|
|
193
|
+
data_cache_filename = self.file_system.join(
|
|
194
|
+
self.cache_dir,
|
|
195
|
+
self._namer.make_data_file_name(
|
|
196
|
+
found_packages=self.found_packages,
|
|
197
|
+
include_external_packages=self.include_external_packages,
|
|
198
|
+
exclude_type_checking_imports=self.exclude_type_checking_imports,
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
try:
|
|
202
|
+
serialized = self.file_system.read(data_cache_filename)
|
|
203
|
+
except FileNotFoundError:
|
|
204
|
+
logger.info(f"No cache file: {data_cache_filename}.")
|
|
205
|
+
return {}
|
|
206
|
+
|
|
207
|
+
# Deserialize to primitives.
|
|
208
|
+
try:
|
|
209
|
+
deserialized_json = json.loads(serialized)
|
|
210
|
+
logger.info(f"Used cache data file {data_cache_filename}.")
|
|
211
|
+
except json.JSONDecodeError:
|
|
212
|
+
logger.warning(f"Could not use corrupt cache file {data_cache_filename}.")
|
|
213
|
+
return {}
|
|
214
|
+
|
|
215
|
+
primitives_map: PrimitiveFormat = self._to_primitives_data_map(deserialized_json)
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
Module(name=name): {
|
|
219
|
+
DirectImport(
|
|
220
|
+
importer=Module(name),
|
|
221
|
+
imported=Module(import_data[0]),
|
|
222
|
+
line_number=int(import_data[1]), # type: ignore
|
|
223
|
+
line_contents=import_data[2],
|
|
224
|
+
)
|
|
225
|
+
for import_data in imports_data
|
|
226
|
+
}
|
|
227
|
+
for name, imports_data in primitives_map.items()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
def _build_data_cache_filename(self, found_package: FoundPackage) -> str:
|
|
231
|
+
return self.file_system.join(self.cache_dir, f"{found_package.name}.data.json")
|
|
232
|
+
|
|
233
|
+
def _to_primitives_data_map(self, deserialized_json: object) -> PrimitiveFormat:
|
|
234
|
+
"""
|
|
235
|
+
Convert the deserialized json from a data file to a narrower schema.
|
|
236
|
+
|
|
237
|
+
Anything that doesn't fit the schema will be removed.
|
|
238
|
+
"""
|
|
239
|
+
if not isinstance(deserialized_json, dict):
|
|
240
|
+
return {}
|
|
241
|
+
|
|
242
|
+
primitives_map: PrimitiveFormat = {}
|
|
243
|
+
|
|
244
|
+
for key, value in deserialized_json.items():
|
|
245
|
+
if not isinstance(key, str):
|
|
246
|
+
continue
|
|
247
|
+
if not isinstance(value, list):
|
|
248
|
+
continue
|
|
249
|
+
primitive_imports = []
|
|
250
|
+
for deserialized_import in value:
|
|
251
|
+
try:
|
|
252
|
+
[imported, line_number, line_contents] = deserialized_import
|
|
253
|
+
except ValueError:
|
|
254
|
+
continue
|
|
255
|
+
try:
|
|
256
|
+
primitive_imports.append(
|
|
257
|
+
(
|
|
258
|
+
str(imported),
|
|
259
|
+
int(line_number) if line_number else None,
|
|
260
|
+
str(line_contents),
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
except TypeError:
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
primitives_map[key] = primitive_imports
|
|
267
|
+
|
|
268
|
+
return primitives_map
|
|
269
|
+
|
|
270
|
+
def _write_marker_files_if_not_already_there(self) -> None:
|
|
271
|
+
marker_files_info = (
|
|
272
|
+
(".gitignore", "# Automatically created by Grimp.\n*"),
|
|
273
|
+
(
|
|
274
|
+
"CACHEDIR.TAG",
|
|
275
|
+
(
|
|
276
|
+
"Signature: 8a477f597d28d172789f06886806bc55\n"
|
|
277
|
+
"# This file is a cache directory tag automatically created by Grimp.\n"
|
|
278
|
+
"# For information about cache directory tags see https://bford.info/cachedir/"
|
|
279
|
+
),
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
for filename, contents in marker_files_info:
|
|
284
|
+
full_filename = self.file_system.join(self.cache_dir, filename)
|
|
285
|
+
if not self.file_system.exists(full_filename):
|
|
286
|
+
self.file_system.write(full_filename, contents)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tokenize
|
|
3
|
+
from typing import Iterator, List, Tuple
|
|
4
|
+
|
|
5
|
+
from grimp.application.ports.filesystem import AbstractFileSystem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileSystem(AbstractFileSystem):
|
|
9
|
+
"""
|
|
10
|
+
Abstraction around file system calls.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def sep(self) -> str:
|
|
15
|
+
return os.sep
|
|
16
|
+
|
|
17
|
+
def dirname(self, filename: str) -> str:
|
|
18
|
+
return os.path.dirname(filename)
|
|
19
|
+
|
|
20
|
+
def walk(self, directory_name: str) -> Iterator[Tuple[str, List[str], List[str]]]:
|
|
21
|
+
yield from os.walk(directory_name)
|
|
22
|
+
|
|
23
|
+
def join(self, *components: str) -> str:
|
|
24
|
+
return os.path.join(*components)
|
|
25
|
+
|
|
26
|
+
def split(self, file_name: str) -> Tuple[str, str]:
|
|
27
|
+
return os.path.split(file_name)
|
|
28
|
+
|
|
29
|
+
def read(self, file_name: str) -> str:
|
|
30
|
+
# Use tokenize.open to give us a better chance of successfully decoding
|
|
31
|
+
# source code in a non-ascii compatible encoding.
|
|
32
|
+
with tokenize.open(file_name) as file:
|
|
33
|
+
return file.read()
|
|
34
|
+
|
|
35
|
+
def exists(self, file_name: str) -> bool:
|
|
36
|
+
return os.path.isfile(file_name)
|
|
37
|
+
|
|
38
|
+
def get_mtime(self, file_name: str) -> float:
|
|
39
|
+
return os.path.getmtime(file_name)
|
|
40
|
+
|
|
41
|
+
def write(self, file_name: str, contents: str) -> None:
|
|
42
|
+
dirname = os.path.dirname(file_name)
|
|
43
|
+
if not os.path.exists(dirname):
|
|
44
|
+
os.makedirs(dirname)
|
|
45
|
+
with open(file_name, "w") as file:
|
|
46
|
+
print(contents, file=file)
|