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.
Files changed (50) hide show
  1. {grimp-3.12 → grimp-3.13}/PKG-INFO +12 -6
  2. {grimp-3.12 → grimp-3.13}/README.rst +10 -4
  3. {grimp-3.12 → grimp-3.13}/pyproject.toml +3 -3
  4. grimp-3.13/rust/src/graph/cycle_breakers.rs +249 -0
  5. {grimp-3.12 → grimp-3.13}/rust/src/graph/direct_import_queries.rs +40 -8
  6. {grimp-3.12 → grimp-3.13}/rust/src/graph/mod.rs +30 -0
  7. {grimp-3.12 → grimp-3.13}/src/grimp/__init__.py +2 -2
  8. {grimp-3.12/src/grimp/application/ports → grimp-3.13/src/grimp/application}/graph.py +176 -64
  9. {grimp-3.12 → grimp-3.13}/src/grimp/application/usecases.py +1 -1
  10. {grimp-3.12 → grimp-3.13}/src/grimp/main.py +1 -1
  11. grimp-3.12/src/grimp/adaptors/graph.py +0 -233
  12. {grimp-3.12 → grimp-3.13}/rust/.cargo/config.toml +0 -0
  13. {grimp-3.12 → grimp-3.13}/rust/Cargo.lock +0 -0
  14. {grimp-3.12 → grimp-3.13}/rust/Cargo.toml +0 -0
  15. {grimp-3.12 → grimp-3.13}/rust/src/caching.rs +0 -0
  16. {grimp-3.12 → grimp-3.13}/rust/src/errors.rs +0 -0
  17. {grimp-3.12 → grimp-3.13}/rust/src/exceptions.rs +0 -0
  18. {grimp-3.12 → grimp-3.13}/rust/src/filesystem.rs +0 -0
  19. {grimp-3.12 → grimp-3.13}/rust/src/graph/graph_manipulation.rs +0 -0
  20. {grimp-3.12 → grimp-3.13}/rust/src/graph/hierarchy_queries.rs +0 -0
  21. {grimp-3.12 → grimp-3.13}/rust/src/graph/higher_order_queries.rs +0 -0
  22. {grimp-3.12 → grimp-3.13}/rust/src/graph/import_chain_queries.rs +0 -0
  23. {grimp-3.12 → grimp-3.13}/rust/src/graph/pathfinding.rs +0 -0
  24. {grimp-3.12 → grimp-3.13}/rust/src/import_parsing.rs +0 -0
  25. {grimp-3.12 → grimp-3.13}/rust/src/import_scanning.rs +0 -0
  26. {grimp-3.12 → grimp-3.13}/rust/src/lib.rs +0 -0
  27. {grimp-3.12 → grimp-3.13}/rust/src/module_expressions.rs +0 -0
  28. {grimp-3.12 → grimp-3.13}/rust/src/module_finding.rs +0 -0
  29. {grimp-3.12 → grimp-3.13}/rust/tests/large.rs +0 -0
  30. {grimp-3.12 → grimp-3.13}/rust/tests/large_graph.json +0 -0
  31. {grimp-3.12 → grimp-3.13}/src/grimp/adaptors/__init__.py +0 -0
  32. {grimp-3.12 → grimp-3.13}/src/grimp/adaptors/caching.py +0 -0
  33. {grimp-3.12 → grimp-3.13}/src/grimp/adaptors/filesystem.py +0 -0
  34. {grimp-3.12 → grimp-3.13}/src/grimp/adaptors/modulefinder.py +0 -0
  35. {grimp-3.12 → grimp-3.13}/src/grimp/adaptors/packagefinder.py +0 -0
  36. {grimp-3.12 → grimp-3.13}/src/grimp/adaptors/timing.py +0 -0
  37. {grimp-3.12 → grimp-3.13}/src/grimp/application/__init__.py +0 -0
  38. {grimp-3.12 → grimp-3.13}/src/grimp/application/config.py +0 -0
  39. {grimp-3.12 → grimp-3.13}/src/grimp/application/ports/__init__.py +0 -0
  40. {grimp-3.12 → grimp-3.13}/src/grimp/application/ports/caching.py +0 -0
  41. {grimp-3.12 → grimp-3.13}/src/grimp/application/ports/filesystem.py +0 -0
  42. {grimp-3.12 → grimp-3.13}/src/grimp/application/ports/modulefinder.py +0 -0
  43. {grimp-3.12 → grimp-3.13}/src/grimp/application/ports/packagefinder.py +0 -0
  44. {grimp-3.12 → grimp-3.13}/src/grimp/application/ports/timing.py +0 -0
  45. {grimp-3.12 → grimp-3.13}/src/grimp/application/scanning.py +0 -0
  46. {grimp-3.12 → grimp-3.13}/src/grimp/domain/__init__.py +0 -0
  47. {grimp-3.12 → grimp-3.13}/src/grimp/domain/analysis.py +0 -0
  48. {grimp-3.12 → grimp-3.13}/src/grimp/domain/valueobjects.py +0 -0
  49. {grimp-3.12 → grimp-3.13}/src/grimp/exceptions.py +0 -0
  50. {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.12
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/seddonym/grimp/
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/seddonym/grimp/workflows/CI/badge.svg?branch=main
43
- :target: https://github.com/seddonym/grimp/actions?workflow=CI
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
- Builds a queryable graph of the imports within one or more Python packages.
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
- * Free software: BSD license
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/seddonym/grimp/workflows/CI/badge.svg?branch=main
13
- :target: https://github.com/seddonym/grimp/actions?workflow=CI
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
- Builds a queryable graph of the imports within one or more Python packages.
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
- * Free software: BSD license
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.12"
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/seddonym/grimp/"
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>=3.2.0",
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 importer: FxHashSet<_> = importer.into();
21
- let mut imported: FxHashSet<_> = imported.into();
20
+ let mut importers: FxHashSet<_> = importer.into();
21
+ let mut importeds: FxHashSet<_> = imported.into();
22
22
  if as_packages {
23
- importer.extend_with_descendants(self);
24
- imported.extend_with_descendants(self);
25
- if !(&importer & &imported).is_empty() {
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 = importer
30
+ let direct_imports = importers
31
31
  .iter()
32
- .flat_map(|module| self.imports.get(*module).unwrap().iter().cloned())
32
+ .flat_map(|importer_module| self.imports.get(*importer_module).unwrap().iter().cloned())
33
33
  .collect::<FxHashSet<ModuleToken>>();
34
34
 
35
- Ok(!(&direct_imports & &imported).is_empty())
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.12"
1
+ __version__ = "3.13"
2
2
 
3
- from .application.ports.graph import DetailedImport, ImportGraph, Import
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 abc
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(abc.ABC):
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
244
+ return self._rustgraph.get_import_details(
245
+ importer=importer,
246
+ imported=imported,
247
+ )
216
248
 
217
- @abc.abstractmethod
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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.ports.graph import ImportGraph
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 .adaptors.graph import ImportGraph
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