grimp 3.12__tar.gz → 3.13__tar.gz
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-3.12 → grimp-3.13}/PKG-INFO +12 -6
- {grimp-3.12 → grimp-3.13}/README.rst +10 -4
- {grimp-3.12 → grimp-3.13}/pyproject.toml +3 -3
- grimp-3.13/rust/src/graph/cycle_breakers.rs +249 -0
- {grimp-3.12 → grimp-3.13}/rust/src/graph/direct_import_queries.rs +40 -8
- {grimp-3.12 → grimp-3.13}/rust/src/graph/mod.rs +30 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/__init__.py +2 -2
- {grimp-3.12/src/grimp/application/ports → grimp-3.13/src/grimp/application}/graph.py +176 -64
- {grimp-3.12 → grimp-3.13}/src/grimp/application/usecases.py +1 -1
- {grimp-3.12 → grimp-3.13}/src/grimp/main.py +1 -1
- grimp-3.12/src/grimp/adaptors/graph.py +0 -233
- {grimp-3.12 → grimp-3.13}/rust/.cargo/config.toml +0 -0
- {grimp-3.12 → grimp-3.13}/rust/Cargo.lock +0 -0
- {grimp-3.12 → grimp-3.13}/rust/Cargo.toml +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/caching.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/errors.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/exceptions.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/filesystem.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/graph/graph_manipulation.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/graph/hierarchy_queries.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/graph/higher_order_queries.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/graph/import_chain_queries.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/graph/pathfinding.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/import_parsing.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/import_scanning.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/lib.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/module_expressions.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/src/module_finding.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/tests/large.rs +0 -0
- {grimp-3.12 → grimp-3.13}/rust/tests/large_graph.json +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/adaptors/__init__.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/adaptors/caching.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/adaptors/filesystem.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/adaptors/modulefinder.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/adaptors/packagefinder.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/adaptors/timing.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/application/__init__.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/application/config.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/application/ports/__init__.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/application/ports/caching.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/application/ports/filesystem.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/application/ports/modulefinder.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/application/ports/packagefinder.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/application/ports/timing.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/application/scanning.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/domain/__init__.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/domain/analysis.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/domain/valueobjects.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/exceptions.py +0 -0
- {grimp-3.12 → grimp-3.13}/src/grimp/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: grimp
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.13
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: BSD License
|
|
@@ -26,7 +26,7 @@ License: BSD 2-Clause License
|
|
|
26
26
|
Requires-Python: >=3.9
|
|
27
27
|
Description-Content-Type: text/x-rst; charset=UTF-8
|
|
28
28
|
Project-URL: Documentation, https://grimp.readthedocs.io/
|
|
29
|
-
Project-URL: Source-code, https://github.com/
|
|
29
|
+
Project-URL: Source-code, https://github.com/python-grimp/grimp/
|
|
30
30
|
|
|
31
31
|
=====
|
|
32
32
|
Grimp
|
|
@@ -39,13 +39,19 @@ Grimp
|
|
|
39
39
|
:alt: Python versions
|
|
40
40
|
:target: https://pypi.org/project/grimp/
|
|
41
41
|
|
|
42
|
-
.. image:: https://github.com/
|
|
43
|
-
:target: https://github.com/
|
|
42
|
+
.. image:: https://github.com/python-grimp/grimp/actions/workflows/main.yml/badge.svg
|
|
43
|
+
:target: https://github.com/python-grimp/grimp/actions?workflow=CI
|
|
44
44
|
:alt: CI Status
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
|
|
47
|
+
:target: https://codspeed.io/python-grimp/grimp?utm_source=badge
|
|
48
|
+
:alt: Codspeed
|
|
49
|
+
|
|
50
|
+
.. image:: https://img.shields.io/badge/License-BSD_2--Clause-orange.svg
|
|
51
|
+
:target: https://opensource.org/licenses/BSD-2-Clause
|
|
52
|
+
:alt: BSD license
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
Builds a queryable graph of the imports within one or more Python packages.
|
|
49
55
|
|
|
50
56
|
Quick start
|
|
51
57
|
-----------
|
|
@@ -9,13 +9,19 @@ Grimp
|
|
|
9
9
|
:alt: Python versions
|
|
10
10
|
:target: https://pypi.org/project/grimp/
|
|
11
11
|
|
|
12
|
-
.. image:: https://github.com/
|
|
13
|
-
:target: https://github.com/
|
|
12
|
+
.. image:: https://github.com/python-grimp/grimp/actions/workflows/main.yml/badge.svg
|
|
13
|
+
:target: https://github.com/python-grimp/grimp/actions?workflow=CI
|
|
14
14
|
:alt: CI Status
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
|
|
17
|
+
:target: https://codspeed.io/python-grimp/grimp?utm_source=badge
|
|
18
|
+
:alt: Codspeed
|
|
19
|
+
|
|
20
|
+
.. image:: https://img.shields.io/badge/License-BSD_2--Clause-orange.svg
|
|
21
|
+
:target: https://opensource.org/licenses/BSD-2-Clause
|
|
22
|
+
:alt: BSD license
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
Builds a queryable graph of the imports within one or more Python packages.
|
|
19
25
|
|
|
20
26
|
Quick start
|
|
21
27
|
-----------
|
|
@@ -8,7 +8,7 @@ module-name = "grimp._rustgrimp"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "grimp"
|
|
11
|
-
version = "3.
|
|
11
|
+
version = "3.13"
|
|
12
12
|
license = {text = "BSD 2-Clause License"}
|
|
13
13
|
description = "Builds a queryable graph of the imports within one or more Python packages."
|
|
14
14
|
authors = [
|
|
@@ -40,7 +40,7 @@ readme = "README.rst"
|
|
|
40
40
|
|
|
41
41
|
[project.urls]
|
|
42
42
|
Documentation = "https://grimp.readthedocs.io/"
|
|
43
|
-
Source-code = "https://github.com/
|
|
43
|
+
Source-code = "https://github.com/python-grimp/grimp/"
|
|
44
44
|
|
|
45
45
|
[dependency-groups]
|
|
46
46
|
dev = [
|
|
@@ -67,7 +67,7 @@ docs = [
|
|
|
67
67
|
"sphinx-rtd-theme>=3.0.2",
|
|
68
68
|
]
|
|
69
69
|
benchmark_ci = [
|
|
70
|
-
"pytest-codspeed>=
|
|
70
|
+
"pytest-codspeed>=4.2.0",
|
|
71
71
|
]
|
|
72
72
|
|
|
73
73
|
[tool.setuptools]
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
use crate::errors::GrimpResult;
|
|
2
|
+
|
|
3
|
+
use crate::graph::{Graph, ModuleToken};
|
|
4
|
+
use rustc_hash::{FxHashMap, FxHashSet};
|
|
5
|
+
use slotmap::SecondaryMap;
|
|
6
|
+
use std::cmp::Ordering;
|
|
7
|
+
|
|
8
|
+
impl Graph {
|
|
9
|
+
pub fn nominate_cycle_breakers(
|
|
10
|
+
&self,
|
|
11
|
+
package: ModuleToken,
|
|
12
|
+
) -> GrimpResult<FxHashSet<(ModuleToken, ModuleToken)>> {
|
|
13
|
+
// Get children of this package.
|
|
14
|
+
let children = match self.module_children.get(package) {
|
|
15
|
+
Some(children) => children,
|
|
16
|
+
None => return Ok(FxHashSet::default()),
|
|
17
|
+
};
|
|
18
|
+
if children.len() < 2 {
|
|
19
|
+
return Ok(FxHashSet::default());
|
|
20
|
+
}
|
|
21
|
+
let (orig_imports, orig_reverse_imports, edge_weights) =
|
|
22
|
+
self.build_graph_maps_of_children(children);
|
|
23
|
+
|
|
24
|
+
// Make a copy of the graph. We'll iteratively remove nodes from this.
|
|
25
|
+
let mut working_imports = orig_imports.clone();
|
|
26
|
+
let mut working_reverse_imports = orig_reverse_imports.clone();
|
|
27
|
+
|
|
28
|
+
// Iteratively extract sources into a vec.
|
|
29
|
+
let mut sources: Vec<ModuleToken> = vec![];
|
|
30
|
+
|
|
31
|
+
loop {
|
|
32
|
+
let current_sources: Vec<ModuleToken> = working_reverse_imports
|
|
33
|
+
.iter()
|
|
34
|
+
.filter(|(_imported, importers)| importers.is_empty())
|
|
35
|
+
.map(|(source, _)| source)
|
|
36
|
+
.collect();
|
|
37
|
+
|
|
38
|
+
if current_sources.is_empty() {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
sources.extend(current_sources);
|
|
43
|
+
// Remove sources from graph, then try again for another round.
|
|
44
|
+
for source in sources.iter() {
|
|
45
|
+
remove_module_from_graph(
|
|
46
|
+
source,
|
|
47
|
+
&mut working_imports,
|
|
48
|
+
&mut working_reverse_imports,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Iteratively extract sinks into a vec.
|
|
54
|
+
let mut sinks: Vec<ModuleToken> = vec![];
|
|
55
|
+
loop {
|
|
56
|
+
let new_sinks: Vec<ModuleToken> = working_imports
|
|
57
|
+
.iter()
|
|
58
|
+
.filter(|(_importer, importeds)| importeds.is_empty())
|
|
59
|
+
.map(|(sink, _)| sink)
|
|
60
|
+
.collect();
|
|
61
|
+
|
|
62
|
+
if new_sinks.is_empty() {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Remove sinks from graph, then try again for another round.
|
|
67
|
+
for sink in new_sinks.iter() {
|
|
68
|
+
remove_module_from_graph(sink, &mut working_imports, &mut working_reverse_imports);
|
|
69
|
+
}
|
|
70
|
+
// Add the new sinks to the beginning of sinks - these need to appear earlier in
|
|
71
|
+
// the overall order, as they depend on the sinks previously found.
|
|
72
|
+
sinks.splice(0..0, new_sinks);
|
|
73
|
+
//sinks.extend(new_sinks);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Iteratively extract remaining nodes, based on out-degree - in-degree.
|
|
77
|
+
let mut middle: Vec<ModuleToken> = vec![];
|
|
78
|
+
loop {
|
|
79
|
+
if working_imports.is_empty() {
|
|
80
|
+
// We've finished ordering the nodes.
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Initialize with the first node.
|
|
85
|
+
let mut node_with_highest_difference: Option<ModuleToken> = None;
|
|
86
|
+
let mut highest_difference_so_far: Option<isize> = None;
|
|
87
|
+
|
|
88
|
+
for (candidate, _) in working_imports.iter() {
|
|
89
|
+
let difference = calculate_degree_difference(
|
|
90
|
+
&candidate,
|
|
91
|
+
&working_imports,
|
|
92
|
+
&working_reverse_imports,
|
|
93
|
+
&edge_weights,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
match node_with_highest_difference {
|
|
97
|
+
Some(incumbent) => {
|
|
98
|
+
match difference.cmp(&highest_difference_so_far.unwrap()) {
|
|
99
|
+
Ordering::Greater => {
|
|
100
|
+
highest_difference_so_far = Some(difference);
|
|
101
|
+
node_with_highest_difference = Some(candidate);
|
|
102
|
+
}
|
|
103
|
+
Ordering::Equal => {
|
|
104
|
+
// Tie breaker - choose the earlier one alphabetically.
|
|
105
|
+
let incumbent_name = self.get_module(incumbent).unwrap().name();
|
|
106
|
+
let candidate_name = self.get_module(candidate).unwrap().name();
|
|
107
|
+
|
|
108
|
+
if candidate_name < incumbent_name {
|
|
109
|
+
highest_difference_so_far = Some(difference);
|
|
110
|
+
node_with_highest_difference = Some(candidate);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
Ordering::Less => {}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
None => {
|
|
117
|
+
node_with_highest_difference = Some(candidate);
|
|
118
|
+
highest_difference_so_far = Some(difference);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Extract node with highest difference to one of the two middle vecs.
|
|
124
|
+
let node_with_highest_difference = node_with_highest_difference.unwrap();
|
|
125
|
+
|
|
126
|
+
// Prioritize high out-degree.
|
|
127
|
+
middle.push(node_with_highest_difference);
|
|
128
|
+
remove_module_from_graph(
|
|
129
|
+
&node_with_highest_difference,
|
|
130
|
+
&mut working_imports,
|
|
131
|
+
&mut working_reverse_imports,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if !working_reverse_imports.is_empty() {
|
|
135
|
+
panic!("Expected reverse imports also to be empty.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Combine sources + ordered + sinks.
|
|
139
|
+
let full_ordering: Vec<_> = sources.into_iter().chain(middle).chain(sinks).collect();
|
|
140
|
+
// Iterate through all edges in original graph.
|
|
141
|
+
// If the edge points leftwards in the full ordering, it is a circuit breaker.
|
|
142
|
+
let mut package_cycle_breakers: Vec<(ModuleToken, ModuleToken)> = vec![];
|
|
143
|
+
for (importer, importeds) in orig_imports.iter() {
|
|
144
|
+
for imported in importeds.iter() {
|
|
145
|
+
let importer_position = get_module_position(&full_ordering, &importer).unwrap();
|
|
146
|
+
let imported_position = get_module_position(&full_ordering, imported).unwrap();
|
|
147
|
+
if imported_position < importer_position {
|
|
148
|
+
package_cycle_breakers.push((importer, *imported));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Expand the package cycle breakers to the specific imports from the unsquashed graph.
|
|
154
|
+
let mut cycle_breakers = FxHashSet::default();
|
|
155
|
+
for (importer, imported) in package_cycle_breakers {
|
|
156
|
+
cycle_breakers.extend(
|
|
157
|
+
self.find_direct_imports_between(importer, imported, true)
|
|
158
|
+
.unwrap(),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
Ok(cycle_breakers)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// These maps represent the local graph. Each module appears as a key in the maps,
|
|
166
|
+
// with empty sets if they have no imports to / from.
|
|
167
|
+
// They are denormalized into two maps for fast lookups.
|
|
168
|
+
#[allow(clippy::type_complexity)]
|
|
169
|
+
fn build_graph_maps_of_children(
|
|
170
|
+
&self,
|
|
171
|
+
children: &FxHashSet<ModuleToken>,
|
|
172
|
+
) -> (
|
|
173
|
+
SecondaryMap<ModuleToken, FxHashSet<ModuleToken>>, // Imports
|
|
174
|
+
SecondaryMap<ModuleToken, FxHashSet<ModuleToken>>, // Reverse imports
|
|
175
|
+
FxHashMap<(ModuleToken, ModuleToken), usize>, // Weights
|
|
176
|
+
) {
|
|
177
|
+
let mut orig_imports: SecondaryMap<ModuleToken, FxHashSet<ModuleToken>> =
|
|
178
|
+
SecondaryMap::default();
|
|
179
|
+
let mut orig_reverse_imports: SecondaryMap<ModuleToken, FxHashSet<ModuleToken>> =
|
|
180
|
+
SecondaryMap::default();
|
|
181
|
+
let mut weights = FxHashMap::default();
|
|
182
|
+
|
|
183
|
+
for child in children {
|
|
184
|
+
orig_imports.insert(*child, FxHashSet::default());
|
|
185
|
+
orig_reverse_imports.insert(*child, FxHashSet::default());
|
|
186
|
+
}
|
|
187
|
+
// Get number of imports between each child.
|
|
188
|
+
// Add as edges.
|
|
189
|
+
for child_a in children {
|
|
190
|
+
for child_b in children.iter().filter(|child| *child != child_a) {
|
|
191
|
+
// Get number of imports from child_a to child_b (as package).
|
|
192
|
+
let num_imports_from_a_to_b = self
|
|
193
|
+
.find_direct_imports_between(*child_a, *child_b, true)
|
|
194
|
+
.unwrap()
|
|
195
|
+
.len();
|
|
196
|
+
|
|
197
|
+
if num_imports_from_a_to_b > 0 {
|
|
198
|
+
// a depends on b - add it as an edge in our temporary graph.
|
|
199
|
+
orig_imports[*child_a].insert(*child_b);
|
|
200
|
+
orig_reverse_imports[*child_b].insert(*child_a);
|
|
201
|
+
weights.insert((*child_a, *child_b), num_imports_from_a_to_b);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
(orig_imports, orig_reverse_imports, weights)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Removes the module from the graph, along with any imports to or from it.
|
|
210
|
+
fn remove_module_from_graph(
|
|
211
|
+
module: &ModuleToken,
|
|
212
|
+
imports: &mut SecondaryMap<ModuleToken, FxHashSet<ModuleToken>>,
|
|
213
|
+
reverse_imports: &mut SecondaryMap<ModuleToken, FxHashSet<ModuleToken>>,
|
|
214
|
+
) {
|
|
215
|
+
imports.remove(*module);
|
|
216
|
+
for (_, importeds) in imports.iter_mut() {
|
|
217
|
+
importeds.remove(module);
|
|
218
|
+
}
|
|
219
|
+
reverse_imports.remove(*module);
|
|
220
|
+
for (_, importers) in reverse_imports.iter_mut() {
|
|
221
|
+
importers.remove(module);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// Removes the module from the graph, along with any imports to or from it.
|
|
226
|
+
fn calculate_degree_difference(
|
|
227
|
+
module: &ModuleToken,
|
|
228
|
+
imports: &SecondaryMap<ModuleToken, FxHashSet<ModuleToken>>,
|
|
229
|
+
reverse_imports: &SecondaryMap<ModuleToken, FxHashSet<ModuleToken>>,
|
|
230
|
+
edge_weights: &FxHashMap<(ModuleToken, ModuleToken), usize>,
|
|
231
|
+
) -> isize {
|
|
232
|
+
let importer_modules = &reverse_imports[*module];
|
|
233
|
+
let imported_modules = &imports[*module];
|
|
234
|
+
|
|
235
|
+
let indegree: isize = importer_modules
|
|
236
|
+
.iter()
|
|
237
|
+
.map(|importer| edge_weights[&(*importer, *module)] as isize)
|
|
238
|
+
.sum();
|
|
239
|
+
let outdegree: isize = imported_modules
|
|
240
|
+
.iter()
|
|
241
|
+
.map(|imported| edge_weights[&(*module, *imported)] as isize)
|
|
242
|
+
.sum();
|
|
243
|
+
|
|
244
|
+
outdegree - indegree
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fn get_module_position(vec: &[ModuleToken], module: &ModuleToken) -> Option<usize> {
|
|
248
|
+
vec.iter().position(|&i| i == *module)
|
|
249
|
+
}
|
|
@@ -17,22 +17,54 @@ impl Graph {
|
|
|
17
17
|
imported: ModuleToken,
|
|
18
18
|
as_packages: bool,
|
|
19
19
|
) -> GrimpResult<bool> {
|
|
20
|
-
let mut
|
|
21
|
-
let mut
|
|
20
|
+
let mut importers: FxHashSet<_> = importer.into();
|
|
21
|
+
let mut importeds: FxHashSet<_> = imported.into();
|
|
22
22
|
if as_packages {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if !(&
|
|
23
|
+
importers.extend_with_descendants(self);
|
|
24
|
+
importeds.extend_with_descendants(self);
|
|
25
|
+
if !(&importers & &importeds).is_empty() {
|
|
26
26
|
return Err(GrimpError::SharedDescendants);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
let direct_imports =
|
|
30
|
+
let direct_imports = importers
|
|
31
31
|
.iter()
|
|
32
|
-
.flat_map(|
|
|
32
|
+
.flat_map(|importer_module| self.imports.get(*importer_module).unwrap().iter().cloned())
|
|
33
33
|
.collect::<FxHashSet<ModuleToken>>();
|
|
34
34
|
|
|
35
|
-
Ok(!(&direct_imports & &
|
|
35
|
+
Ok(!(&direct_imports & &importeds).is_empty())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn find_direct_imports_between(
|
|
39
|
+
&self,
|
|
40
|
+
importer: ModuleToken,
|
|
41
|
+
imported: ModuleToken,
|
|
42
|
+
as_packages: bool,
|
|
43
|
+
) -> GrimpResult<FxHashSet<(ModuleToken, ModuleToken)>> {
|
|
44
|
+
let mut all_imports: FxHashSet<(ModuleToken, ModuleToken)> = FxHashSet::default();
|
|
45
|
+
|
|
46
|
+
let mut importers: FxHashSet<_> = importer.into();
|
|
47
|
+
let mut importeds: FxHashSet<_> = imported.into();
|
|
48
|
+
if as_packages {
|
|
49
|
+
importers.extend_with_descendants(self);
|
|
50
|
+
importeds.extend_with_descendants(self);
|
|
51
|
+
if !(&importers & &importeds).is_empty() {
|
|
52
|
+
return Err(GrimpError::SharedDescendants);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for importer_module in importers.iter() {
|
|
57
|
+
if let Some(all_imports_imported_by_this_one) = self.imports.get(*importer_module) {
|
|
58
|
+
let imports_of_supplied_package: FxHashSet<_> = all_imports_imported_by_this_one
|
|
59
|
+
.iter()
|
|
60
|
+
.filter(|candidate| importeds.contains(*candidate))
|
|
61
|
+
.map(|imported_module| (*importer_module, *imported_module))
|
|
62
|
+
.collect();
|
|
63
|
+
all_imports.extend(imports_of_supplied_package);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Ok(all_imports)
|
|
36
68
|
}
|
|
37
69
|
|
|
38
70
|
pub fn modules_directly_imported_by(&self, importer: ModuleToken) -> &FxHashSet<ModuleToken> {
|
|
@@ -25,6 +25,7 @@ pub mod hierarchy_queries;
|
|
|
25
25
|
pub mod higher_order_queries;
|
|
26
26
|
pub mod import_chain_queries;
|
|
27
27
|
|
|
28
|
+
pub mod cycle_breakers;
|
|
28
29
|
pub(crate) mod pathfinding;
|
|
29
30
|
|
|
30
31
|
static MODULE_NAMES: LazyLock<RwLock<StringInterner<StringBackend>>> =
|
|
@@ -619,6 +620,35 @@ impl GraphWrapper {
|
|
|
619
620
|
self.convert_package_dependencies_to_python(py, illegal_dependencies)
|
|
620
621
|
}
|
|
621
622
|
|
|
623
|
+
pub fn nominate_cycle_breakers<'py>(
|
|
624
|
+
&self,
|
|
625
|
+
py: Python<'py>,
|
|
626
|
+
package: &str,
|
|
627
|
+
) -> PyResult<Bound<'py, PySet>> {
|
|
628
|
+
let package = self.get_visible_module_by_name(package)?.token();
|
|
629
|
+
let cycle_breakers = self._graph.nominate_cycle_breakers(package)?;
|
|
630
|
+
PySet::new(
|
|
631
|
+
py,
|
|
632
|
+
cycle_breakers
|
|
633
|
+
.into_iter()
|
|
634
|
+
.map(|(importer, imported)| {
|
|
635
|
+
let importer = self._graph.get_module(importer).unwrap();
|
|
636
|
+
let imported = self._graph.get_module(imported).unwrap();
|
|
637
|
+
Import::new(importer.name(), imported.name())
|
|
638
|
+
})
|
|
639
|
+
.map(|import| {
|
|
640
|
+
PyTuple::new(
|
|
641
|
+
py,
|
|
642
|
+
[
|
|
643
|
+
import.importer.into_py_any(py).unwrap(),
|
|
644
|
+
import.imported.into_py_any(py).unwrap(),
|
|
645
|
+
],
|
|
646
|
+
)
|
|
647
|
+
.unwrap()
|
|
648
|
+
}),
|
|
649
|
+
)
|
|
650
|
+
}
|
|
651
|
+
|
|
622
652
|
#[pyo3(name = "clone")]
|
|
623
653
|
pub fn clone_py(&self) -> GraphWrapper {
|
|
624
654
|
self.clone()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
__version__ = "3.
|
|
1
|
+
__version__ = "3.13"
|
|
2
2
|
|
|
3
|
-
from .application.
|
|
3
|
+
from .application.graph import DetailedImport, ImportGraph, Import
|
|
4
4
|
from .domain.analysis import PackageDependency, Route
|
|
5
5
|
from .domain.valueobjects import DirectImport, Module, Layer
|
|
6
6
|
from .main import build_graph
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
from typing import Iterator, List, Optional, Sequence, Set, Tuple
|
|
5
|
-
|
|
6
|
-
from typing_extensions import TypedDict
|
|
7
|
-
|
|
8
|
-
from grimp.domain.analysis import PackageDependency
|
|
2
|
+
from typing import List, Optional, Sequence, Set, Tuple, TypedDict
|
|
3
|
+
from grimp.domain.analysis import PackageDependency, Route
|
|
9
4
|
from grimp.domain.valueobjects import Layer
|
|
5
|
+
from grimp import _rustgrimp as rust # type: ignore[attr-defined]
|
|
6
|
+
from grimp.exceptions import (
|
|
7
|
+
ModuleNotPresent,
|
|
8
|
+
NoSuchContainer,
|
|
9
|
+
InvalidModuleExpression,
|
|
10
|
+
InvalidImportExpression,
|
|
11
|
+
)
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class Import(TypedDict):
|
|
@@ -14,28 +16,38 @@ class Import(TypedDict):
|
|
|
14
16
|
imported: str
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
# Corresponds to importer, imported.
|
|
20
|
+
# Prefer this form to Import, as it's both more lightweight, and hashable.
|
|
21
|
+
ImportTuple = Tuple[str, str]
|
|
22
|
+
|
|
23
|
+
|
|
17
24
|
class DetailedImport(Import):
|
|
18
25
|
line_number: int
|
|
19
26
|
line_contents: str
|
|
20
27
|
|
|
21
28
|
|
|
22
|
-
class ImportGraph
|
|
29
|
+
class ImportGraph:
|
|
23
30
|
"""
|
|
24
31
|
A Directed Graph of imports between Python modules.
|
|
25
32
|
"""
|
|
26
33
|
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
super().__init__()
|
|
36
|
+
self._cached_modules: Set[str] | None = None
|
|
37
|
+
self._rustgraph = rust.Graph()
|
|
38
|
+
|
|
27
39
|
# Mechanics
|
|
28
40
|
# ---------
|
|
29
41
|
|
|
30
42
|
@property
|
|
31
|
-
@abc.abstractmethod
|
|
32
43
|
def modules(self) -> Set[str]:
|
|
33
44
|
"""
|
|
34
45
|
The names of all the modules in the graph.
|
|
35
46
|
"""
|
|
36
|
-
|
|
47
|
+
if self._cached_modules is None:
|
|
48
|
+
self._cached_modules = self._rustgraph.get_modules()
|
|
49
|
+
return self._cached_modules
|
|
37
50
|
|
|
38
|
-
@abc.abstractmethod
|
|
39
51
|
def find_matching_modules(self, expression: str) -> Set[str]:
|
|
40
52
|
"""
|
|
41
53
|
Find all modules matching the passed expression.
|
|
@@ -67,9 +79,11 @@ class ImportGraph(abc.ABC):
|
|
|
67
79
|
- ``mypackage.foo*``: is not a valid expression. (The wildcard must replace a whole module
|
|
68
80
|
name.)
|
|
69
81
|
"""
|
|
70
|
-
|
|
82
|
+
try:
|
|
83
|
+
return self._rustgraph.find_matching_modules(expression)
|
|
84
|
+
except rust.InvalidModuleExpression as e:
|
|
85
|
+
raise InvalidModuleExpression(str(e)) from e
|
|
71
86
|
|
|
72
|
-
@abc.abstractmethod
|
|
73
87
|
def add_module(self, module: str, is_squashed: bool = False) -> None:
|
|
74
88
|
"""
|
|
75
89
|
Add a module to the graph.
|
|
@@ -80,18 +94,18 @@ class ImportGraph(abc.ABC):
|
|
|
80
94
|
want to include an external package in the graph but don't care about all the dependencies
|
|
81
95
|
within that package.
|
|
82
96
|
"""
|
|
83
|
-
|
|
97
|
+
self._cached_modules = None
|
|
98
|
+
self._rustgraph.add_module(module, is_squashed)
|
|
84
99
|
|
|
85
|
-
@abc.abstractmethod
|
|
86
100
|
def remove_module(self, module: str) -> None:
|
|
87
101
|
"""
|
|
88
102
|
Remove a module from the graph, if it exists.
|
|
89
103
|
|
|
90
104
|
If the module is not present in the graph, no exception will be raised.
|
|
91
105
|
"""
|
|
92
|
-
|
|
106
|
+
self._cached_modules = None
|
|
107
|
+
self._rustgraph.remove_module(module)
|
|
93
108
|
|
|
94
|
-
@abc.abstractmethod
|
|
95
109
|
def squash_module(self, module: str) -> None:
|
|
96
110
|
"""
|
|
97
111
|
'Squash' a module in the graph.
|
|
@@ -101,18 +115,21 @@ class ImportGraph(abc.ABC):
|
|
|
101
115
|
A squashed module represents both itself and all its descendants. This allow parts of the
|
|
102
116
|
graph to be simplified.
|
|
103
117
|
"""
|
|
104
|
-
|
|
118
|
+
self._cached_modules = None
|
|
119
|
+
if not self._rustgraph.contains_module(module):
|
|
120
|
+
raise ModuleNotPresent(f'"{module}" not present in the graph.')
|
|
121
|
+
self._rustgraph.squash_module(module)
|
|
105
122
|
|
|
106
|
-
@abc.abstractmethod
|
|
107
123
|
def is_module_squashed(self, module: str) -> bool:
|
|
108
124
|
"""
|
|
109
125
|
Return whether a module is squashed.
|
|
110
126
|
|
|
111
127
|
If the module is not present in the graph, grimp.exceptions.ModuleNotPresent will be raised.
|
|
112
128
|
"""
|
|
113
|
-
|
|
129
|
+
if not self._rustgraph.contains_module(module):
|
|
130
|
+
raise ModuleNotPresent(f'"{module}" not present in the graph.')
|
|
131
|
+
return self._rustgraph.is_module_squashed(module)
|
|
114
132
|
|
|
115
|
-
@abc.abstractmethod
|
|
116
133
|
def add_import(
|
|
117
134
|
self,
|
|
118
135
|
*,
|
|
@@ -125,26 +142,30 @@ class ImportGraph(abc.ABC):
|
|
|
125
142
|
Add a direct import between two modules to the graph. If the modules are not already
|
|
126
143
|
present, they will be added to the graph.
|
|
127
144
|
"""
|
|
128
|
-
|
|
145
|
+
self._cached_modules = None
|
|
146
|
+
self._rustgraph.add_import(
|
|
147
|
+
importer=importer,
|
|
148
|
+
imported=imported,
|
|
149
|
+
line_number=line_number,
|
|
150
|
+
line_contents=line_contents,
|
|
151
|
+
)
|
|
129
152
|
|
|
130
|
-
@abc.abstractmethod
|
|
131
153
|
def remove_import(self, *, importer: str, imported: str) -> None:
|
|
132
154
|
"""
|
|
133
155
|
Remove a direct import between two modules. Does not remove the modules themselves.
|
|
134
156
|
"""
|
|
135
|
-
|
|
157
|
+
self._cached_modules = None
|
|
158
|
+
return self._rustgraph.remove_import(importer=importer, imported=imported)
|
|
136
159
|
|
|
137
|
-
@abc.abstractmethod
|
|
138
160
|
def count_imports(self) -> int:
|
|
139
161
|
"""
|
|
140
162
|
Return the number of imports in the graph.
|
|
141
163
|
"""
|
|
142
|
-
|
|
164
|
+
return self._rustgraph.count_imports()
|
|
143
165
|
|
|
144
166
|
# Descendants
|
|
145
167
|
# -----------
|
|
146
168
|
|
|
147
|
-
@abc.abstractmethod
|
|
148
169
|
def find_children(self, module: str) -> Set[str]:
|
|
149
170
|
"""
|
|
150
171
|
Find all modules one level below the module. For example, the children of
|
|
@@ -153,9 +174,12 @@ class ImportGraph(abc.ABC):
|
|
|
153
174
|
Raises:
|
|
154
175
|
ValueError if attempted on a squashed module.
|
|
155
176
|
"""
|
|
156
|
-
|
|
177
|
+
# It doesn't make sense to find the children of a squashed module, as we don't store
|
|
178
|
+
# the children in the graph.
|
|
179
|
+
if self.is_module_squashed(module):
|
|
180
|
+
raise ValueError("Cannot find children of a squashed module.")
|
|
181
|
+
return self._rustgraph.find_children(module)
|
|
157
182
|
|
|
158
|
-
@abc.abstractmethod
|
|
159
183
|
def find_descendants(self, module: str) -> Set[str]:
|
|
160
184
|
"""
|
|
161
185
|
Find all modules below the module. For example, the descendants of
|
|
@@ -164,12 +188,15 @@ class ImportGraph(abc.ABC):
|
|
|
164
188
|
Raises:
|
|
165
189
|
ValueError if attempted on a squashed module.
|
|
166
190
|
"""
|
|
167
|
-
|
|
191
|
+
# It doesn't make sense to find the descendants of a squashed module, as we don't store
|
|
192
|
+
# the descendants in the graph.
|
|
193
|
+
if self.is_module_squashed(module):
|
|
194
|
+
raise ValueError("Cannot find descendants of a squashed module.")
|
|
195
|
+
return self._rustgraph.find_descendants(module)
|
|
168
196
|
|
|
169
197
|
# Direct imports
|
|
170
198
|
# --------------
|
|
171
199
|
|
|
172
|
-
@abc.abstractmethod
|
|
173
200
|
def direct_import_exists(
|
|
174
201
|
self, *, importer: str, imported: str, as_packages: bool = False
|
|
175
202
|
) -> bool:
|
|
@@ -182,17 +209,19 @@ class ImportGraph(abc.ABC):
|
|
|
182
209
|
as_packages: Whether or not to treat the supplied modules as individual modules,
|
|
183
210
|
or as entire packages (including any descendants).
|
|
184
211
|
"""
|
|
185
|
-
|
|
212
|
+
return self._rustgraph.direct_import_exists(
|
|
213
|
+
importer=importer, imported=imported, as_packages=as_packages
|
|
214
|
+
)
|
|
186
215
|
|
|
187
|
-
@abc.abstractmethod
|
|
188
216
|
def find_modules_directly_imported_by(self, module: str) -> Set[str]:
|
|
189
|
-
|
|
217
|
+
return self._rustgraph.find_modules_directly_imported_by(module)
|
|
190
218
|
|
|
191
|
-
@abc.abstractmethod
|
|
192
219
|
def find_modules_that_directly_import(self, module: str) -> Set[str]:
|
|
193
|
-
|
|
220
|
+
if self._rustgraph.contains_module(module):
|
|
221
|
+
# TODO panics if module isn't in modules.
|
|
222
|
+
return self._rustgraph.find_modules_that_directly_import(module)
|
|
223
|
+
return set()
|
|
194
224
|
|
|
195
|
-
@abc.abstractmethod
|
|
196
225
|
def get_import_details(self, *, importer: str, imported: str) -> List[DetailedImport]:
|
|
197
226
|
"""
|
|
198
227
|
Return available metadata relating to the direct imports between two modules, in the form:
|
|
@@ -212,10 +241,12 @@ class ImportGraph(abc.ABC):
|
|
|
212
241
|
For example, if an import has been added by the `add_import` method without line_number and
|
|
213
242
|
line_contents specified.
|
|
214
243
|
"""
|
|
215
|
-
|
|
244
|
+
return self._rustgraph.get_import_details(
|
|
245
|
+
importer=importer,
|
|
246
|
+
imported=imported,
|
|
247
|
+
)
|
|
216
248
|
|
|
217
|
-
|
|
218
|
-
def find_matching_direct_imports(self, *, import_expression: str) -> List[Import]:
|
|
249
|
+
def find_matching_direct_imports(self, import_expression: str) -> List[Import]:
|
|
219
250
|
"""
|
|
220
251
|
Find all direct imports matching the passed expressions.
|
|
221
252
|
|
|
@@ -242,12 +273,23 @@ class ImportGraph(abc.ABC):
|
|
|
242
273
|
|
|
243
274
|
See `ImportGraph.find_matching_modules` for a description of module expressions.
|
|
244
275
|
"""
|
|
245
|
-
|
|
276
|
+
try:
|
|
277
|
+
importer_expression, imported_expression = import_expression.split(" -> ")
|
|
278
|
+
except ValueError:
|
|
279
|
+
raise InvalidImportExpression(f"{import_expression} is not a valid import expression.")
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
return self._rustgraph.find_matching_direct_imports(
|
|
283
|
+
importer_expression=importer_expression, imported_expression=imported_expression
|
|
284
|
+
)
|
|
285
|
+
except rust.InvalidModuleExpression as e:
|
|
286
|
+
raise InvalidImportExpression(
|
|
287
|
+
f"{import_expression} is not a valid import expression."
|
|
288
|
+
) from e
|
|
246
289
|
|
|
247
290
|
# Indirect imports
|
|
248
291
|
# ----------------
|
|
249
292
|
|
|
250
|
-
@abc.abstractmethod
|
|
251
293
|
def find_downstream_modules(self, module: str, as_package: bool = False) -> Set[str]:
|
|
252
294
|
"""
|
|
253
295
|
Return a set of the names of all the modules that import (even indirectly) the
|
|
@@ -268,9 +310,8 @@ class ImportGraph(abc.ABC):
|
|
|
268
310
|
# mypackage.foo.two.
|
|
269
311
|
import_graph.find_downstream_modules('mypackage.foo', as_package=True)
|
|
270
312
|
"""
|
|
271
|
-
|
|
313
|
+
return self._rustgraph.find_downstream_modules(module, as_package)
|
|
272
314
|
|
|
273
|
-
@abc.abstractmethod
|
|
274
315
|
def find_upstream_modules(self, module: str, as_package: bool = False) -> Set[str]:
|
|
275
316
|
"""
|
|
276
317
|
Return a set of the names of all the modules that are imported (even indirectly) by the
|
|
@@ -284,9 +325,8 @@ class ImportGraph(abc.ABC):
|
|
|
284
325
|
modules *external* to the subpackage, and won't include modules within
|
|
285
326
|
the subpackage.
|
|
286
327
|
"""
|
|
287
|
-
|
|
328
|
+
return self._rustgraph.find_upstream_modules(module, as_package)
|
|
288
329
|
|
|
289
|
-
@abc.abstractmethod
|
|
290
330
|
def find_shortest_chain(
|
|
291
331
|
self, importer: str, imported: str, as_packages: bool = False
|
|
292
332
|
) -> tuple[str, ...] | None:
|
|
@@ -303,9 +343,13 @@ class ImportGraph(abc.ABC):
|
|
|
303
343
|
Returns:
|
|
304
344
|
Tuple of module names, from importer to imported, or None if no chain exists.
|
|
305
345
|
"""
|
|
306
|
-
|
|
346
|
+
for module in (importer, imported):
|
|
347
|
+
if not self._rustgraph.contains_module(module):
|
|
348
|
+
raise ValueError(f"Module {module} is not present in the graph.")
|
|
349
|
+
|
|
350
|
+
chain = self._rustgraph.find_shortest_chain(importer, imported, as_packages)
|
|
351
|
+
return tuple(chain) if chain else None
|
|
307
352
|
|
|
308
|
-
@abc.abstractmethod
|
|
309
353
|
def find_shortest_chains(
|
|
310
354
|
self, importer: str, imported: str, as_packages: bool = True
|
|
311
355
|
) -> Set[Tuple[str, ...]]:
|
|
@@ -318,24 +362,11 @@ class ImportGraph(abc.ABC):
|
|
|
318
362
|
The default behavior is to treat the import and imported as packages, however, if
|
|
319
363
|
as_packages is False, both the importer and imported will be treated as modules instead.
|
|
320
364
|
|
|
321
|
-
|
|
322
|
-
|
|
323
365
|
Returns:
|
|
324
366
|
A set of tuples of strings. Each tuple is ordered from importer to imported modules.
|
|
325
367
|
"""
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
def find_all_simple_chains(self, importer: str, imported: str) -> Iterator[Tuple[str, ...]]:
|
|
329
|
-
"""
|
|
330
|
-
Generate all simple chains between the importer and the imported modules.
|
|
331
|
-
|
|
332
|
-
Note: this method is no longer documented and will be removed.
|
|
333
|
-
"""
|
|
334
|
-
raise AttributeError(
|
|
335
|
-
"This method has been removed. Consider using find_shortest_chains instead?"
|
|
336
|
-
)
|
|
368
|
+
return self._rustgraph.find_shortest_chains(importer, imported, as_packages)
|
|
337
369
|
|
|
338
|
-
@abc.abstractmethod
|
|
339
370
|
def chain_exists(self, importer: str, imported: str, as_packages: bool = False) -> bool:
|
|
340
371
|
"""
|
|
341
372
|
Return whether any chain of imports exists between the two modules, in the direction
|
|
@@ -347,7 +378,7 @@ class ImportGraph(abc.ABC):
|
|
|
347
378
|
treating them as subpackages, all descendants of the supplied modules
|
|
348
379
|
will be checked too.
|
|
349
380
|
"""
|
|
350
|
-
|
|
381
|
+
return self._rustgraph.chain_exists(importer, imported, as_packages)
|
|
351
382
|
|
|
352
383
|
# High level analysis
|
|
353
384
|
# -------------------
|
|
@@ -395,7 +426,34 @@ class ImportGraph(abc.ABC):
|
|
|
395
426
|
|
|
396
427
|
Raises NoSuchContainer if the container is not a module in the graph.
|
|
397
428
|
"""
|
|
398
|
-
|
|
429
|
+
layers = _parse_layers(layers)
|
|
430
|
+
try:
|
|
431
|
+
result = self._rustgraph.find_illegal_dependencies_for_layers(
|
|
432
|
+
layers=tuple(
|
|
433
|
+
{
|
|
434
|
+
"layers": layer.module_tails,
|
|
435
|
+
"independent": layer.independent,
|
|
436
|
+
"closed": layer.closed,
|
|
437
|
+
}
|
|
438
|
+
for layer in layers
|
|
439
|
+
),
|
|
440
|
+
containers=set(containers) if containers else set(),
|
|
441
|
+
)
|
|
442
|
+
except rust.NoSuchContainer as e:
|
|
443
|
+
raise NoSuchContainer(str(e))
|
|
444
|
+
|
|
445
|
+
return _dependencies_from_tuple(result)
|
|
446
|
+
|
|
447
|
+
def nominate_cycle_breakers(self, package: str) -> set[ImportTuple]:
|
|
448
|
+
"""
|
|
449
|
+
Identify a set of imports that, if removed, would make the package locally acyclic.
|
|
450
|
+
"""
|
|
451
|
+
if not self._rustgraph.contains_module(package):
|
|
452
|
+
raise ModuleNotPresent(f'"{package}" not present in the graph.')
|
|
453
|
+
return self._rustgraph.nominate_cycle_breakers(package)
|
|
454
|
+
|
|
455
|
+
# Dunder methods
|
|
456
|
+
# --------------
|
|
399
457
|
|
|
400
458
|
def __repr__(self) -> str:
|
|
401
459
|
"""
|
|
@@ -415,3 +473,57 @@ class ImportGraph(abc.ABC):
|
|
|
415
473
|
else:
|
|
416
474
|
stringified_modules = "empty"
|
|
417
475
|
return f"<{self.__class__.__name__}: {stringified_modules}>"
|
|
476
|
+
|
|
477
|
+
def __deepcopy__(self, memodict: dict) -> "ImportGraph":
|
|
478
|
+
new_graph = ImportGraph()
|
|
479
|
+
new_graph._rustgraph = self._rustgraph.clone()
|
|
480
|
+
return new_graph
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class _RustRoute(TypedDict):
|
|
484
|
+
heads: frozenset[str]
|
|
485
|
+
middle: tuple[str, ...]
|
|
486
|
+
tails: frozenset[str]
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class _RustPackageDependency(TypedDict):
|
|
490
|
+
importer: str
|
|
491
|
+
imported: str
|
|
492
|
+
routes: tuple[_RustRoute, ...]
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _parse_layers(layers: Sequence[Layer | str | set[str]]) -> tuple[Layer, ...]:
|
|
496
|
+
"""
|
|
497
|
+
Convert the passed raw `layers` into `Layer`s.
|
|
498
|
+
"""
|
|
499
|
+
out_layers = []
|
|
500
|
+
for layer in layers:
|
|
501
|
+
if isinstance(layer, Layer):
|
|
502
|
+
out_layers.append(layer)
|
|
503
|
+
elif isinstance(layer, str):
|
|
504
|
+
out_layers.append(Layer(layer))
|
|
505
|
+
else:
|
|
506
|
+
out_layers.append(Layer(*tuple(layer)))
|
|
507
|
+
return tuple(out_layers)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _dependencies_from_tuple(
|
|
511
|
+
rust_package_dependency_tuple: tuple[_RustPackageDependency, ...],
|
|
512
|
+
) -> set[PackageDependency]:
|
|
513
|
+
return {
|
|
514
|
+
PackageDependency(
|
|
515
|
+
imported=dep_dict["imported"],
|
|
516
|
+
importer=dep_dict["importer"],
|
|
517
|
+
routes=frozenset(
|
|
518
|
+
{
|
|
519
|
+
Route(
|
|
520
|
+
heads=route_dict["heads"],
|
|
521
|
+
middle=route_dict["middle"],
|
|
522
|
+
tails=route_dict["tails"],
|
|
523
|
+
)
|
|
524
|
+
for route_dict in dep_dict["routes"]
|
|
525
|
+
}
|
|
526
|
+
),
|
|
527
|
+
)
|
|
528
|
+
for dep_dict in rust_package_dependency_tuple
|
|
529
|
+
}
|
|
@@ -7,7 +7,7 @@ from typing import Dict, Sequence, Set, Type, Union, cast, Iterable
|
|
|
7
7
|
from .scanning import scan_imports
|
|
8
8
|
from ..application.ports import caching
|
|
9
9
|
from ..application.ports.filesystem import AbstractFileSystem
|
|
10
|
-
from ..application.
|
|
10
|
+
from ..application.graph import ImportGraph
|
|
11
11
|
from ..application.ports.modulefinder import AbstractModuleFinder, FoundPackage, ModuleFile
|
|
12
12
|
from ..application.ports.packagefinder import AbstractPackageFinder
|
|
13
13
|
from ..domain.valueobjects import DirectImport, Module
|
|
@@ -2,7 +2,7 @@ __all__ = ["build_graph"]
|
|
|
2
2
|
|
|
3
3
|
from .adaptors.caching import Cache
|
|
4
4
|
from .adaptors.filesystem import FileSystem
|
|
5
|
-
from .
|
|
5
|
+
from .application.graph import ImportGraph
|
|
6
6
|
from .adaptors.modulefinder import ModuleFinder
|
|
7
7
|
from .adaptors.packagefinder import ImportLibPackageFinder
|
|
8
8
|
from .adaptors.timing import SystemClockTimer
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
from typing import List, Optional, Sequence, Set, Tuple, TypedDict
|
|
3
|
-
from grimp.application.ports.graph import DetailedImport, Import
|
|
4
|
-
from grimp.domain.analysis import PackageDependency, Route
|
|
5
|
-
from grimp.domain.valueobjects import Layer
|
|
6
|
-
from grimp import _rustgrimp as rust # type: ignore[attr-defined]
|
|
7
|
-
from grimp.exceptions import (
|
|
8
|
-
ModuleNotPresent,
|
|
9
|
-
NoSuchContainer,
|
|
10
|
-
InvalidModuleExpression,
|
|
11
|
-
InvalidImportExpression,
|
|
12
|
-
)
|
|
13
|
-
from grimp.application.ports import graph
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ImportGraph(graph.ImportGraph):
|
|
17
|
-
"""
|
|
18
|
-
Rust-backed implementation of the ImportGraph.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
def __init__(self) -> None:
|
|
22
|
-
super().__init__()
|
|
23
|
-
self._cached_modules: Set[str] | None = None
|
|
24
|
-
self._rustgraph = rust.Graph()
|
|
25
|
-
|
|
26
|
-
@property
|
|
27
|
-
def modules(self) -> Set[str]:
|
|
28
|
-
if self._cached_modules is None:
|
|
29
|
-
self._cached_modules = self._rustgraph.get_modules()
|
|
30
|
-
return self._cached_modules
|
|
31
|
-
|
|
32
|
-
def find_matching_modules(self, expression: str) -> Set[str]:
|
|
33
|
-
try:
|
|
34
|
-
return self._rustgraph.find_matching_modules(expression)
|
|
35
|
-
except rust.InvalidModuleExpression as e:
|
|
36
|
-
raise InvalidModuleExpression(str(e)) from e
|
|
37
|
-
|
|
38
|
-
def add_module(self, module: str, is_squashed: bool = False) -> None:
|
|
39
|
-
self._cached_modules = None
|
|
40
|
-
self._rustgraph.add_module(module, is_squashed)
|
|
41
|
-
|
|
42
|
-
def remove_module(self, module: str) -> None:
|
|
43
|
-
self._cached_modules = None
|
|
44
|
-
self._rustgraph.remove_module(module)
|
|
45
|
-
|
|
46
|
-
def squash_module(self, module: str) -> None:
|
|
47
|
-
self._cached_modules = None
|
|
48
|
-
if not self._rustgraph.contains_module(module):
|
|
49
|
-
raise ModuleNotPresent(f'"{module}" not present in the graph.')
|
|
50
|
-
self._rustgraph.squash_module(module)
|
|
51
|
-
|
|
52
|
-
def is_module_squashed(self, module: str) -> bool:
|
|
53
|
-
if not self._rustgraph.contains_module(module):
|
|
54
|
-
raise ModuleNotPresent(f'"{module}" not present in the graph.')
|
|
55
|
-
return self._rustgraph.is_module_squashed(module)
|
|
56
|
-
|
|
57
|
-
def add_import(
|
|
58
|
-
self,
|
|
59
|
-
*,
|
|
60
|
-
importer: str,
|
|
61
|
-
imported: str,
|
|
62
|
-
line_number: Optional[int] = None,
|
|
63
|
-
line_contents: Optional[str] = None,
|
|
64
|
-
) -> None:
|
|
65
|
-
self._cached_modules = None
|
|
66
|
-
self._rustgraph.add_import(
|
|
67
|
-
importer=importer,
|
|
68
|
-
imported=imported,
|
|
69
|
-
line_number=line_number,
|
|
70
|
-
line_contents=line_contents,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
def remove_import(self, *, importer: str, imported: str) -> None:
|
|
74
|
-
self._cached_modules = None
|
|
75
|
-
return self._rustgraph.remove_import(importer=importer, imported=imported)
|
|
76
|
-
|
|
77
|
-
def count_imports(self) -> int:
|
|
78
|
-
return self._rustgraph.count_imports()
|
|
79
|
-
|
|
80
|
-
def find_children(self, module: str) -> Set[str]:
|
|
81
|
-
# It doesn't make sense to find the children of a squashed module, as we don't store
|
|
82
|
-
# the children in the graph.
|
|
83
|
-
if self.is_module_squashed(module):
|
|
84
|
-
raise ValueError("Cannot find children of a squashed module.")
|
|
85
|
-
return self._rustgraph.find_children(module)
|
|
86
|
-
|
|
87
|
-
def find_descendants(self, module: str) -> Set[str]:
|
|
88
|
-
# It doesn't make sense to find the descendants of a squashed module, as we don't store
|
|
89
|
-
# the descendants in the graph.
|
|
90
|
-
if self.is_module_squashed(module):
|
|
91
|
-
raise ValueError("Cannot find descendants of a squashed module.")
|
|
92
|
-
return self._rustgraph.find_descendants(module)
|
|
93
|
-
|
|
94
|
-
def direct_import_exists(
|
|
95
|
-
self, *, importer: str, imported: str, as_packages: bool = False
|
|
96
|
-
) -> bool:
|
|
97
|
-
return self._rustgraph.direct_import_exists(
|
|
98
|
-
importer=importer, imported=imported, as_packages=as_packages
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
def find_modules_directly_imported_by(self, module: str) -> Set[str]:
|
|
102
|
-
return self._rustgraph.find_modules_directly_imported_by(module)
|
|
103
|
-
|
|
104
|
-
def find_modules_that_directly_import(self, module: str) -> Set[str]:
|
|
105
|
-
if self._rustgraph.contains_module(module):
|
|
106
|
-
# TODO panics if module isn't in modules.
|
|
107
|
-
return self._rustgraph.find_modules_that_directly_import(module)
|
|
108
|
-
return set()
|
|
109
|
-
|
|
110
|
-
def get_import_details(self, *, importer: str, imported: str) -> List[DetailedImport]:
|
|
111
|
-
return self._rustgraph.get_import_details(
|
|
112
|
-
importer=importer,
|
|
113
|
-
imported=imported,
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
def find_matching_direct_imports(self, import_expression: str) -> List[Import]:
|
|
117
|
-
try:
|
|
118
|
-
importer_expression, imported_expression = import_expression.split(" -> ")
|
|
119
|
-
except ValueError:
|
|
120
|
-
raise InvalidImportExpression(f"{import_expression} is not a valid import expression.")
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
return self._rustgraph.find_matching_direct_imports(
|
|
124
|
-
importer_expression=importer_expression, imported_expression=imported_expression
|
|
125
|
-
)
|
|
126
|
-
except rust.InvalidModuleExpression as e:
|
|
127
|
-
raise InvalidImportExpression(
|
|
128
|
-
f"{import_expression} is not a valid import expression."
|
|
129
|
-
) from e
|
|
130
|
-
|
|
131
|
-
def find_downstream_modules(self, module: str, as_package: bool = False) -> Set[str]:
|
|
132
|
-
return self._rustgraph.find_downstream_modules(module, as_package)
|
|
133
|
-
|
|
134
|
-
def find_upstream_modules(self, module: str, as_package: bool = False) -> Set[str]:
|
|
135
|
-
return self._rustgraph.find_upstream_modules(module, as_package)
|
|
136
|
-
|
|
137
|
-
def find_shortest_chain(
|
|
138
|
-
self, importer: str, imported: str, as_packages: bool = False
|
|
139
|
-
) -> tuple[str, ...] | None:
|
|
140
|
-
for module in (importer, imported):
|
|
141
|
-
if not self._rustgraph.contains_module(module):
|
|
142
|
-
raise ValueError(f"Module {module} is not present in the graph.")
|
|
143
|
-
|
|
144
|
-
chain = self._rustgraph.find_shortest_chain(importer, imported, as_packages)
|
|
145
|
-
return tuple(chain) if chain else None
|
|
146
|
-
|
|
147
|
-
def find_shortest_chains(
|
|
148
|
-
self, importer: str, imported: str, as_packages: bool = True
|
|
149
|
-
) -> Set[Tuple[str, ...]]:
|
|
150
|
-
return self._rustgraph.find_shortest_chains(importer, imported, as_packages)
|
|
151
|
-
|
|
152
|
-
def chain_exists(self, importer: str, imported: str, as_packages: bool = False) -> bool:
|
|
153
|
-
return self._rustgraph.chain_exists(importer, imported, as_packages)
|
|
154
|
-
|
|
155
|
-
def find_illegal_dependencies_for_layers(
|
|
156
|
-
self,
|
|
157
|
-
layers: Sequence[Layer | str | set[str]],
|
|
158
|
-
containers: set[str] | None = None,
|
|
159
|
-
) -> set[PackageDependency]:
|
|
160
|
-
layers = _parse_layers(layers)
|
|
161
|
-
try:
|
|
162
|
-
result = self._rustgraph.find_illegal_dependencies_for_layers(
|
|
163
|
-
layers=tuple(
|
|
164
|
-
{
|
|
165
|
-
"layers": layer.module_tails,
|
|
166
|
-
"independent": layer.independent,
|
|
167
|
-
"closed": layer.closed,
|
|
168
|
-
}
|
|
169
|
-
for layer in layers
|
|
170
|
-
),
|
|
171
|
-
containers=set(containers) if containers else set(),
|
|
172
|
-
)
|
|
173
|
-
except rust.NoSuchContainer as e:
|
|
174
|
-
raise NoSuchContainer(str(e))
|
|
175
|
-
|
|
176
|
-
return _dependencies_from_tuple(result)
|
|
177
|
-
|
|
178
|
-
# Dunder methods
|
|
179
|
-
# --------------
|
|
180
|
-
|
|
181
|
-
def __deepcopy__(self, memodict: dict) -> "ImportGraph":
|
|
182
|
-
new_graph = ImportGraph()
|
|
183
|
-
new_graph._rustgraph = self._rustgraph.clone()
|
|
184
|
-
return new_graph
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
class _RustRoute(TypedDict):
|
|
188
|
-
heads: frozenset[str]
|
|
189
|
-
middle: tuple[str, ...]
|
|
190
|
-
tails: frozenset[str]
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
class _RustPackageDependency(TypedDict):
|
|
194
|
-
importer: str
|
|
195
|
-
imported: str
|
|
196
|
-
routes: tuple[_RustRoute, ...]
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def _parse_layers(layers: Sequence[Layer | str | set[str]]) -> tuple[Layer, ...]:
|
|
200
|
-
"""
|
|
201
|
-
Convert the passed raw `layers` into `Layer`s.
|
|
202
|
-
"""
|
|
203
|
-
out_layers = []
|
|
204
|
-
for layer in layers:
|
|
205
|
-
if isinstance(layer, Layer):
|
|
206
|
-
out_layers.append(layer)
|
|
207
|
-
elif isinstance(layer, str):
|
|
208
|
-
out_layers.append(Layer(layer))
|
|
209
|
-
else:
|
|
210
|
-
out_layers.append(Layer(*tuple(layer)))
|
|
211
|
-
return tuple(out_layers)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def _dependencies_from_tuple(
|
|
215
|
-
rust_package_dependency_tuple: tuple[_RustPackageDependency, ...],
|
|
216
|
-
) -> set[PackageDependency]:
|
|
217
|
-
return {
|
|
218
|
-
PackageDependency(
|
|
219
|
-
imported=dep_dict["imported"],
|
|
220
|
-
importer=dep_dict["importer"],
|
|
221
|
-
routes=frozenset(
|
|
222
|
-
{
|
|
223
|
-
Route(
|
|
224
|
-
heads=route_dict["heads"],
|
|
225
|
-
middle=route_dict["middle"],
|
|
226
|
-
tails=route_dict["tails"],
|
|
227
|
-
)
|
|
228
|
-
for route_dict in dep_dict["routes"]
|
|
229
|
-
}
|
|
230
|
-
),
|
|
231
|
-
)
|
|
232
|
-
for dep_dict in rust_package_dependency_tuple
|
|
233
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|