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.
- grimp-3.20/AUTHORS.rst +16 -0
- grimp-3.20/LICENSE +21 -0
- {grimp-3.18 → grimp-3.20}/PKG-INFO +2 -3
- {grimp-3.18 → grimp-3.20}/pyproject.toml +3 -3
- {grimp-3.18 → grimp-3.20}/rust/src/graph/cycle_breakers.rs +50 -22
- grimp-3.20/rust/src/graph/direct_import_queries.rs +328 -0
- {grimp-3.18 → grimp-3.20}/rust/src/graph/graph_manipulation.rs +4 -12
- {grimp-3.18 → grimp-3.20}/rust/src/graph/hierarchy_queries.rs +15 -6
- {grimp-3.18 → grimp-3.20}/rust/src/graph/mod.rs +50 -25
- {grimp-3.18 → grimp-3.20}/src/grimp/__init__.py +1 -1
- {grimp-3.18 → grimp-3.20}/src/grimp/application/graph.py +28 -10
- {grimp-3.18 → grimp-3.20}/src/grimp/application/ports/timing.py +2 -1
- grimp-3.18/rust/src/graph/direct_import_queries.rs +0 -158
- {grimp-3.18 → grimp-3.20}/README.rst +0 -0
- {grimp-3.18 → grimp-3.20}/rust/Cargo.lock +0 -0
- {grimp-3.18 → grimp-3.20}/rust/Cargo.toml +0 -0
- {grimp-3.18 → grimp-3.20}/rust/build.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/rust-toolchain.toml +0 -0
- {grimp-3.18 → grimp-3.20}/rust/src/caching.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/src/errors.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/src/exceptions.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/src/filesystem.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/src/graph/higher_order_queries.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/src/graph/import_chain_queries.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/src/graph/pathfinding.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/src/import_parsing.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/src/import_scanning.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/src/lib.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/src/module_expressions.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/src/module_finding.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/tests/large.rs +0 -0
- {grimp-3.18 → grimp-3.20}/rust/tests/large_graph.json +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/adaptors/__init__.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/adaptors/caching.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/adaptors/filesystem.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/adaptors/modulefinder.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/adaptors/packagefinder.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/adaptors/timing.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/application/__init__.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/application/config.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/application/ports/__init__.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/application/ports/caching.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/application/ports/filesystem.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/application/ports/modulefinder.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/application/ports/packagefinder.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/application/scanning.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/application/usecases.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/domain/__init__.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/domain/analysis.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/domain/valueobjects.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/exceptions.py +0 -0
- {grimp-3.18 → grimp-3.20}/src/grimp/main.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
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
|
|
197
|
+
fn build_graph_maps_of_nodes(
|
|
170
198
|
&self,
|
|
171
|
-
|
|
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
|
|
184
|
-
orig_imports.insert(*
|
|
185
|
-
orig_reverse_imports.insert(*
|
|
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
|
|
215
|
+
// Get number of imports between each node.
|
|
188
216
|
// Add as edges.
|
|
189
|
-
for
|
|
190
|
-
for
|
|
191
|
-
// Get number of imports from
|
|
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(*
|
|
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[*
|
|
200
|
-
orig_reverse_imports[*
|
|
201
|
-
weights.insert((*
|
|
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)
|
|
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)
|
|
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
|
|
58
|
-
|
|
59
|
-
.modules
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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")]
|
|
@@ -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
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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) ->
|
|
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
|
|
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
|