grimp 3.0b3__tar.gz → 3.2__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.0b3 → grimp-3.2}/PKG-INFO +2 -1
- {grimp-3.0b3 → grimp-3.2}/pyproject.toml +4 -2
- {grimp-3.0b3 → grimp-3.2}/rust/src/layers.rs +115 -7
- {grimp-3.0b3 → grimp-3.2}/rust/src/lib.rs +58 -10
- {grimp-3.0b3 → grimp-3.2}/rust/tests/large.rs +5 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/__init__.py +3 -2
- grimp-3.2/src/grimp/adaptors/_layers.py +174 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/caching.py +21 -12
- {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/graph.py +4 -3
- {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/importscanner.py +116 -137
- {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/modulefinder.py +7 -4
- {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/packagefinder.py +1 -3
- {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/caching.py +5 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/graph.py +15 -10
- {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/importscanner.py +9 -7
- {grimp-3.0b3 → grimp-3.2}/src/grimp/application/usecases.py +12 -8
- {grimp-3.0b3 → grimp-3.2}/src/grimp/domain/valueobjects.py +21 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/exceptions.py +1 -3
- grimp-3.0b3/src/grimp/adaptors/_layers.py +0 -354
- {grimp-3.0b3 → grimp-3.2}/README.rst +0 -0
- {grimp-3.0b3 → grimp-3.2}/rust/.cargo/config +0 -0
- {grimp-3.0b3 → grimp-3.2}/rust/Cargo.lock +0 -0
- {grimp-3.0b3 → grimp-3.2}/rust/Cargo.toml +0 -0
- {grimp-3.0b3 → grimp-3.2}/rust/src/containers.rs +0 -0
- {grimp-3.0b3 → grimp-3.2}/rust/src/dependencies.rs +0 -0
- {grimp-3.0b3 → grimp-3.2}/rust/src/importgraph.rs +0 -0
- {grimp-3.0b3 → grimp-3.2}/rust/tests/large_graph.json +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/__init__.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/filesystem.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/timing.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/algorithms/__init__.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/algorithms/shortest_path.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/application/__init__.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/application/config.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/__init__.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/filesystem.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/modulefinder.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/packagefinder.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/timing.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/domain/__init__.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/domain/analysis.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/helpers.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/main.py +0 -0
- {grimp-3.0b3 → grimp-3.2}/src/grimp/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: grimp
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: BSD License
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.9
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
16
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
17
|
Classifier: Programming Language :: Rust
|
|
17
18
|
Classifier: Topic :: Utilities
|
|
@@ -8,7 +8,7 @@ module-name = "grimp._rustgrimp"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "grimp"
|
|
11
|
-
version = "3.
|
|
11
|
+
version = "3.2"
|
|
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 = [
|
|
@@ -30,6 +30,7 @@ classifiers = [
|
|
|
30
30
|
"Programming Language :: Python :: 3.9",
|
|
31
31
|
"Programming Language :: Python :: 3.10",
|
|
32
32
|
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
33
34
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
34
35
|
"Programming Language :: Rust",
|
|
35
36
|
"Topic :: Utilities",
|
|
@@ -52,4 +53,5 @@ where = ["src"]
|
|
|
52
53
|
namespaces = false
|
|
53
54
|
|
|
54
55
|
[tool.black]
|
|
55
|
-
line-length = 99
|
|
56
|
+
line-length = 99
|
|
57
|
+
exclude = 'tests/assets/syntaxerrorpackage'
|
|
@@ -6,10 +6,10 @@ use std::collections::HashSet;
|
|
|
6
6
|
use std::time::Instant;
|
|
7
7
|
|
|
8
8
|
/// A group of layers at the same level in the layering.
|
|
9
|
-
/// These layers should be independent.
|
|
10
9
|
#[derive(PartialEq, Eq, Hash, Debug)]
|
|
11
10
|
pub struct Level<'a> {
|
|
12
11
|
pub layers: Vec<&'a str>,
|
|
12
|
+
pub independent: bool,
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
pub fn find_illegal_dependencies<'a>(
|
|
@@ -45,6 +45,8 @@ pub fn find_illegal_dependencies<'a>(
|
|
|
45
45
|
dependencies
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/// Return every permutation of modules that exist in the graph
|
|
49
|
+
/// in which the second should not import the first.
|
|
48
50
|
fn _generate_module_permutations<'a>(
|
|
49
51
|
graph: &'a ImportGraph,
|
|
50
52
|
levels: &'a [Level],
|
|
@@ -69,13 +71,18 @@ fn _generate_module_permutations<'a>(
|
|
|
69
71
|
continue;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
// Build the layers that mustn't import this higher layer.
|
|
73
|
-
// includes
|
|
74
|
+
// Build the layers that mustn't import this higher layer.
|
|
75
|
+
// That includes:
|
|
76
|
+
// * lower layers.
|
|
77
|
+
// * sibling layers, if the layer is independent.
|
|
74
78
|
let mut layers_forbidden_to_import_higher_layer: Vec<&str> = vec![];
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
|
|
80
|
+
if higher_level.independent {
|
|
81
|
+
for potential_sibling_layer in &higher_level.layers {
|
|
82
|
+
if potential_sibling_layer != higher_layer {
|
|
83
|
+
// It's a sibling layer.
|
|
84
|
+
layers_forbidden_to_import_higher_layer.push(potential_sibling_layer);
|
|
85
|
+
}
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
|
|
@@ -333,12 +340,15 @@ mod tests {
|
|
|
333
340
|
]));
|
|
334
341
|
let levels = vec![
|
|
335
342
|
Level {
|
|
343
|
+
independent: true,
|
|
336
344
|
layers: vec!["high"],
|
|
337
345
|
},
|
|
338
346
|
Level {
|
|
347
|
+
independent: true,
|
|
339
348
|
layers: vec!["mid_a", "mid_b", "mid_c"],
|
|
340
349
|
},
|
|
341
350
|
Level {
|
|
351
|
+
independent: true,
|
|
342
352
|
layers: vec!["low"],
|
|
343
353
|
},
|
|
344
354
|
];
|
|
@@ -405,9 +415,11 @@ mod tests {
|
|
|
405
415
|
]));
|
|
406
416
|
let levels = vec![
|
|
407
417
|
Level {
|
|
418
|
+
independent: true,
|
|
408
419
|
layers: vec!["high"],
|
|
409
420
|
},
|
|
410
421
|
Level {
|
|
422
|
+
independent: true,
|
|
411
423
|
layers: vec!["low"],
|
|
412
424
|
},
|
|
413
425
|
];
|
|
@@ -460,12 +472,15 @@ mod tests {
|
|
|
460
472
|
]));
|
|
461
473
|
let levels = vec![
|
|
462
474
|
Level {
|
|
475
|
+
independent: true,
|
|
463
476
|
layers: vec!["high"],
|
|
464
477
|
},
|
|
465
478
|
Level {
|
|
479
|
+
independent: true,
|
|
466
480
|
layers: vec!["mid_a", "mid_b", "mid_c"],
|
|
467
481
|
},
|
|
468
482
|
Level {
|
|
483
|
+
independent: true,
|
|
469
484
|
layers: vec!["low"],
|
|
470
485
|
},
|
|
471
486
|
];
|
|
@@ -553,16 +568,109 @@ mod tests {
|
|
|
553
568
|
);
|
|
554
569
|
}
|
|
555
570
|
|
|
571
|
+
#[test]
|
|
572
|
+
fn test_generate_module_permutations_sibling_layer_not_independent() {
|
|
573
|
+
let graph = ImportGraph::new(HashMap::from([
|
|
574
|
+
("mypackage.low", HashSet::new()),
|
|
575
|
+
("mypackage.low.blue", HashSet::from(["mypackage.utils"])),
|
|
576
|
+
("mypackage.low.green", HashSet::new()),
|
|
577
|
+
(
|
|
578
|
+
"mypackage.low.green.alpha",
|
|
579
|
+
HashSet::from(["mypackage.high.yellow"]),
|
|
580
|
+
),
|
|
581
|
+
("mypackage.mid_a", HashSet::new()),
|
|
582
|
+
("mypackage.mid_a.foo", HashSet::new()),
|
|
583
|
+
("mypackage.mid_b", HashSet::new()),
|
|
584
|
+
("mypackage.mid_b.foo", HashSet::new()),
|
|
585
|
+
("mypackage.mid_c", HashSet::new()),
|
|
586
|
+
("mypackage.mid_c.foo", HashSet::new()),
|
|
587
|
+
("mypackage.high", HashSet::from(["mypackage.low.blue"])),
|
|
588
|
+
("mypackage.high.yellow", HashSet::new()),
|
|
589
|
+
("mypackage.high.red", HashSet::new()),
|
|
590
|
+
("mypackage.high.red.beta", HashSet::new()),
|
|
591
|
+
("mypackage.utils", HashSet::from(["mypackage.high.red"])),
|
|
592
|
+
]));
|
|
593
|
+
let levels = vec![
|
|
594
|
+
Level {
|
|
595
|
+
independent: true,
|
|
596
|
+
layers: vec!["high"],
|
|
597
|
+
},
|
|
598
|
+
Level {
|
|
599
|
+
independent: false,
|
|
600
|
+
layers: vec!["mid_a", "mid_b", "mid_c"],
|
|
601
|
+
},
|
|
602
|
+
Level {
|
|
603
|
+
independent: true,
|
|
604
|
+
layers: vec!["low"],
|
|
605
|
+
},
|
|
606
|
+
];
|
|
607
|
+
let containers = HashSet::from(["mypackage"]);
|
|
608
|
+
|
|
609
|
+
let perms = _generate_module_permutations(&graph, &levels, &containers);
|
|
610
|
+
|
|
611
|
+
let result: HashSet<(String, String, Option<String>)> = HashSet::from_iter(perms);
|
|
612
|
+
let (high, mid_a, mid_b, mid_c, low) = (
|
|
613
|
+
"mypackage.high",
|
|
614
|
+
"mypackage.mid_a",
|
|
615
|
+
"mypackage.mid_b",
|
|
616
|
+
"mypackage.mid_c",
|
|
617
|
+
"mypackage.low",
|
|
618
|
+
);
|
|
619
|
+
assert_eq!(
|
|
620
|
+
result,
|
|
621
|
+
HashSet::from_iter([
|
|
622
|
+
(
|
|
623
|
+
high.to_string(),
|
|
624
|
+
mid_a.to_string(),
|
|
625
|
+
Some("mypackage".to_string())
|
|
626
|
+
),
|
|
627
|
+
(
|
|
628
|
+
high.to_string(),
|
|
629
|
+
mid_b.to_string(),
|
|
630
|
+
Some("mypackage".to_string())
|
|
631
|
+
),
|
|
632
|
+
(
|
|
633
|
+
high.to_string(),
|
|
634
|
+
mid_c.to_string(),
|
|
635
|
+
Some("mypackage".to_string())
|
|
636
|
+
),
|
|
637
|
+
(
|
|
638
|
+
high.to_string(),
|
|
639
|
+
low.to_string(),
|
|
640
|
+
Some("mypackage".to_string())
|
|
641
|
+
),
|
|
642
|
+
(
|
|
643
|
+
mid_a.to_string(),
|
|
644
|
+
low.to_string(),
|
|
645
|
+
Some("mypackage".to_string())
|
|
646
|
+
),
|
|
647
|
+
(
|
|
648
|
+
mid_b.to_string(),
|
|
649
|
+
low.to_string(),
|
|
650
|
+
Some("mypackage".to_string())
|
|
651
|
+
),
|
|
652
|
+
(
|
|
653
|
+
mid_c.to_string(),
|
|
654
|
+
low.to_string(),
|
|
655
|
+
Some("mypackage".to_string())
|
|
656
|
+
),
|
|
657
|
+
])
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
556
661
|
#[test]
|
|
557
662
|
fn test_layers_from_levels() {
|
|
558
663
|
let levels = vec![
|
|
559
664
|
Level {
|
|
665
|
+
independent: true,
|
|
560
666
|
layers: vec!["high"],
|
|
561
667
|
},
|
|
562
668
|
Level {
|
|
669
|
+
independent: true,
|
|
563
670
|
layers: vec!["medium_a", "medium_b", "medium_c"],
|
|
564
671
|
},
|
|
565
672
|
Level {
|
|
673
|
+
independent: true,
|
|
566
674
|
layers: vec!["low"],
|
|
567
675
|
},
|
|
568
676
|
];
|
|
@@ -50,8 +50,11 @@ pub fn find_illegal_dependencies<'a>(
|
|
|
50
50
|
fn rustify_levels(levels_python: &PyTuple) -> Vec<Level> {
|
|
51
51
|
let mut rust_levels: Vec<Level> = vec![];
|
|
52
52
|
for level_python in levels_python.into_iter() {
|
|
53
|
-
let
|
|
53
|
+
let level_dict = level_python.downcast::<PyDict>().unwrap();
|
|
54
|
+
let layers: HashSet<&str> = level_dict.get_item("layers").unwrap().extract().unwrap();
|
|
55
|
+
let independent: bool = level_dict.get_item("independent").unwrap().extract().unwrap();
|
|
54
56
|
rust_levels.push(Level {
|
|
57
|
+
independent,
|
|
55
58
|
layers: layers.into_iter().collect(),
|
|
56
59
|
});
|
|
57
60
|
}
|
|
@@ -104,16 +107,38 @@ fn convert_dependencies_to_python<'a>(
|
|
|
104
107
|
#[cfg(test)]
|
|
105
108
|
mod tests {
|
|
106
109
|
use super::*;
|
|
107
|
-
|
|
110
|
+
|
|
111
|
+
// Macro to easily define a python dict.
|
|
112
|
+
// Adapted from the hash_map! macro in https://github.com/jofas/map_macro.
|
|
113
|
+
macro_rules! pydict {
|
|
114
|
+
($py: ident, {$($k: expr => $v: expr),*, $(,)?}) => {
|
|
115
|
+
{
|
|
116
|
+
let dict = PyDict::new($py);
|
|
117
|
+
$(
|
|
118
|
+
dict.set_item($k, $v)?;
|
|
119
|
+
)*
|
|
120
|
+
dict
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
108
124
|
|
|
109
125
|
#[test]
|
|
110
126
|
fn test_rustify_levels_no_sibling_layers() {
|
|
111
127
|
pyo3::prepare_freethreaded_python();
|
|
112
128
|
Python::with_gil(|py| -> PyResult<()> {
|
|
113
|
-
let elements: Vec<&
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
let elements: Vec<&PyDict> = vec![
|
|
130
|
+
pydict! (py, {
|
|
131
|
+
"independent" => true,
|
|
132
|
+
"layers" => HashSet::from(["high"]),
|
|
133
|
+
}),
|
|
134
|
+
pydict! (py, {
|
|
135
|
+
"independent" => true,
|
|
136
|
+
"layers" => HashSet::from(["medium"]),
|
|
137
|
+
}),
|
|
138
|
+
pydict! (py, {
|
|
139
|
+
"independent" => true,
|
|
140
|
+
"layers" => HashSet::from(["low"]),
|
|
141
|
+
})
|
|
117
142
|
];
|
|
118
143
|
let python_levels: &PyTuple = PyTuple::new(py, elements);
|
|
119
144
|
|
|
@@ -123,12 +148,15 @@ mod tests {
|
|
|
123
148
|
result,
|
|
124
149
|
vec![
|
|
125
150
|
Level {
|
|
151
|
+
independent: true,
|
|
126
152
|
layers: vec!["high"]
|
|
127
153
|
},
|
|
128
154
|
Level {
|
|
155
|
+
independent: true,
|
|
129
156
|
layers: vec!["medium"]
|
|
130
157
|
},
|
|
131
158
|
Level {
|
|
159
|
+
independent: true,
|
|
132
160
|
layers: vec!["low"]
|
|
133
161
|
}
|
|
134
162
|
]
|
|
@@ -143,10 +171,23 @@ mod tests {
|
|
|
143
171
|
fn test_rustify_levels_sibling_layers() {
|
|
144
172
|
pyo3::prepare_freethreaded_python();
|
|
145
173
|
Python::with_gil(|py| -> PyResult<()> {
|
|
146
|
-
let elements: Vec<&
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
174
|
+
let elements: Vec<&PyDict> = vec![
|
|
175
|
+
pydict! (py, {
|
|
176
|
+
"independent" => true,
|
|
177
|
+
"layers" => HashSet::from(["high"]),
|
|
178
|
+
}),
|
|
179
|
+
pydict! (py, {
|
|
180
|
+
"independent" => true,
|
|
181
|
+
"layers" => HashSet::from(["blue", "green", "orange"]),
|
|
182
|
+
}),
|
|
183
|
+
pydict! (py, {
|
|
184
|
+
"independent" => false,
|
|
185
|
+
"layers" => HashSet::from(["red", "yellow"]),
|
|
186
|
+
}),
|
|
187
|
+
pydict! (py, {
|
|
188
|
+
"independent" => true,
|
|
189
|
+
"layers" => HashSet::from(["low"]),
|
|
190
|
+
})
|
|
150
191
|
];
|
|
151
192
|
let python_levels: &PyTuple = PyTuple::new(py, elements);
|
|
152
193
|
|
|
@@ -159,12 +200,19 @@ mod tests {
|
|
|
159
200
|
result,
|
|
160
201
|
vec![
|
|
161
202
|
Level {
|
|
203
|
+
independent: true,
|
|
162
204
|
layers: vec!["high"]
|
|
163
205
|
},
|
|
164
206
|
Level {
|
|
207
|
+
independent: true,
|
|
165
208
|
layers: vec!["blue", "green", "orange"]
|
|
166
209
|
},
|
|
167
210
|
Level {
|
|
211
|
+
independent: false,
|
|
212
|
+
layers: vec!["red", "yellow"]
|
|
213
|
+
},
|
|
214
|
+
Level {
|
|
215
|
+
independent: true,
|
|
168
216
|
layers: vec!["low"]
|
|
169
217
|
}
|
|
170
218
|
]
|
|
@@ -21,18 +21,23 @@ fn test_large_graph() {
|
|
|
21
21
|
|
|
22
22
|
let levels = vec![
|
|
23
23
|
Level {
|
|
24
|
+
independent: true,
|
|
24
25
|
layers: vec!["plugins"],
|
|
25
26
|
},
|
|
26
27
|
Level {
|
|
28
|
+
independent: true,
|
|
27
29
|
layers: vec!["interfaces"],
|
|
28
30
|
},
|
|
29
31
|
Level {
|
|
32
|
+
independent: true,
|
|
30
33
|
layers: vec!["application"],
|
|
31
34
|
},
|
|
32
35
|
Level {
|
|
36
|
+
independent: true,
|
|
33
37
|
layers: vec!["domain"],
|
|
34
38
|
},
|
|
35
39
|
Level {
|
|
40
|
+
independent: true,
|
|
36
41
|
layers: vec!["data"],
|
|
37
42
|
},
|
|
38
43
|
];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
__version__ = "3.
|
|
1
|
+
__version__ = "3.2"
|
|
2
2
|
|
|
3
3
|
from .application.ports.graph import DetailedImport, ImportGraph
|
|
4
4
|
from .domain.analysis import PackageDependency, Route
|
|
5
|
-
from .domain.valueobjects import DirectImport, Module
|
|
5
|
+
from .domain.valueobjects import DirectImport, Module, Layer
|
|
6
6
|
from .main import build_graph
|
|
7
7
|
|
|
8
8
|
__all__ = [
|
|
@@ -13,4 +13,5 @@ __all__ = [
|
|
|
13
13
|
"PackageDependency",
|
|
14
14
|
"Route",
|
|
15
15
|
"build_graph",
|
|
16
|
+
"Layer",
|
|
16
17
|
]
|
|
@@ -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)
|
|
@@ -21,24 +21,28 @@ class CacheFileNamer:
|
|
|
21
21
|
|
|
22
22
|
@classmethod
|
|
23
23
|
def make_data_file_name(
|
|
24
|
-
cls,
|
|
24
|
+
cls,
|
|
25
|
+
found_packages: Set[FoundPackage],
|
|
26
|
+
include_external_packages: bool,
|
|
27
|
+
exclude_type_checking_imports: bool,
|
|
25
28
|
) -> str:
|
|
26
29
|
identifier = cls.make_data_file_unique_string(
|
|
27
|
-
found_packages, include_external_packages
|
|
30
|
+
found_packages, include_external_packages, exclude_type_checking_imports
|
|
28
31
|
)
|
|
29
32
|
|
|
30
33
|
bytes_identifier = identifier.encode()
|
|
31
34
|
# Use a hash algorithm with a limited size to avoid cache filenames that are too long
|
|
32
35
|
# the filesystem, which can happen if there are more than a few root packages
|
|
33
36
|
# being analyzed.
|
|
34
|
-
safe_unicode_identifier = hashlib.blake2b(
|
|
35
|
-
bytes_identifier, digest_size=20
|
|
36
|
-
).hexdigest()
|
|
37
|
+
safe_unicode_identifier = hashlib.blake2b(bytes_identifier, digest_size=20).hexdigest()
|
|
37
38
|
return f"{safe_unicode_identifier}.data.json"
|
|
38
39
|
|
|
39
40
|
@classmethod
|
|
40
41
|
def make_data_file_unique_string(
|
|
41
|
-
cls,
|
|
42
|
+
cls,
|
|
43
|
+
found_packages: Set[FoundPackage],
|
|
44
|
+
include_external_packages: bool,
|
|
45
|
+
exclude_type_checking_imports: bool,
|
|
42
46
|
) -> str:
|
|
43
47
|
"""
|
|
44
48
|
Construct a unique string that identifies the analysis parameters.
|
|
@@ -47,10 +51,13 @@ class CacheFileNamer:
|
|
|
47
51
|
"""
|
|
48
52
|
package_names = (p.name for p in found_packages)
|
|
49
53
|
csv_packages = ",".join(sorted(package_names))
|
|
50
|
-
include_external_packages_option =
|
|
51
|
-
|
|
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
|
|
52
60
|
)
|
|
53
|
-
return csv_packages + include_external_packages_option
|
|
54
61
|
|
|
55
62
|
|
|
56
63
|
class Cache(AbstractCache):
|
|
@@ -71,6 +78,7 @@ class Cache(AbstractCache):
|
|
|
71
78
|
file_system: AbstractFileSystem,
|
|
72
79
|
found_packages: Set[FoundPackage],
|
|
73
80
|
include_external_packages: bool,
|
|
81
|
+
exclude_type_checking_imports: bool = False,
|
|
74
82
|
cache_dir: Optional[str] = None,
|
|
75
83
|
namer: Type[CacheFileNamer] = CacheFileNamer,
|
|
76
84
|
) -> "Cache":
|
|
@@ -78,6 +86,7 @@ class Cache(AbstractCache):
|
|
|
78
86
|
file_system=file_system,
|
|
79
87
|
found_packages=found_packages,
|
|
80
88
|
include_external_packages=include_external_packages,
|
|
89
|
+
exclude_type_checking_imports=exclude_type_checking_imports,
|
|
81
90
|
cache_dir=cls.cache_dir_or_default(cache_dir),
|
|
82
91
|
namer=namer,
|
|
83
92
|
)
|
|
@@ -132,6 +141,7 @@ class Cache(AbstractCache):
|
|
|
132
141
|
self._namer.make_data_file_name(
|
|
133
142
|
found_packages=self.found_packages,
|
|
134
143
|
include_external_packages=self.include_external_packages,
|
|
144
|
+
exclude_type_checking_imports=self.exclude_type_checking_imports,
|
|
135
145
|
),
|
|
136
146
|
)
|
|
137
147
|
self.file_system.write(data_cache_filename, serialized)
|
|
@@ -185,6 +195,7 @@ class Cache(AbstractCache):
|
|
|
185
195
|
self._namer.make_data_file_name(
|
|
186
196
|
found_packages=self.found_packages,
|
|
187
197
|
include_external_packages=self.include_external_packages,
|
|
198
|
+
exclude_type_checking_imports=self.exclude_type_checking_imports,
|
|
188
199
|
),
|
|
189
200
|
)
|
|
190
201
|
try:
|
|
@@ -201,9 +212,7 @@ class Cache(AbstractCache):
|
|
|
201
212
|
logger.warning(f"Could not use corrupt cache file {data_cache_filename}.")
|
|
202
213
|
return {}
|
|
203
214
|
|
|
204
|
-
primitives_map: PrimitiveFormat = self._to_primitives_data_map(
|
|
205
|
-
deserialized_json
|
|
206
|
-
)
|
|
215
|
+
primitives_map: PrimitiveFormat = self._to_primitives_data_map(deserialized_json)
|
|
207
216
|
|
|
208
217
|
return {
|
|
209
218
|
Module(name=name): {
|
|
@@ -6,7 +6,7 @@ from typing import Dict, List, Optional, Sequence, Set, Tuple, cast
|
|
|
6
6
|
from grimp.algorithms.shortest_path import bidirectional_shortest_path
|
|
7
7
|
from grimp.application.ports import graph
|
|
8
8
|
from grimp.domain.analysis import PackageDependency
|
|
9
|
-
from grimp.domain.valueobjects import Module
|
|
9
|
+
from grimp.domain.valueobjects import Module, Layer
|
|
10
10
|
from grimp.exceptions import ModuleNotPresent
|
|
11
11
|
|
|
12
12
|
from . import _layers
|
|
@@ -365,9 +365,10 @@ class ImportGraph(graph.ImportGraph):
|
|
|
365
365
|
|
|
366
366
|
def find_illegal_dependencies_for_layers(
|
|
367
367
|
self,
|
|
368
|
-
layers: Sequence[str | set[str]],
|
|
369
|
-
containers:
|
|
368
|
+
layers: Sequence[Layer | str | set[str]],
|
|
369
|
+
containers: set[str] | None = None,
|
|
370
370
|
) -> set[PackageDependency]:
|
|
371
|
+
layers = _layers.parse_layers(layers)
|
|
371
372
|
return _layers.find_illegal_dependencies(
|
|
372
373
|
graph=self, layers=layers, containers=containers or set()
|
|
373
374
|
)
|