grimp 3.1__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.1 → grimp-3.2}/PKG-INFO +2 -1
  2. {grimp-3.1 → grimp-3.2}/pyproject.toml +2 -1
  3. {grimp-3.1 → grimp-3.2}/rust/src/layers.rs +115 -7
  4. {grimp-3.1 → grimp-3.2}/rust/src/lib.rs +58 -10
  5. {grimp-3.1 → grimp-3.2}/rust/tests/large.rs +5 -0
  6. {grimp-3.1 → grimp-3.2}/src/grimp/__init__.py +3 -2
  7. grimp-3.2/src/grimp/adaptors/_layers.py +174 -0
  8. {grimp-3.1 → grimp-3.2}/src/grimp/adaptors/graph.py +4 -3
  9. {grimp-3.1 → grimp-3.2}/src/grimp/adaptors/modulefinder.py +4 -1
  10. {grimp-3.1 → grimp-3.2}/src/grimp/application/ports/graph.py +15 -10
  11. {grimp-3.1 → grimp-3.2}/src/grimp/domain/valueobjects.py +20 -0
  12. grimp-3.1/src/grimp/adaptors/_layers.py +0 -353
  13. {grimp-3.1 → grimp-3.2}/README.rst +0 -0
  14. {grimp-3.1 → grimp-3.2}/rust/.cargo/config +0 -0
  15. {grimp-3.1 → grimp-3.2}/rust/Cargo.lock +0 -0
  16. {grimp-3.1 → grimp-3.2}/rust/Cargo.toml +0 -0
  17. {grimp-3.1 → grimp-3.2}/rust/src/containers.rs +0 -0
  18. {grimp-3.1 → grimp-3.2}/rust/src/dependencies.rs +0 -0
  19. {grimp-3.1 → grimp-3.2}/rust/src/importgraph.rs +0 -0
  20. {grimp-3.1 → grimp-3.2}/rust/tests/large_graph.json +0 -0
  21. {grimp-3.1 → grimp-3.2}/src/grimp/adaptors/__init__.py +0 -0
  22. {grimp-3.1 → grimp-3.2}/src/grimp/adaptors/caching.py +0 -0
  23. {grimp-3.1 → grimp-3.2}/src/grimp/adaptors/filesystem.py +0 -0
  24. {grimp-3.1 → grimp-3.2}/src/grimp/adaptors/importscanner.py +0 -0
  25. {grimp-3.1 → grimp-3.2}/src/grimp/adaptors/packagefinder.py +0 -0
  26. {grimp-3.1 → grimp-3.2}/src/grimp/adaptors/timing.py +0 -0
  27. {grimp-3.1 → grimp-3.2}/src/grimp/algorithms/__init__.py +0 -0
  28. {grimp-3.1 → grimp-3.2}/src/grimp/algorithms/shortest_path.py +0 -0
  29. {grimp-3.1 → grimp-3.2}/src/grimp/application/__init__.py +0 -0
  30. {grimp-3.1 → grimp-3.2}/src/grimp/application/config.py +0 -0
  31. {grimp-3.1 → grimp-3.2}/src/grimp/application/ports/__init__.py +0 -0
  32. {grimp-3.1 → grimp-3.2}/src/grimp/application/ports/caching.py +0 -0
  33. {grimp-3.1 → grimp-3.2}/src/grimp/application/ports/filesystem.py +0 -0
  34. {grimp-3.1 → grimp-3.2}/src/grimp/application/ports/importscanner.py +0 -0
  35. {grimp-3.1 → grimp-3.2}/src/grimp/application/ports/modulefinder.py +0 -0
  36. {grimp-3.1 → grimp-3.2}/src/grimp/application/ports/packagefinder.py +0 -0
  37. {grimp-3.1 → grimp-3.2}/src/grimp/application/ports/timing.py +0 -0
  38. {grimp-3.1 → grimp-3.2}/src/grimp/application/usecases.py +0 -0
  39. {grimp-3.1 → grimp-3.2}/src/grimp/domain/__init__.py +0 -0
  40. {grimp-3.1 → grimp-3.2}/src/grimp/domain/analysis.py +0 -0
  41. {grimp-3.1 → grimp-3.2}/src/grimp/exceptions.py +0 -0
  42. {grimp-3.1 → grimp-3.2}/src/grimp/helpers.py +0 -0
  43. {grimp-3.1 → grimp-3.2}/src/grimp/main.py +0 -0
  44. {grimp-3.1 → 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.1
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.1"
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",
@@ -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.1"
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)
@@ -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
  )
@@ -74,6 +74,9 @@ class ModuleFinder(modulefinder.AbstractModuleFinder):
74
74
  if filename.startswith("."):
75
75
  return False
76
76
 
77
+ if not filename.endswith(".py"):
78
+ return False
79
+
77
80
  # Ignore files like some.module.py.
78
81
  if filename.count(".") > 1:
79
82
  logger.warning(
@@ -82,7 +85,7 @@ class ModuleFinder(modulefinder.AbstractModuleFinder):
82
85
  )
83
86
  return False
84
87
 
85
- return filename.endswith(".py")
88
+ return True
86
89
 
87
90
  def _module_name_from_filename(
88
91
  self, package_name: str, filename_and_path: str, package_directory: str
@@ -1,11 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import abc
4
- from typing import Iterator, List, Optional, Sequence, Set, Tuple, Union
4
+ from typing import Iterator, List, Optional, Sequence, Set, Tuple
5
5
 
6
6
  from typing_extensions import TypedDict
7
7
 
8
8
  from grimp.domain.analysis import PackageDependency
9
+ from grimp.domain.valueobjects import Layer
9
10
 
10
11
 
11
12
  class DetailedImport(TypedDict):
@@ -271,25 +272,29 @@ class ImportGraph(abc.ABC):
271
272
 
272
273
  def find_illegal_dependencies_for_layers(
273
274
  self,
274
- layers: Sequence[Union[str, set[str]]],
275
- containers: Optional[set[str]] = None,
275
+ layers: Sequence[Layer | str | set[str]],
276
+ containers: set[str] | None = None,
276
277
  ) -> set[PackageDependency]:
277
278
  """
278
279
  Find dependencies that don't conform to the supplied layered architecture.
279
280
 
280
- 'Layers' is an architectural pattern in which a list of sibling modules/packages
281
+ 'Layers' is an architectural pattern in which a list of modules/packages
281
282
  have a dependency direction from high to low. In other words, a higher layer would
282
283
  be allowed to import a lower layer, but not the other way around.
283
284
 
284
- Additionally, multiple layers can be grouped together at the same level; for example
285
- `mypackage.utils` and `mypackage.logging` might sit at the bottom, so they cannot
286
- import from any other layers. Layers at the same level must be independent, so any
287
- dependencies in either direction will be treated as illegal.
285
+ Additionally, multiple modules can be grouped together at the same layer;
286
+ for example `mypackage.utils` and `mypackage.logging` might sit at the bottom, so they
287
+ cannot import from any other layers. To specify that multiple modules should
288
+ be treated as siblings within a single layer, pass a `Layer`. The `Layer.independent`
289
+ field can be used to specify whether the sibling modules should be treated as independent
290
+ - should imports between sibling modules be forbidden (default) or allowed? For backwards
291
+ compatibility it is also possible to pass a simple `set[str]` to describe a layer. In this
292
+ case the sibling modules within the layer will be considered independent.
288
293
 
289
294
  Arguments:
290
295
 
291
- - layers: A sequence, each element of which consists either of the name of a layer
292
- module, or a set of sibling layers that at the same level. If containers
296
+ - layers: A sequence, each element of which consists either of a `Layer`, the name
297
+ of a layer module or a set of sibling modules. If containers
293
298
  are also specified, then these names must be relative to the container.
294
299
  The order is from higher to lower level layers. Any layers that don't
295
300
  exist in the graph will be ignored.
@@ -84,3 +84,23 @@ class DirectImport(ValueObject):
84
84
 
85
85
  def __hash__(self) -> int:
86
86
  return hash((str(self), self.line_contents))
87
+
88
+
89
+ class Layer(ValueObject):
90
+ """
91
+ A layer within a layered architecture.
92
+
93
+ If layer.independent is True then the modules within the layer are considered
94
+ independent. This is the default.
95
+ """
96
+
97
+ def __init__(
98
+ self,
99
+ *module_tails: str,
100
+ independent: bool = True,
101
+ ) -> None:
102
+ self.module_tails = set(module_tails)
103
+ self.independent = independent
104
+
105
+ def __str__(self) -> str:
106
+ return f"{self.module_tails}, independent={self.independent}"
@@ -1,353 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import copy
4
- from dataclasses import dataclass
5
- from typing import TYPE_CHECKING, Any, Iterator, Sequence, TypedDict, Union
6
-
7
- from grimp import Route
8
- from grimp import _rustgrimp as rust # type: ignore[attr-defined]
9
-
10
- if TYPE_CHECKING:
11
- from grimp.adaptors.graph import ImportGraph
12
-
13
- from grimp.domain.analysis import PackageDependency
14
- from grimp.exceptions import NoSuchContainer
15
-
16
-
17
- def find_illegal_dependencies(
18
- graph: ImportGraph,
19
- layers: Sequence[Union[str, set[str]]],
20
- containers: set[str],
21
- ) -> set[PackageDependency]:
22
- """
23
- Find dependencies that don't conform to the supplied layered architecture.
24
-
25
- See ImportGraph.find_illegal_dependencies_for_layers.
26
-
27
- The only difference between this and the method is that the containers passed in
28
- is already a (potentially empty) set.
29
- """
30
- try:
31
- rust_package_dependency_tuple = rust.find_illegal_dependencies(
32
- levels=_layers_to_levels(layers),
33
- containers=set(containers),
34
- importeds_by_importer=graph._importeds_by_importer,
35
- )
36
- except rust.NoSuchContainer as e:
37
- raise NoSuchContainer(str(e))
38
-
39
- rust_package_dependencies = _dependencies_from_tuple(rust_package_dependency_tuple)
40
- return rust_package_dependencies
41
-
42
-
43
- class _RustRoute(TypedDict):
44
- heads: frozenset[str]
45
- middle: tuple[str, ...]
46
- tails: frozenset[str]
47
-
48
-
49
- class _RustPackageDependency(TypedDict):
50
- importer: str
51
- imported: str
52
- routes: tuple[_RustRoute, ...]
53
-
54
-
55
- def _layers_to_levels(layers: Sequence[Union[str, set[str]]]) -> tuple[set[str], ...]:
56
- """
57
- Convert any standalone layers to a one-element level.
58
- """
59
- return tuple({layer} if isinstance(layer, str) else set(layer) for layer in layers)
60
-
61
-
62
- def _dependencies_from_tuple(
63
- rust_package_dependency_tuple: tuple[_RustPackageDependency, ...]
64
- ) -> set[PackageDependency]:
65
- return {
66
- PackageDependency(
67
- imported=dep_dict["imported"],
68
- importer=dep_dict["importer"],
69
- routes=frozenset(
70
- {
71
- Route(
72
- heads=route_dict["heads"],
73
- middle=route_dict["middle"],
74
- tails=route_dict["tails"],
75
- )
76
- for route_dict in dep_dict["routes"]
77
- }
78
- ),
79
- )
80
- for dep_dict in rust_package_dependency_tuple
81
- }
82
-
83
-
84
- class _Module:
85
- """
86
- A Python module.
87
- """
88
-
89
- def __init__(self, name: str) -> None:
90
- """
91
- Args:
92
- name: The fully qualified name of a Python module, e.g. 'package.foo.bar'.
93
- """
94
- self.name = name
95
-
96
- def __str__(self) -> str:
97
- return self.name
98
-
99
- def __eq__(self, other: Any) -> bool:
100
- if isinstance(other, self.__class__):
101
- return hash(self) == hash(other)
102
- else:
103
- return False
104
-
105
- def __hash__(self) -> int:
106
- return hash(str(self))
107
-
108
- def is_descendant_of(self, module: "_Module") -> bool:
109
- return self.name.startswith(f"{module.name}.")
110
-
111
-
112
- @dataclass(frozen=True)
113
- class _Link:
114
- importer: str
115
- imported: str
116
-
117
-
118
- # A chain of modules, each of which imports the next.
119
- if TYPE_CHECKING:
120
- # TODO: remove TYPE_CHECKING conditional once on Python 3.9.
121
- _Chain = tuple[str, ...]
122
-
123
-
124
- def _generate_module_permutations(
125
- graph: ImportGraph,
126
- layers: Sequence[str],
127
- containers: set[str],
128
- ) -> Iterator[tuple[_Module, _Module, str | None]]:
129
- """
130
- Return all possible combinations of higher level and lower level modules, in pairs.
131
-
132
- Each pair of modules consists of immediate children of two different layers. The first
133
- module is in a layer higher than the layer of the second module. This means the first
134
- module is allowed to import the second, but not the other way around.
135
-
136
- Returns:
137
- module_in_higher_layer, module_in_lower_layer, container
138
- """
139
- # If there are no containers, we still want to run the loop once.
140
- quasi_containers = containers or [None]
141
-
142
- for container in quasi_containers:
143
- for index, higher_layer in enumerate(layers):
144
- higher_layer_module = _module_from_layer(higher_layer, container)
145
-
146
- if higher_layer_module.name not in graph.modules:
147
- continue
148
-
149
- for lower_layer in layers[index + 1 :]:
150
- lower_layer_module = _module_from_layer(lower_layer, container)
151
-
152
- if lower_layer_module.name not in graph.modules:
153
- continue
154
-
155
- yield higher_layer_module, lower_layer_module, container
156
-
157
-
158
- def _module_from_layer(layer: str, container: str | None = None) -> _Module:
159
- if container:
160
- name = ".".join([container, layer])
161
- else:
162
- name = layer
163
- return _Module(name)
164
-
165
-
166
- def _search_for_package_dependency(
167
- higher_layer_package: _Module,
168
- lower_layer_package: _Module,
169
- layers: Sequence[str],
170
- container: str | None,
171
- graph: ImportGraph,
172
- ) -> PackageDependency | None:
173
- """
174
- Return a PackageDependency containing illegal chains between two layers, if they exist.
175
- """
176
- temp_graph = copy.deepcopy(graph)
177
- _remove_other_layers(
178
- temp_graph,
179
- layers=layers,
180
- container=container,
181
- layers_to_preserve=(higher_layer_package, lower_layer_package),
182
- )
183
- # Assemble direct imports between the layers, then remove them.
184
- import_details_between_layers = _pop_direct_imports(
185
- higher_layer_package=higher_layer_package,
186
- lower_layer_package=lower_layer_package,
187
- graph=temp_graph,
188
- )
189
- routes: set[Route] = set()
190
-
191
- for import_details_list in import_details_between_layers:
192
- any_element = tuple(import_details_list)[0]
193
- routes.add(
194
- Route(
195
- heads=frozenset({any_element.importer}),
196
- middle=(),
197
- tails=frozenset({any_element.imported}),
198
- )
199
- )
200
-
201
- indirect_routes = _get_indirect_routes(
202
- temp_graph,
203
- importer_package=lower_layer_package,
204
- imported_package=higher_layer_package,
205
- )
206
-
207
- routes |= indirect_routes
208
-
209
- if routes:
210
- return PackageDependency(
211
- importer=lower_layer_package.name,
212
- imported=higher_layer_package.name,
213
- routes=frozenset(routes),
214
- )
215
- else:
216
- return None
217
-
218
-
219
- def _get_indirect_routes(
220
- graph: ImportGraph, importer_package: _Module, imported_package: _Module
221
- ) -> set[Route]:
222
- """
223
- Squashes the two packages.
224
- Gets a list of paths between them, called segments.
225
- Add the heads and tails to the segments.
226
- """
227
- temp_graph = copy.deepcopy(graph)
228
-
229
- temp_graph.squash_module(importer_package.name)
230
- temp_graph.squash_module(imported_package.name)
231
-
232
- middles = _find_middles(
233
- temp_graph,
234
- importer=importer_package,
235
- imported=imported_package,
236
- )
237
- return _middles_to_routes(graph, middles, importer=importer_package, imported=imported_package)
238
-
239
-
240
- def _remove_other_layers(
241
- graph: ImportGraph,
242
- layers: Sequence[str],
243
- container: str | None,
244
- layers_to_preserve: tuple[_Module, ...],
245
- ) -> None:
246
- for index, layer in enumerate(layers): # type: ignore
247
- candidate_layer = _module_from_layer(layer, container)
248
- if candidate_layer.name in graph.modules and candidate_layer not in layers_to_preserve:
249
- _remove_layer(graph, layer_package=candidate_layer)
250
-
251
-
252
- def _remove_layer(graph: ImportGraph, layer_package: _Module) -> None:
253
- for module in graph.find_descendants(layer_package.name):
254
- graph.remove_module(module)
255
- graph.remove_module(layer_package.name)
256
-
257
-
258
- def _pop_direct_imports(
259
- higher_layer_package, lower_layer_package, graph: ImportGraph
260
- ) -> set[frozenset[_Link]]:
261
- import_details_set: set[frozenset[_Link]] = set()
262
-
263
- lower_layer_modules = {lower_layer_package.name} | graph.find_descendants(
264
- lower_layer_package.name
265
- )
266
- for lower_layer_module in lower_layer_modules:
267
- imported_modules = graph.find_modules_directly_imported_by(lower_layer_module).copy()
268
- for imported_module in imported_modules:
269
- if _Module(imported_module) == higher_layer_package or _Module(
270
- imported_module
271
- ).is_descendant_of(higher_layer_package):
272
- import_details = frozenset(
273
- {
274
- _Link(
275
- importer=lower_layer_module,
276
- imported=imported_module,
277
- ),
278
- }
279
- )
280
- import_details_set.add(import_details)
281
- graph.remove_import(importer=lower_layer_module, imported=imported_module)
282
- return import_details_set
283
-
284
-
285
- def _find_middles(graph: ImportGraph, importer: _Module, imported: _Module) -> set[_Chain]:
286
- """
287
- Return set of headless and tailless chains.
288
- """
289
- middles: set[_Chain] = set()
290
-
291
- for chain in _pop_shortest_chains(graph, importer=importer.name, imported=imported.name):
292
- if len(chain) == 2:
293
- raise ValueError("Direct chain found - these should have been removed.")
294
- middles.add(chain[1:-1])
295
-
296
- return middles
297
-
298
-
299
- def _middles_to_routes(
300
- graph: ImportGraph, middles: set[_Chain], importer: _Module, imported: _Module
301
- ) -> set[Route]:
302
- """
303
- Build a set of routes from the chains between one package and another.
304
-
305
- The middles are the chains that exist from the importer package to
306
- the importer package. This function works out the head and tail packages of
307
- those chains by consulting the graph.
308
- """
309
- routes: set[Route] = set()
310
-
311
- for middle in middles:
312
- heads: set[str] = set()
313
- imported_module = middle[0]
314
- candidate_modules = sorted(graph.find_modules_that_directly_import(imported_module))
315
- for module in [
316
- m
317
- for m in candidate_modules
318
- if _Module(m) == importer or _Module(m).is_descendant_of(importer)
319
- ]:
320
- heads.add(module)
321
-
322
- tails: set[str] = set()
323
- importer_module = middle[-1]
324
- candidate_modules = sorted(graph.find_modules_directly_imported_by(importer_module))
325
- for module in [
326
- m
327
- for m in candidate_modules
328
- if _Module(m) == imported or _Module(m).is_descendant_of(imported)
329
- ]:
330
- tails.add(module)
331
-
332
- routes.add(
333
- Route(
334
- heads=frozenset(heads),
335
- middle=middle,
336
- tails=frozenset(tails),
337
- )
338
- )
339
-
340
- return routes
341
-
342
-
343
- def _pop_shortest_chains(
344
- graph: ImportGraph, importer: str, imported: str
345
- ) -> Iterator[tuple[str, ...]]:
346
- chain: tuple[str, ...] | bool | None = True
347
- while chain:
348
- chain = graph.find_shortest_chain(importer, imported)
349
- if chain:
350
- # Remove chain of imports from graph.
351
- for index in range(len(chain) - 1):
352
- graph.remove_import(importer=chain[index], imported=chain[index + 1])
353
- yield chain
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