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.
Files changed (44) hide show
  1. {grimp-3.0b3 → grimp-3.2}/PKG-INFO +2 -1
  2. {grimp-3.0b3 → grimp-3.2}/pyproject.toml +4 -2
  3. {grimp-3.0b3 → grimp-3.2}/rust/src/layers.rs +115 -7
  4. {grimp-3.0b3 → grimp-3.2}/rust/src/lib.rs +58 -10
  5. {grimp-3.0b3 → grimp-3.2}/rust/tests/large.rs +5 -0
  6. {grimp-3.0b3 → grimp-3.2}/src/grimp/__init__.py +3 -2
  7. grimp-3.2/src/grimp/adaptors/_layers.py +174 -0
  8. {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/caching.py +21 -12
  9. {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/graph.py +4 -3
  10. {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/importscanner.py +116 -137
  11. {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/modulefinder.py +7 -4
  12. {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/packagefinder.py +1 -3
  13. {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/caching.py +5 -0
  14. {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/graph.py +15 -10
  15. {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/importscanner.py +9 -7
  16. {grimp-3.0b3 → grimp-3.2}/src/grimp/application/usecases.py +12 -8
  17. {grimp-3.0b3 → grimp-3.2}/src/grimp/domain/valueobjects.py +21 -0
  18. {grimp-3.0b3 → grimp-3.2}/src/grimp/exceptions.py +1 -3
  19. grimp-3.0b3/src/grimp/adaptors/_layers.py +0 -354
  20. {grimp-3.0b3 → grimp-3.2}/README.rst +0 -0
  21. {grimp-3.0b3 → grimp-3.2}/rust/.cargo/config +0 -0
  22. {grimp-3.0b3 → grimp-3.2}/rust/Cargo.lock +0 -0
  23. {grimp-3.0b3 → grimp-3.2}/rust/Cargo.toml +0 -0
  24. {grimp-3.0b3 → grimp-3.2}/rust/src/containers.rs +0 -0
  25. {grimp-3.0b3 → grimp-3.2}/rust/src/dependencies.rs +0 -0
  26. {grimp-3.0b3 → grimp-3.2}/rust/src/importgraph.rs +0 -0
  27. {grimp-3.0b3 → grimp-3.2}/rust/tests/large_graph.json +0 -0
  28. {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/__init__.py +0 -0
  29. {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/filesystem.py +0 -0
  30. {grimp-3.0b3 → grimp-3.2}/src/grimp/adaptors/timing.py +0 -0
  31. {grimp-3.0b3 → grimp-3.2}/src/grimp/algorithms/__init__.py +0 -0
  32. {grimp-3.0b3 → grimp-3.2}/src/grimp/algorithms/shortest_path.py +0 -0
  33. {grimp-3.0b3 → grimp-3.2}/src/grimp/application/__init__.py +0 -0
  34. {grimp-3.0b3 → grimp-3.2}/src/grimp/application/config.py +0 -0
  35. {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/__init__.py +0 -0
  36. {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/filesystem.py +0 -0
  37. {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/modulefinder.py +0 -0
  38. {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/packagefinder.py +0 -0
  39. {grimp-3.0b3 → grimp-3.2}/src/grimp/application/ports/timing.py +0 -0
  40. {grimp-3.0b3 → grimp-3.2}/src/grimp/domain/__init__.py +0 -0
  41. {grimp-3.0b3 → grimp-3.2}/src/grimp/domain/analysis.py +0 -0
  42. {grimp-3.0b3 → grimp-3.2}/src/grimp/helpers.py +0 -0
  43. {grimp-3.0b3 → grimp-3.2}/src/grimp/main.py +0 -0
  44. {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.0b3
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.0b3"
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. That
73
- // includes lower layers and siblings.
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
- for potential_sibling_layer in &higher_level.layers {
76
- if potential_sibling_layer != higher_layer {
77
- // It's a sibling layer.
78
- layers_forbidden_to_import_higher_layer.push(potential_sibling_layer);
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 layers: HashSet<&str> = level_python.extract().unwrap();
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
- use pyo3::types::{PySet, PyTuple};
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<&PySet> = vec![
114
- PySet::new(py, &vec!["high"]).unwrap(),
115
- PySet::new(py, &vec!["medium"]).unwrap(),
116
- PySet::new(py, &vec!["low"]).unwrap(),
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<&PySet> = vec![
147
- PySet::new(py, &vec!["high"]).unwrap(),
148
- PySet::new(py, &vec!["blue", "green", "orange"]).unwrap(),
149
- PySet::new(py, &vec!["low"]).unwrap(),
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.0b3"
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, found_packages: Set[FoundPackage], include_external_packages: bool
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, found_packages: Set[FoundPackage], include_external_packages: bool
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
- ":external" if include_external_packages else ""
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: Optional[set[str]] = None,
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
  )