grimp 3.18__tar.gz → 3.20__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 (53) hide show
  1. grimp-3.20/AUTHORS.rst +16 -0
  2. grimp-3.20/LICENSE +21 -0
  3. {grimp-3.18 → grimp-3.20}/PKG-INFO +2 -3
  4. {grimp-3.18 → grimp-3.20}/pyproject.toml +3 -3
  5. {grimp-3.18 → grimp-3.20}/rust/src/graph/cycle_breakers.rs +50 -22
  6. grimp-3.20/rust/src/graph/direct_import_queries.rs +328 -0
  7. {grimp-3.18 → grimp-3.20}/rust/src/graph/graph_manipulation.rs +4 -12
  8. {grimp-3.18 → grimp-3.20}/rust/src/graph/hierarchy_queries.rs +15 -6
  9. {grimp-3.18 → grimp-3.20}/rust/src/graph/mod.rs +50 -25
  10. {grimp-3.18 → grimp-3.20}/src/grimp/__init__.py +1 -1
  11. {grimp-3.18 → grimp-3.20}/src/grimp/application/graph.py +28 -10
  12. {grimp-3.18 → grimp-3.20}/src/grimp/application/ports/timing.py +2 -1
  13. grimp-3.18/rust/src/graph/direct_import_queries.rs +0 -158
  14. {grimp-3.18 → grimp-3.20}/README.rst +0 -0
  15. {grimp-3.18 → grimp-3.20}/rust/Cargo.lock +0 -0
  16. {grimp-3.18 → grimp-3.20}/rust/Cargo.toml +0 -0
  17. {grimp-3.18 → grimp-3.20}/rust/build.rs +0 -0
  18. {grimp-3.18 → grimp-3.20}/rust/rust-toolchain.toml +0 -0
  19. {grimp-3.18 → grimp-3.20}/rust/src/caching.rs +0 -0
  20. {grimp-3.18 → grimp-3.20}/rust/src/errors.rs +0 -0
  21. {grimp-3.18 → grimp-3.20}/rust/src/exceptions.rs +0 -0
  22. {grimp-3.18 → grimp-3.20}/rust/src/filesystem.rs +0 -0
  23. {grimp-3.18 → grimp-3.20}/rust/src/graph/higher_order_queries.rs +0 -0
  24. {grimp-3.18 → grimp-3.20}/rust/src/graph/import_chain_queries.rs +0 -0
  25. {grimp-3.18 → grimp-3.20}/rust/src/graph/pathfinding.rs +0 -0
  26. {grimp-3.18 → grimp-3.20}/rust/src/import_parsing.rs +0 -0
  27. {grimp-3.18 → grimp-3.20}/rust/src/import_scanning.rs +0 -0
  28. {grimp-3.18 → grimp-3.20}/rust/src/lib.rs +0 -0
  29. {grimp-3.18 → grimp-3.20}/rust/src/module_expressions.rs +0 -0
  30. {grimp-3.18 → grimp-3.20}/rust/src/module_finding.rs +0 -0
  31. {grimp-3.18 → grimp-3.20}/rust/tests/large.rs +0 -0
  32. {grimp-3.18 → grimp-3.20}/rust/tests/large_graph.json +0 -0
  33. {grimp-3.18 → grimp-3.20}/src/grimp/adaptors/__init__.py +0 -0
  34. {grimp-3.18 → grimp-3.20}/src/grimp/adaptors/caching.py +0 -0
  35. {grimp-3.18 → grimp-3.20}/src/grimp/adaptors/filesystem.py +0 -0
  36. {grimp-3.18 → grimp-3.20}/src/grimp/adaptors/modulefinder.py +0 -0
  37. {grimp-3.18 → grimp-3.20}/src/grimp/adaptors/packagefinder.py +0 -0
  38. {grimp-3.18 → grimp-3.20}/src/grimp/adaptors/timing.py +0 -0
  39. {grimp-3.18 → grimp-3.20}/src/grimp/application/__init__.py +0 -0
  40. {grimp-3.18 → grimp-3.20}/src/grimp/application/config.py +0 -0
  41. {grimp-3.18 → grimp-3.20}/src/grimp/application/ports/__init__.py +0 -0
  42. {grimp-3.18 → grimp-3.20}/src/grimp/application/ports/caching.py +0 -0
  43. {grimp-3.18 → grimp-3.20}/src/grimp/application/ports/filesystem.py +0 -0
  44. {grimp-3.18 → grimp-3.20}/src/grimp/application/ports/modulefinder.py +0 -0
  45. {grimp-3.18 → grimp-3.20}/src/grimp/application/ports/packagefinder.py +0 -0
  46. {grimp-3.18 → grimp-3.20}/src/grimp/application/scanning.py +0 -0
  47. {grimp-3.18 → grimp-3.20}/src/grimp/application/usecases.py +0 -0
  48. {grimp-3.18 → grimp-3.20}/src/grimp/domain/__init__.py +0 -0
  49. {grimp-3.18 → grimp-3.20}/src/grimp/domain/analysis.py +0 -0
  50. {grimp-3.18 → grimp-3.20}/src/grimp/domain/valueobjects.py +0 -0
  51. {grimp-3.18 → grimp-3.20}/src/grimp/exceptions.py +0 -0
  52. {grimp-3.18 → grimp-3.20}/src/grimp/main.py +0 -0
  53. {grimp-3.18 → grimp-3.20}/src/grimp/py.typed +0 -0
grimp-3.20/AUTHORS.rst ADDED
@@ -0,0 +1,16 @@
1
+
2
+ Authors
3
+ =======
4
+
5
+ * David Seddon - https://seddonym.me
6
+ * Kevin Amado - https://github.com/kamadorueda
7
+ * Matthew Gamble - https://github.com/mwgamble
8
+ * NetworkX developers - The shortest path algorithm was adapted from the NetworkX library https://networkx.org/.
9
+ * Peter Byfield - https://github.com/Peter554
10
+ * Shane Smiskol - https://github.com/sshane
11
+ * Andreas Rammhold - https://github.com/andir
12
+ * Nicholas Bunn - https://github.com/NicholasBunn
13
+ * Nathan McDougall - https://github.com/nathanjmcdougall
14
+ * Oleksandr Zaiats - https://github.com/z4y4ts
15
+ * Nikhil Dabas - https://github.com/ndabas
16
+ * Pierre-Yves Le Borgne - https://github.com/pylaterreur
grimp-3.20/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2018, David Seddon
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
7
+ conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
10
+ disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
13
+ disclaimer in the documentation and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
16
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
19
+ USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
20
+ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
21
+ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grimp
3
- Version: 3.18
3
+ Version: 3.20
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: BSD License
@@ -8,7 +8,6 @@ Classifier: Operating System :: Unix
8
8
  Classifier: Operating System :: POSIX
9
9
  Classifier: Operating System :: Microsoft :: Windows
10
10
  Classifier: Programming Language :: Python
11
- Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
14
13
  Classifier: Programming Language :: Python :: 3.13
@@ -22,7 +21,7 @@ License-File: AUTHORS.rst
22
21
  Summary: Builds a queryable graph of the imports within one or more Python packages.
23
22
  Author-email: David Seddon <david@seddonym.me>
24
23
  License: BSD 2-Clause License
25
- Requires-Python: >=3.10
24
+ Requires-Python: >=3.11
26
25
  Description-Content-Type: text/x-rst; charset=UTF-8
27
26
  Project-URL: Documentation, https://grimp.readthedocs.io/
28
27
  Project-URL: Source-code, https://github.com/python-grimp/grimp/
@@ -8,13 +8,13 @@ module-name = "grimp._rustgrimp"
8
8
 
9
9
  [project]
10
10
  name = "grimp"
11
- version = "3.18"
11
+ version = "3.20"
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 = [
15
15
  {name = "David Seddon", email = "david@seddonym.me"},
16
16
  ]
17
- requires-python = ">=3.10"
17
+ requires-python = ">=3.11"
18
18
  dependencies = []
19
19
  classifiers = [
20
20
  "Development Status :: 5 - Production/Stable",
@@ -24,7 +24,6 @@ classifiers = [
24
24
  "Operating System :: POSIX",
25
25
  "Operating System :: Microsoft :: Windows",
26
26
  "Programming Language :: Python",
27
- "Programming Language :: Python :: 3.10",
28
27
  "Programming Language :: Python :: 3.11",
29
28
  "Programming Language :: Python :: 3.12",
30
29
  "Programming Language :: Python :: 3.13",
@@ -60,6 +59,7 @@ dev = [
60
59
  "requests==2.32.3",
61
60
  "sqlalchemy==2.0.35",
62
61
  "google-cloud-audit-log==0.3.0",
62
+ "pyupgrade>=3.21.2",
63
63
  ]
64
64
  docs = [
65
65
  "sphinx>=7.4.7",
@@ -1,6 +1,6 @@
1
- use crate::errors::GrimpResult;
1
+ use crate::errors::{GrimpError, GrimpResult};
2
2
 
3
- use crate::graph::{Graph, ModuleToken};
3
+ use crate::graph::{Graph, ModuleIterator, ModuleToken};
4
4
  use rustc_hash::{FxHashMap, FxHashSet};
5
5
  use slotmap::SecondaryMap;
6
6
  use std::cmp::Ordering;
@@ -9,17 +9,45 @@ impl Graph {
9
9
  pub fn nominate_cycle_breakers(
10
10
  &self,
11
11
  package: ModuleToken,
12
+ additional_packages: &[ModuleToken],
12
13
  ) -> 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 {
14
+ if additional_packages.is_empty() {
15
+ // A single package was passed: the nodes of the graph are its children.
16
+ // Children of a single package can never overlap, so no need to check.
17
+ let children = match self.module_children.get(package) {
18
+ Some(children) => children.clone(),
19
+ None => return Ok(FxHashSet::default()),
20
+ };
21
+ self.nominate_cycle_breakers_for_nodes(&children)
22
+ } else {
23
+ // Multiple packages were passed: the packages themselves are the nodes.
24
+ let mut nodes: FxHashSet<ModuleToken> = FxHashSet::default();
25
+ nodes.insert(package);
26
+ nodes.extend(additional_packages.iter().copied());
27
+
28
+ // The packages must not overlap: if one is a descendant of another, the edges
29
+ // between them (as packages) would be ill-defined.
30
+ for node in &nodes {
31
+ for descendant in self.get_module_descendants(*node).tokens() {
32
+ if nodes.contains(&descendant) {
33
+ return Err(GrimpError::SharedDescendants);
34
+ }
35
+ }
36
+ }
37
+
38
+ self.nominate_cycle_breakers_for_nodes(&nodes)
39
+ }
40
+ }
41
+
42
+ fn nominate_cycle_breakers_for_nodes(
43
+ &self,
44
+ nodes: &FxHashSet<ModuleToken>,
45
+ ) -> GrimpResult<FxHashSet<(ModuleToken, ModuleToken)>> {
46
+ if nodes.len() < 2 {
19
47
  return Ok(FxHashSet::default());
20
48
  }
21
49
  let (orig_imports, orig_reverse_imports, edge_weights) =
22
- self.build_graph_maps_of_children(children);
50
+ self.build_graph_maps_of_nodes(nodes);
23
51
 
24
52
  // Make a copy of the graph. We'll iteratively remove nodes from this.
25
53
  let mut working_imports = orig_imports.clone();
@@ -166,9 +194,9 @@ impl Graph {
166
194
  // with empty sets if they have no imports to / from.
167
195
  // They are denormalized into two maps for fast lookups.
168
196
  #[allow(clippy::type_complexity)]
169
- fn build_graph_maps_of_children(
197
+ fn build_graph_maps_of_nodes(
170
198
  &self,
171
- children: &FxHashSet<ModuleToken>,
199
+ nodes: &FxHashSet<ModuleToken>,
172
200
  ) -> (
173
201
  SecondaryMap<ModuleToken, FxHashSet<ModuleToken>>, // Imports
174
202
  SecondaryMap<ModuleToken, FxHashSet<ModuleToken>>, // Reverse imports
@@ -180,25 +208,25 @@ impl Graph {
180
208
  SecondaryMap::default();
181
209
  let mut weights = FxHashMap::default();
182
210
 
183
- for child in children {
184
- orig_imports.insert(*child, FxHashSet::default());
185
- orig_reverse_imports.insert(*child, FxHashSet::default());
211
+ for node in nodes {
212
+ orig_imports.insert(*node, FxHashSet::default());
213
+ orig_reverse_imports.insert(*node, FxHashSet::default());
186
214
  }
187
- // Get number of imports between each child.
215
+ // Get number of imports between each node.
188
216
  // 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).
217
+ for node_a in nodes {
218
+ for node_b in nodes.iter().filter(|node| *node != node_a) {
219
+ // Get number of imports from node_a to node_b (as package).
192
220
  let num_imports_from_a_to_b = self
193
- .find_direct_imports_between(*child_a, *child_b, true)
221
+ .find_direct_imports_between(*node_a, *node_b, true)
194
222
  .unwrap()
195
223
  .len();
196
224
 
197
225
  if num_imports_from_a_to_b > 0 {
198
226
  // 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);
227
+ orig_imports[*node_a].insert(*node_b);
228
+ orig_reverse_imports[*node_b].insert(*node_a);
229
+ weights.insert((*node_a, *node_b), num_imports_from_a_to_b);
202
230
  }
203
231
  }
204
232
  }
@@ -0,0 +1,328 @@
1
+ use crate::errors::{GrimpError, GrimpResult};
2
+ use crate::graph::{
3
+ EMPTY_IMPORT_DETAILS, EMPTY_MODULE_TOKENS, ExtendWithDescendants, Graph, ModuleIterator,
4
+ ModuleToken, PyImportDetails,
5
+ };
6
+ use crate::module_expressions::ModuleExpression;
7
+ use rustc_hash::FxHashSet;
8
+
9
+ impl Graph {
10
+ pub fn count_imports(&self) -> usize {
11
+ self.imports.values().map(|imports| imports.len()).sum()
12
+ }
13
+
14
+ pub fn direct_import_exists(
15
+ &self,
16
+ importer: ModuleToken,
17
+ imported: ModuleToken,
18
+ as_packages: bool,
19
+ ) -> GrimpResult<bool> {
20
+ let mut importers: FxHashSet<_> = importer.into();
21
+ let mut importeds: FxHashSet<_> = imported.into();
22
+ if as_packages {
23
+ importers.extend_with_descendants(self);
24
+ importeds.extend_with_descendants(self);
25
+ if !(&importers & &importeds).is_empty() {
26
+ return Err(GrimpError::SharedDescendants);
27
+ }
28
+ }
29
+
30
+ let direct_imports = importers
31
+ .iter()
32
+ .flat_map(|importer_module| self.imports.get(*importer_module).unwrap().iter().cloned())
33
+ .collect::<FxHashSet<ModuleToken>>();
34
+
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)
68
+ }
69
+
70
+ /// Find the modules directly imported by the module.
71
+ ///
72
+ /// If `as_package` is true, the module is treated as a package: the result is the modules
73
+ /// directly imported by it or any of its descendants, excluding modules within the package.
74
+ pub fn modules_directly_imported_by(
75
+ &self,
76
+ importer: ModuleToken,
77
+ as_package: bool,
78
+ ) -> FxHashSet<ModuleToken> {
79
+ let mut importers: FxHashSet<_> = importer.into();
80
+ if as_package {
81
+ importers.extend_with_descendants(self);
82
+ }
83
+
84
+ let mut imported_modules: FxHashSet<_> = importers
85
+ .iter()
86
+ .flat_map(|importer| self.imports.get(*importer).unwrap_or(&EMPTY_MODULE_TOKENS))
87
+ .cloned()
88
+ .collect();
89
+ if as_package {
90
+ imported_modules.retain(|imported| !importers.contains(imported));
91
+ }
92
+ imported_modules
93
+ }
94
+
95
+ /// Find the modules that directly import the module.
96
+ ///
97
+ /// If `as_package` is true, the module is treated as a package: the result is the modules
98
+ /// that directly import it or any of its descendants, excluding modules within the package.
99
+ pub fn modules_that_directly_import(
100
+ &self,
101
+ imported: ModuleToken,
102
+ as_package: bool,
103
+ ) -> FxHashSet<ModuleToken> {
104
+ let mut importeds: FxHashSet<_> = imported.into();
105
+ if as_package {
106
+ importeds.extend_with_descendants(self);
107
+ }
108
+
109
+ let mut importers: FxHashSet<_> = importeds
110
+ .iter()
111
+ .flat_map(|imported| {
112
+ self.reverse_imports
113
+ .get(*imported)
114
+ .unwrap_or(&EMPTY_MODULE_TOKENS)
115
+ })
116
+ .cloned()
117
+ .collect();
118
+ if as_package {
119
+ importers.retain(|importer| !importeds.contains(importer));
120
+ }
121
+ importers
122
+ }
123
+
124
+ pub fn get_import_details(
125
+ &self,
126
+ importer: ModuleToken,
127
+ imported: ModuleToken,
128
+ ) -> &FxHashSet<PyImportDetails> {
129
+ match self.import_details.get(&(importer, imported)) {
130
+ Some(import_details) => import_details,
131
+ None => &EMPTY_IMPORT_DETAILS,
132
+ }
133
+ }
134
+
135
+ pub fn find_matching_direct_imports(
136
+ &self,
137
+ importer_expression: &ModuleExpression,
138
+ imported_expression: &ModuleExpression,
139
+ ) -> FxHashSet<(ModuleToken, ModuleToken)> {
140
+ let importers: FxHashSet<ModuleToken> = self
141
+ .find_matching_modules(importer_expression)
142
+ .tokens()
143
+ .collect();
144
+ let importeds: FxHashSet<ModuleToken> = self
145
+ .find_matching_modules(imported_expression)
146
+ .tokens()
147
+ .collect();
148
+
149
+ importers
150
+ .iter()
151
+ .flat_map(|importer| {
152
+ self.modules_directly_imported_by(*importer, false)
153
+ .into_iter()
154
+ .filter(|imported| importeds.contains(imported))
155
+ .map(move |imported| (*importer, imported))
156
+ })
157
+ .collect()
158
+ }
159
+ }
160
+
161
+ #[cfg(test)]
162
+ mod test {
163
+ use super::*;
164
+
165
+ #[test]
166
+ fn test_modules_directly_imported_by() {
167
+ let mut graph = Graph::default();
168
+
169
+ let animals = graph.get_or_add_module("pkg.animals").token;
170
+ let dog = graph.get_or_add_module("pkg.animals.dog").token;
171
+ let cat = graph.get_or_add_module("pkg.animals.cat").token;
172
+ let food = graph.get_or_add_module("pkg.food").token;
173
+ let chicken = graph.get_or_add_module("pkg.food.chicken").token;
174
+ let tesco = graph.get_or_add_module("pkg.shops.tesco").token;
175
+
176
+ graph.add_import(dog, chicken);
177
+ graph.add_import(cat, food);
178
+ // An import within the package.
179
+ graph.add_import(dog, cat);
180
+ // An import of the package, not by it.
181
+ graph.add_import(tesco, animals);
182
+
183
+ // Without as_package, only the module itself is looked at.
184
+ assert_eq!(
185
+ graph.modules_directly_imported_by(animals, false),
186
+ FxHashSet::default()
187
+ );
188
+ assert_eq!(
189
+ graph.modules_directly_imported_by(dog, false),
190
+ FxHashSet::from_iter([chicken, cat])
191
+ );
192
+ // With as_package, descendants are looked at too, but modules within the package
193
+ // are excluded.
194
+ assert_eq!(
195
+ graph.modules_directly_imported_by(animals, true),
196
+ FxHashSet::from_iter([chicken, food])
197
+ );
198
+ }
199
+
200
+ #[test]
201
+ fn test_modules_that_directly_import() {
202
+ let mut graph = Graph::default();
203
+
204
+ let animals = graph.get_or_add_module("pkg.animals").token;
205
+ let dog = graph.get_or_add_module("pkg.animals.dog").token;
206
+ let cat = graph.get_or_add_module("pkg.animals.cat").token;
207
+ let chicken = graph.get_or_add_module("pkg.food.chicken").token;
208
+ let shops = graph.get_or_add_module("pkg.shops").token;
209
+ let tesco = graph.get_or_add_module("pkg.shops.tesco").token;
210
+
211
+ graph.add_import(tesco, animals);
212
+ graph.add_import(shops, cat);
213
+ // An import within the package.
214
+ graph.add_import(dog, cat);
215
+ // An import by the package, not of it.
216
+ graph.add_import(dog, chicken);
217
+
218
+ // Without as_package, only the module itself is looked at.
219
+ assert_eq!(
220
+ graph.modules_that_directly_import(animals, false),
221
+ FxHashSet::from_iter([tesco])
222
+ );
223
+ assert_eq!(
224
+ graph.modules_that_directly_import(cat, false),
225
+ FxHashSet::from_iter([shops, dog])
226
+ );
227
+ // With as_package, descendants are looked at too, but modules within the package
228
+ // are excluded.
229
+ assert_eq!(
230
+ graph.modules_that_directly_import(animals, true),
231
+ FxHashSet::from_iter([tesco, shops])
232
+ );
233
+ }
234
+
235
+ #[test]
236
+ fn test_find_matching_direct_imports() {
237
+ let mut graph = Graph::default();
238
+
239
+ let _ = graph.get_or_add_module("pkg.animals").token;
240
+ let dog = graph.get_or_add_module("pkg.animals.dog").token;
241
+ let cat = graph.get_or_add_module("pkg.animals.cat").token;
242
+ let _ = graph.get_or_add_module("pkg.food").token;
243
+ let chicken = graph.get_or_add_module("pkg.food.chicken").token;
244
+ let fish = graph.get_or_add_module("pkg.food.fish").token;
245
+ let _ = graph.get_or_add_module("pkg.colors").token;
246
+ let golden = graph.get_or_add_module("pkg.colors.golden").token;
247
+ let ginger = graph.get_or_add_module("pkg.colors.ginger").token;
248
+ let _ = graph.get_or_add_module("pkg.shops").token;
249
+ let tesco = graph.get_or_add_module("pkg.shops.tesco").token;
250
+ let coop = graph.get_or_add_module("pkg.shops.coop").token;
251
+
252
+ // Should match
253
+ graph.add_import(dog, chicken);
254
+ graph.add_import(cat, fish);
255
+ // Should not match: Imported does not match
256
+ graph.add_import(dog, golden);
257
+ graph.add_import(cat, ginger);
258
+ // Should not match: Importer does not match
259
+ graph.add_import(tesco, chicken);
260
+ graph.add_import(coop, fish);
261
+
262
+ let importer_expression = "pkg.animals.*".parse().unwrap();
263
+ let imported_expression = "pkg.food.*".parse().unwrap();
264
+ let matching_imports =
265
+ graph.find_matching_direct_imports(&importer_expression, &imported_expression);
266
+
267
+ assert_eq!(
268
+ matching_imports,
269
+ FxHashSet::from_iter([(dog, chicken), (cat, fish)])
270
+ );
271
+ }
272
+
273
+ #[test]
274
+ fn test_find_matching_direct_imports_with_and_without_wildcards() {
275
+ let mut graph = Graph::default();
276
+
277
+ let dog = graph.get_or_add_module("pkg.animals.dog").token;
278
+ let cat = graph.get_or_add_module("pkg.animals.cat").token;
279
+ let chicken = graph.get_or_add_module("pkg.food.chicken").token;
280
+ let fish = graph.get_or_add_module("pkg.food.fish").token;
281
+ let golden = graph.get_or_add_module("pkg.colors.golden").token;
282
+ let tesco = graph.get_or_add_module("pkg.shops.tesco").token;
283
+ graph.get_or_add_module("pkg.shops.coop");
284
+
285
+ graph.add_import(dog, chicken);
286
+ graph.add_import(dog, golden);
287
+ graph.add_import(cat, fish);
288
+ graph.add_import(tesco, chicken);
289
+
290
+ let find = |importer_expression: &str, imported_expression: &str| {
291
+ graph.find_matching_direct_imports(
292
+ &importer_expression.parse().unwrap(),
293
+ &imported_expression.parse().unwrap(),
294
+ )
295
+ };
296
+
297
+ // Neither side has wildcards.
298
+ assert_eq!(
299
+ find("pkg.animals.dog", "pkg.food.chicken"),
300
+ FxHashSet::from_iter([(dog, chicken)])
301
+ );
302
+ // Only the importer is a single module.
303
+ assert_eq!(
304
+ find("pkg.animals.dog", "pkg.*.*"),
305
+ FxHashSet::from_iter([(dog, chicken), (dog, golden)])
306
+ );
307
+ // Only the imported is a single module.
308
+ assert_eq!(
309
+ find("pkg.*.*", "pkg.food.chicken"),
310
+ FxHashSet::from_iter([(dog, chicken), (tesco, chicken)])
311
+ );
312
+ // Both sides have wildcards.
313
+ assert_eq!(
314
+ find("pkg.shops.*", "pkg.food.*"),
315
+ FxHashSet::from_iter([(tesco, chicken)])
316
+ );
317
+ // A module with no imports.
318
+ assert!(find("pkg.shops.coop", "pkg.*.*").is_empty());
319
+ // A module that has no importers.
320
+ assert!(find("pkg.*.*", "pkg.shops.coop").is_empty());
321
+ // Modules that aren't in the graph.
322
+ assert!(find("pkg.animals.missing", "pkg.*.*").is_empty());
323
+ assert!(find("pkg.*.*", "pkg.food.missing").is_empty());
324
+ assert!(find("pkg.animals.missing", "pkg.food.missing").is_empty());
325
+ // Wildcards that match nothing.
326
+ assert!(find("pkg.nothing.*", "pkg.*.*").is_empty());
327
+ }
328
+ }
@@ -91,10 +91,10 @@ impl Graph {
91
91
  self.module_children.remove(module);
92
92
 
93
93
  // Update imports.
94
- for imported in self.modules_directly_imported_by(module).clone() {
94
+ for imported in self.modules_directly_imported_by(module, false) {
95
95
  self.remove_import(module, imported);
96
96
  }
97
- for importer in self.modules_that_directly_import(module).clone() {
97
+ for importer in self.modules_that_directly_import(module, false) {
98
98
  self.remove_import(importer, module);
99
99
  }
100
100
  self.imports.remove(module);
@@ -164,19 +164,11 @@ impl Graph {
164
164
 
165
165
  let modules_imported_by_descendants: FxHashSet<_> = descendants
166
166
  .iter()
167
- .flat_map(|descendant| {
168
- self.modules_directly_imported_by(*descendant)
169
- .iter()
170
- .cloned()
171
- })
167
+ .flat_map(|descendant| self.modules_directly_imported_by(*descendant, false))
172
168
  .collect();
173
169
  let modules_that_import_descendants: FxHashSet<_> = descendants
174
170
  .iter()
175
- .flat_map(|descendant| {
176
- self.modules_that_directly_import(*descendant)
177
- .iter()
178
- .cloned()
179
- })
171
+ .flat_map(|descendant| self.modules_that_directly_import(*descendant, false))
180
172
  .collect();
181
173
 
182
174
  // Add descendants and imports to parent module.
@@ -54,12 +54,19 @@ impl Graph {
54
54
  &self,
55
55
  expression: &ModuleExpression,
56
56
  ) -> impl ModuleIterator<'_> + use<'_> {
57
- let interner = MODULE_NAMES.read().unwrap();
58
- let modules: FxHashSet<_> = self
59
- .modules
60
- .values()
61
- .filter(|m| expression.is_match(interner.resolve(m.interned_name).unwrap()))
62
- .collect();
57
+ let modules: FxHashSet<_> = if expression.as_ref().contains('*') {
58
+ let interner = MODULE_NAMES.read().unwrap();
59
+ self.modules
60
+ .values()
61
+ .filter(|m| expression.is_match(interner.resolve(m.interned_name).unwrap()))
62
+ .collect()
63
+ } else {
64
+ // Without wildcards the expression can only match the module with that name,
65
+ // so there's no need to check every module.
66
+ self.get_module_by_name(expression.as_ref())
67
+ .into_iter()
68
+ .collect()
69
+ };
63
70
  modules.into_iter()
64
71
  }
65
72
  }
@@ -80,6 +87,8 @@ mod tests {
80
87
 
81
88
  #[parameterized(case = {
82
89
  FindMatchingModulesTestCase::new("foo", ["foo"]),
90
+ FindMatchingModulesTestCase::new("foo.bar.baz", ["foo.bar.baz"]),
91
+ FindMatchingModulesTestCase::new("foo.missing", Vec::<&str>::new()),
83
92
  FindMatchingModulesTestCase::new("foo.*", ["foo.bar"]),
84
93
  FindMatchingModulesTestCase::new("foo.**", ["foo.bar", "foo.bar.baz"]),
85
94
  })]
@@ -198,6 +198,33 @@ impl GraphWrapper {
198
198
 
199
199
  PyTuple::new(py, python_dependencies)
200
200
  }
201
+
202
+ fn convert_cycle_breakers_to_python<'py>(
203
+ &self,
204
+ py: Python<'py>,
205
+ cycle_breakers: FxHashSet<(ModuleToken, ModuleToken)>,
206
+ ) -> PyResult<Bound<'py, PySet>> {
207
+ PySet::new(
208
+ py,
209
+ cycle_breakers
210
+ .into_iter()
211
+ .map(|(importer, imported)| {
212
+ let importer = self._graph.get_module(importer).unwrap();
213
+ let imported = self._graph.get_module(imported).unwrap();
214
+ Import::new(importer.name(), imported.name())
215
+ })
216
+ .map(|import| {
217
+ PyTuple::new(
218
+ py,
219
+ [
220
+ import.importer.into_py_any(py).unwrap(),
221
+ import.imported.into_py_any(py).unwrap(),
222
+ ],
223
+ )
224
+ .unwrap()
225
+ }),
226
+ )
227
+ }
201
228
  }
202
229
 
203
230
  /// Wrapper around the Graph struct that integrates with Python.
@@ -367,11 +394,16 @@ impl GraphWrapper {
367
394
  .direct_import_exists(importer, imported, as_packages)?)
368
395
  }
369
396
 
370
- pub fn find_modules_directly_imported_by(&self, module: &str) -> PyResult<HashSet<String>> {
397
+ #[pyo3(signature = (module, as_package=false))]
398
+ pub fn find_modules_directly_imported_by(
399
+ &self,
400
+ module: &str,
401
+ as_package: bool,
402
+ ) -> PyResult<HashSet<String>> {
371
403
  let module = self.get_visible_module_by_name(module)?.token();
372
404
  Ok(self
373
405
  ._graph
374
- .modules_directly_imported_by(module)
406
+ .modules_directly_imported_by(module, as_package)
375
407
  .iter()
376
408
  .into_module_iterator(&self._graph)
377
409
  .visible()
@@ -379,11 +411,16 @@ impl GraphWrapper {
379
411
  .collect())
380
412
  }
381
413
 
382
- pub fn find_modules_that_directly_import(&self, module: &str) -> PyResult<HashSet<String>> {
414
+ #[pyo3(signature = (module, as_package=false))]
415
+ pub fn find_modules_that_directly_import(
416
+ &self,
417
+ module: &str,
418
+ as_package: bool,
419
+ ) -> PyResult<HashSet<String>> {
383
420
  let module = self.get_visible_module_by_name(module)?.token();
384
421
  Ok(self
385
422
  ._graph
386
- .modules_that_directly_import(module)
423
+ .modules_that_directly_import(module, as_package)
387
424
  .iter()
388
425
  .into_module_iterator(&self._graph)
389
426
  .visible()
@@ -642,29 +679,17 @@ impl GraphWrapper {
642
679
  &self,
643
680
  py: Python<'py>,
644
681
  package: &str,
682
+ additional_packages: Vec<String>,
645
683
  ) -> PyResult<Bound<'py, PySet>> {
646
684
  let package = self.get_visible_module_by_name(package)?.token();
647
- let cycle_breakers = self._graph.nominate_cycle_breakers(package)?;
648
- PySet::new(
649
- py,
650
- cycle_breakers
651
- .into_iter()
652
- .map(|(importer, imported)| {
653
- let importer = self._graph.get_module(importer).unwrap();
654
- let imported = self._graph.get_module(imported).unwrap();
655
- Import::new(importer.name(), imported.name())
656
- })
657
- .map(|import| {
658
- PyTuple::new(
659
- py,
660
- [
661
- import.importer.into_py_any(py).unwrap(),
662
- import.imported.into_py_any(py).unwrap(),
663
- ],
664
- )
665
- .unwrap()
666
- }),
667
- )
685
+ let additional_packages = additional_packages
686
+ .iter()
687
+ .map(|package| Ok(self.get_visible_module_by_name(package)?.token()))
688
+ .collect::<PyResult<Vec<_>>>()?;
689
+ let cycle_breakers = self
690
+ ._graph
691
+ .nominate_cycle_breakers(package, &additional_packages)?;
692
+ self.convert_cycle_breakers_to_python(py, cycle_breakers)
668
693
  }
669
694
 
670
695
  #[pyo3(name = "clone")]
@@ -1,4 +1,4 @@
1
- __version__ = "3.18"
1
+ __version__ = "3.20"
2
2
 
3
3
  from .application.graph import DetailedImport, Import, ImportGraph
4
4
  from .domain.analysis import PackageDependency, Route
@@ -223,27 +223,41 @@ class ImportGraph:
223
223
  importer=importer, imported=imported, as_packages=as_packages
224
224
  )
225
225
 
226
- def find_modules_directly_imported_by(self, module: str) -> set[str]:
226
+ def find_modules_directly_imported_by(self, module: str, as_package: bool = False) -> set[str]:
227
227
  """
228
228
  Find all modules that are directly imported by the supplied module.
229
229
 
230
+ Args:
231
+ module: The name of the importing module.
232
+ as_package: Whether or not to treat the supplied module as an individual module,
233
+ or as a package (i.e. including any descendants, if there are any). If
234
+ treating it as a package, the result will include modules *external* to
235
+ the package, and won't include modules within it.
236
+
230
237
  If the module is not present in the graph, grimp.exceptions.ModuleNotPresent will be raised.
231
238
  """
232
239
  if not self._rustgraph.contains_module(module):
233
240
  msg = f'"{module}" not present in the graph.'
234
241
  raise ModuleNotPresent(msg)
235
- return self._rustgraph.find_modules_directly_imported_by(module)
242
+ return self._rustgraph.find_modules_directly_imported_by(module, as_package)
236
243
 
237
- def find_modules_that_directly_import(self, module: str) -> set[str]:
244
+ def find_modules_that_directly_import(self, module: str, as_package: bool = False) -> set[str]:
238
245
  """
239
246
  Find all modules that directly import the supplied module.
240
247
 
248
+ Args:
249
+ module: The name of the imported module.
250
+ as_package: Whether or not to treat the supplied module as an individual module,
251
+ or as a package (i.e. including any descendants, if there are any). If
252
+ treating it as a package, the result will include modules *external* to
253
+ the package, and won't include modules within it.
254
+
241
255
  If the module is not present in the graph, grimp.exceptions.ModuleNotPresent will be raised.
242
256
  """
243
257
  if not self._rustgraph.contains_module(module):
244
258
  msg = f'"{module}" not present in the graph.'
245
259
  raise ModuleNotPresent(msg)
246
- return self._rustgraph.find_modules_that_directly_import(module)
260
+ return self._rustgraph.find_modules_that_directly_import(module, as_package)
247
261
 
248
262
  def get_import_details(self, *, importer: str, imported: str) -> list[DetailedImport]:
249
263
  """
@@ -476,14 +490,18 @@ class ImportGraph:
476
490
 
477
491
  return _dependencies_from_tuple(result)
478
492
 
479
- def nominate_cycle_breakers(self, package: str) -> set[ImportTuple]:
493
+ def nominate_cycle_breakers(self, package: str, *additional_packages: str) -> set[ImportTuple]:
480
494
  """
481
- Identify a set of imports that, if removed, would make the package locally acyclic.
495
+ Identify a set of imports that, if removed, would make the supplied packages acyclic.
496
+
497
+ For more information see the docs in usage.rst.
482
498
  """
483
- if not self._rustgraph.contains_module(package):
484
- msg = f'"{package}" not present in the graph.'
485
- raise ModuleNotPresent(msg)
486
- return self._rustgraph.nominate_cycle_breakers(package)
499
+ for package_ in (package, *additional_packages):
500
+ if not self._rustgraph.contains_module(package_):
501
+ msg = f'"{package_}" not present in the graph.'
502
+ raise ModuleNotPresent(msg)
503
+
504
+ return self._rustgraph.nominate_cycle_breakers(package, additional_packages)
487
505
 
488
506
  # Dunder methods
489
507
  # --------------
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import abc
4
4
  from types import TracebackType
5
+ from typing import Self
5
6
 
6
7
 
7
8
  class Timer(abc.ABC):
@@ -22,7 +23,7 @@ class Timer(abc.ABC):
22
23
  # We use a stack so context managers can be nested.
23
24
  self._start_stack: list[float] = []
24
25
 
25
- def __enter__(self) -> Timer:
26
+ def __enter__(self) -> Self:
26
27
  self._start_stack.append(self.get_current_time())
27
28
  return self
28
29
 
@@ -1,158 +0,0 @@
1
- use crate::errors::{GrimpError, GrimpResult};
2
- use crate::graph::{
3
- EMPTY_IMPORT_DETAILS, EMPTY_MODULE_TOKENS, ExtendWithDescendants, Graph, MODULE_NAMES,
4
- ModuleToken, PyImportDetails,
5
- };
6
- use crate::module_expressions::ModuleExpression;
7
- use rustc_hash::FxHashSet;
8
-
9
- impl Graph {
10
- pub fn count_imports(&self) -> usize {
11
- self.imports.values().map(|imports| imports.len()).sum()
12
- }
13
-
14
- pub fn direct_import_exists(
15
- &self,
16
- importer: ModuleToken,
17
- imported: ModuleToken,
18
- as_packages: bool,
19
- ) -> GrimpResult<bool> {
20
- let mut importers: FxHashSet<_> = importer.into();
21
- let mut importeds: FxHashSet<_> = imported.into();
22
- if as_packages {
23
- importers.extend_with_descendants(self);
24
- importeds.extend_with_descendants(self);
25
- if !(&importers & &importeds).is_empty() {
26
- return Err(GrimpError::SharedDescendants);
27
- }
28
- }
29
-
30
- let direct_imports = importers
31
- .iter()
32
- .flat_map(|importer_module| self.imports.get(*importer_module).unwrap().iter().cloned())
33
- .collect::<FxHashSet<ModuleToken>>();
34
-
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)
68
- }
69
-
70
- pub fn modules_directly_imported_by(&self, importer: ModuleToken) -> &FxHashSet<ModuleToken> {
71
- self.imports.get(importer).unwrap_or(&EMPTY_MODULE_TOKENS)
72
- }
73
-
74
- pub fn modules_that_directly_import(&self, imported: ModuleToken) -> &FxHashSet<ModuleToken> {
75
- self.reverse_imports
76
- .get(imported)
77
- .unwrap_or(&EMPTY_MODULE_TOKENS)
78
- }
79
-
80
- pub fn get_import_details(
81
- &self,
82
- importer: ModuleToken,
83
- imported: ModuleToken,
84
- ) -> &FxHashSet<PyImportDetails> {
85
- match self.import_details.get(&(importer, imported)) {
86
- Some(import_details) => import_details,
87
- None => &EMPTY_IMPORT_DETAILS,
88
- }
89
- }
90
-
91
- pub fn find_matching_direct_imports(
92
- &self,
93
- importer_expression: &ModuleExpression,
94
- imported_expression: &ModuleExpression,
95
- ) -> FxHashSet<(ModuleToken, ModuleToken)> {
96
- let interner = MODULE_NAMES.read().unwrap();
97
- self.imports
98
- .iter()
99
- .flat_map(|(importer, imports)| {
100
- imports
101
- .iter()
102
- .cloned()
103
- .map(move |imported| (importer, imported))
104
- })
105
- .filter(|(importer, imported)| {
106
- let importer = self.get_module(*importer).unwrap();
107
- let importer_name = interner.resolve(importer.interned_name).unwrap();
108
- let imported = self.get_module(*imported).unwrap();
109
- let imported_name = interner.resolve(imported.interned_name).unwrap();
110
- importer_expression.is_match(importer_name)
111
- && imported_expression.is_match(imported_name)
112
- })
113
- .collect()
114
- }
115
- }
116
-
117
- #[cfg(test)]
118
- mod test {
119
- use super::*;
120
-
121
- #[test]
122
- fn test_find_matching_direct_imports() {
123
- let mut graph = Graph::default();
124
-
125
- let _ = graph.get_or_add_module("pkg.animals").token;
126
- let dog = graph.get_or_add_module("pkg.animals.dog").token;
127
- let cat = graph.get_or_add_module("pkg.animals.cat").token;
128
- let _ = graph.get_or_add_module("pkg.food").token;
129
- let chicken = graph.get_or_add_module("pkg.food.chicken").token;
130
- let fish = graph.get_or_add_module("pkg.food.fish").token;
131
- let _ = graph.get_or_add_module("pkg.colors").token;
132
- let golden = graph.get_or_add_module("pkg.colors.golden").token;
133
- let ginger = graph.get_or_add_module("pkg.colors.ginger").token;
134
- let _ = graph.get_or_add_module("pkg.shops").token;
135
- let tesco = graph.get_or_add_module("pkg.shops.tesco").token;
136
- let coop = graph.get_or_add_module("pkg.shops.coop").token;
137
-
138
- // Should match
139
- graph.add_import(dog, chicken);
140
- graph.add_import(cat, fish);
141
- // Should not match: Imported does not match
142
- graph.add_import(dog, golden);
143
- graph.add_import(cat, ginger);
144
- // Should not match: Importer does not match
145
- graph.add_import(tesco, chicken);
146
- graph.add_import(coop, fish);
147
-
148
- let importer_expression = "pkg.animals.*".parse().unwrap();
149
- let imported_expression = "pkg.food.*".parse().unwrap();
150
- let matching_imports =
151
- graph.find_matching_direct_imports(&importer_expression, &imported_expression);
152
-
153
- assert_eq!(
154
- matching_imports,
155
- FxHashSet::from_iter([(dog, chicken), (cat, fish)])
156
- );
157
- }
158
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes