grimp 3.9__tar.gz → 3.10__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.9 → grimp-3.10}/PKG-INFO +2 -2
- {grimp-3.9 → grimp-3.10}/README.rst +1 -1
- {grimp-3.9 → grimp-3.10}/pyproject.toml +1 -1
- {grimp-3.9 → grimp-3.10}/rust/Cargo.lock +32 -0
- {grimp-3.9 → grimp-3.10}/rust/Cargo.toml +4 -0
- {grimp-3.9 → grimp-3.10}/rust/src/errors.rs +1 -0
- grimp-3.10/rust/src/filesystem.rs +357 -0
- {grimp-3.9 → grimp-3.10}/rust/src/graph/hierarchy_queries.rs +4 -4
- {grimp-3.9 → grimp-3.10}/rust/src/graph/higher_order_queries.rs +134 -30
- {grimp-3.9 → grimp-3.10}/rust/src/graph/import_chain_queries.rs +47 -16
- {grimp-3.9 → grimp-3.10}/rust/src/import_parsing.rs +10 -11
- grimp-3.10/rust/src/import_scanning.rs +353 -0
- {grimp-3.9 → grimp-3.10}/rust/src/lib.rs +91 -20
- grimp-3.10/rust/src/module_finding.rs +60 -0
- {grimp-3.9 → grimp-3.10}/rust/tests/large.rs +1 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/__init__.py +1 -1
- {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/filesystem.py +5 -1
- {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/graph.py +7 -3
- {grimp-3.9 → grimp-3.10}/src/grimp/application/ports/filesystem.py +34 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/application/ports/graph.py +5 -0
- grimp-3.10/src/grimp/application/scanning.py +103 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/application/usecases.py +8 -105
- grimp-3.10/src/grimp/domain/valueobjects.py +83 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/main.py +0 -2
- grimp-3.9/src/grimp/adaptors/importscanner.py +0 -231
- grimp-3.9/src/grimp/algorithms/shortest_path.py +0 -143
- grimp-3.9/src/grimp/application/ports/importscanner.py +0 -46
- grimp-3.9/src/grimp/domain/__init__.py +0 -0
- grimp-3.9/src/grimp/domain/valueobjects.py +0 -106
- grimp-3.9/src/grimp/helpers.py +0 -6
- {grimp-3.9 → grimp-3.10}/rust/.cargo/config.toml +0 -0
- {grimp-3.9 → grimp-3.10}/rust/src/exceptions.rs +0 -0
- {grimp-3.9 → grimp-3.10}/rust/src/graph/direct_import_queries.rs +0 -0
- {grimp-3.9 → grimp-3.10}/rust/src/graph/graph_manipulation.rs +0 -0
- {grimp-3.9 → grimp-3.10}/rust/src/graph/mod.rs +0 -0
- {grimp-3.9 → grimp-3.10}/rust/src/graph/pathfinding.rs +0 -0
- {grimp-3.9 → grimp-3.10}/rust/src/module_expressions.rs +0 -0
- {grimp-3.9 → grimp-3.10}/rust/tests/large_graph.json +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/__init__.py +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/caching.py +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/modulefinder.py +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/packagefinder.py +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/timing.py +0 -0
- {grimp-3.9/src/grimp/algorithms → grimp-3.10/src/grimp/application}/__init__.py +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/application/config.py +0 -0
- {grimp-3.9/src/grimp/application → grimp-3.10/src/grimp/application/ports}/__init__.py +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/application/ports/caching.py +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/application/ports/modulefinder.py +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/application/ports/packagefinder.py +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/application/ports/timing.py +0 -0
- {grimp-3.9/src/grimp/application/ports → grimp-3.10/src/grimp/domain}/__init__.py +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/domain/analysis.py +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/exceptions.py +0 -0
- {grimp-3.9 → grimp-3.10}/src/grimp/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: grimp
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.10
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: BSD License
|
|
@@ -39,7 +39,7 @@ 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=
|
|
42
|
+
.. image:: https://github.com/seddonym/grimp/workflows/CI/badge.svg?branch=main
|
|
43
43
|
:target: https://github.com/seddonym/grimp/actions?workflow=CI
|
|
44
44
|
:alt: CI Status
|
|
45
45
|
|
|
@@ -9,7 +9,7 @@ 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=
|
|
12
|
+
.. image:: https://github.com/seddonym/grimp/workflows/CI/badge.svg?branch=main
|
|
13
13
|
:target: https://github.com/seddonym/grimp/actions?workflow=CI
|
|
14
14
|
:alt: CI Status
|
|
15
15
|
|
|
@@ -9,6 +9,7 @@ dependencies = [
|
|
|
9
9
|
"bimap",
|
|
10
10
|
"const_format",
|
|
11
11
|
"derive-new",
|
|
12
|
+
"encoding_rs",
|
|
12
13
|
"getset",
|
|
13
14
|
"indexmap 2.9.0",
|
|
14
15
|
"itertools 0.14.0",
|
|
@@ -21,11 +22,14 @@ dependencies = [
|
|
|
21
22
|
"ruff_python_parser",
|
|
22
23
|
"ruff_source_file",
|
|
23
24
|
"rustc-hash 2.1.1",
|
|
25
|
+
"serde",
|
|
24
26
|
"serde_json",
|
|
27
|
+
"serde_yaml",
|
|
25
28
|
"slotmap",
|
|
26
29
|
"string-interner",
|
|
27
30
|
"tap",
|
|
28
31
|
"thiserror",
|
|
32
|
+
"unindent",
|
|
29
33
|
]
|
|
30
34
|
|
|
31
35
|
[[package]]
|
|
@@ -134,6 +138,15 @@ version = "1.15.0"
|
|
|
134
138
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
135
139
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
|
136
140
|
|
|
141
|
+
[[package]]
|
|
142
|
+
name = "encoding_rs"
|
|
143
|
+
version = "0.8.35"
|
|
144
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
145
|
+
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
|
146
|
+
dependencies = [
|
|
147
|
+
"cfg-if",
|
|
148
|
+
]
|
|
149
|
+
|
|
137
150
|
[[package]]
|
|
138
151
|
name = "equivalent"
|
|
139
152
|
version = "1.0.2"
|
|
@@ -666,6 +679,19 @@ dependencies = [
|
|
|
666
679
|
"serde",
|
|
667
680
|
]
|
|
668
681
|
|
|
682
|
+
[[package]]
|
|
683
|
+
name = "serde_yaml"
|
|
684
|
+
version = "0.9.34+deprecated"
|
|
685
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
686
|
+
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
|
687
|
+
dependencies = [
|
|
688
|
+
"indexmap 2.9.0",
|
|
689
|
+
"itoa",
|
|
690
|
+
"ryu",
|
|
691
|
+
"serde",
|
|
692
|
+
"unsafe-libyaml",
|
|
693
|
+
]
|
|
694
|
+
|
|
669
695
|
[[package]]
|
|
670
696
|
name = "siphasher"
|
|
671
697
|
version = "1.0.1"
|
|
@@ -810,6 +836,12 @@ version = "0.2.4"
|
|
|
810
836
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
811
837
|
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
|
|
812
838
|
|
|
839
|
+
[[package]]
|
|
840
|
+
name = "unsafe-libyaml"
|
|
841
|
+
version = "0.2.11"
|
|
842
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
843
|
+
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
|
844
|
+
|
|
813
845
|
[[package]]
|
|
814
846
|
name = "version_check"
|
|
815
847
|
version = "0.9.5"
|
|
@@ -25,6 +25,10 @@ const_format = "0.2.34"
|
|
|
25
25
|
ruff_python_parser = { git = "https://github.com/astral-sh/ruff.git", tag = "v0.4.10" }
|
|
26
26
|
ruff_python_ast = { git = "https://github.com/astral-sh/ruff.git", tag = "v0.4.10" }
|
|
27
27
|
ruff_source_file = { git = "https://github.com/astral-sh/ruff.git", tag = "v0.4.10" }
|
|
28
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
29
|
+
serde_yaml = "0.9"
|
|
30
|
+
unindent = "0.2.4"
|
|
31
|
+
encoding_rs = "0.8.35"
|
|
28
32
|
|
|
29
33
|
[dependencies.pyo3]
|
|
30
34
|
version = "0.24.1"
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
use pyo3::exceptions::{PyFileNotFoundError, PyUnicodeDecodeError};
|
|
2
|
+
use pyo3::prelude::*;
|
|
3
|
+
use regex::Regex;
|
|
4
|
+
use std::collections::HashMap;
|
|
5
|
+
use std::ffi::OsStr;
|
|
6
|
+
use std::fs;
|
|
7
|
+
use std::path::{Path, PathBuf};
|
|
8
|
+
use unindent::unindent;
|
|
9
|
+
|
|
10
|
+
pub trait FileSystem: Send + Sync {
|
|
11
|
+
fn sep(&self) -> String;
|
|
12
|
+
|
|
13
|
+
fn join(&self, components: Vec<String>) -> String;
|
|
14
|
+
|
|
15
|
+
fn split(&self, file_name: &str) -> (String, String);
|
|
16
|
+
|
|
17
|
+
fn exists(&self, file_name: &str) -> bool;
|
|
18
|
+
|
|
19
|
+
fn read(&self, file_name: &str) -> PyResult<String>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[derive(Clone)]
|
|
23
|
+
#[pyclass]
|
|
24
|
+
pub struct RealBasicFileSystem {}
|
|
25
|
+
|
|
26
|
+
// Implements a BasicFileSystem (defined in grimp.application.ports.filesystem.BasicFileSystem)
|
|
27
|
+
// that actually reads files.
|
|
28
|
+
#[pyclass(name = "RealBasicFileSystem")]
|
|
29
|
+
pub struct PyRealBasicFileSystem {
|
|
30
|
+
pub inner: RealBasicFileSystem,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl FileSystem for RealBasicFileSystem {
|
|
34
|
+
fn sep(&self) -> String {
|
|
35
|
+
std::path::MAIN_SEPARATOR.to_string()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn join(&self, components: Vec<String>) -> String {
|
|
39
|
+
let mut path = PathBuf::new();
|
|
40
|
+
for component in components {
|
|
41
|
+
path.push(component);
|
|
42
|
+
}
|
|
43
|
+
path.to_str().unwrap().to_string()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn split(&self, file_name: &str) -> (String, String) {
|
|
47
|
+
let path = Path::new(file_name);
|
|
48
|
+
|
|
49
|
+
// Get the "tail" part (the file name or last directory)
|
|
50
|
+
let tail = match path.file_name() {
|
|
51
|
+
Some(name) => PathBuf::from(name),
|
|
52
|
+
None => PathBuf::new(), // If there's no file name (e.g., path is a root), return empty
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Get the "head" part (the parent directory)
|
|
56
|
+
let head = match path.parent() {
|
|
57
|
+
Some(parent_path) => parent_path.to_path_buf(),
|
|
58
|
+
None => PathBuf::new(), // If there's no parent (e.g., just a filename), return empty
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
(
|
|
62
|
+
head.to_str().unwrap().to_string(),
|
|
63
|
+
tail.to_str().unwrap().to_string(),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn exists(&self, file_name: &str) -> bool {
|
|
68
|
+
Path::new(file_name).is_file()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn read(&self, file_name: &str) -> PyResult<String> {
|
|
72
|
+
// Python files are assumed UTF-8 by default (PEP 686), but they can specify an alternative
|
|
73
|
+
// encoding, which we need to take into account here.
|
|
74
|
+
// See https://peps.python.org/pep-0263/
|
|
75
|
+
|
|
76
|
+
// This method was authored primarily by an LLM.
|
|
77
|
+
|
|
78
|
+
let path = Path::new(file_name);
|
|
79
|
+
let bytes = fs::read(path).map_err(|e| {
|
|
80
|
+
PyFileNotFoundError::new_err(format!("Failed to read file {file_name}: {e}"))
|
|
81
|
+
})?;
|
|
82
|
+
|
|
83
|
+
let s = String::from_utf8_lossy(&bytes);
|
|
84
|
+
let encoding_re = Regex::new(r"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)").unwrap();
|
|
85
|
+
|
|
86
|
+
let mut detected_encoding: Option<String> = None;
|
|
87
|
+
|
|
88
|
+
// Coding specification needs to be in the first two lines, or it's ignored.
|
|
89
|
+
for line in s.lines().take(2) {
|
|
90
|
+
if let Some(captures) = encoding_re.captures(line)
|
|
91
|
+
&& let Some(encoding_name) = captures.get(1) {
|
|
92
|
+
detected_encoding = Some(encoding_name.as_str().to_string());
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if let Some(enc_name) = detected_encoding {
|
|
98
|
+
let encoding =
|
|
99
|
+
encoding_rs::Encoding::for_label(enc_name.as_bytes()).ok_or_else(|| {
|
|
100
|
+
PyUnicodeDecodeError::new_err(format!(
|
|
101
|
+
"Failed to decode file {file_name} (unknown encoding '{enc_name}')"
|
|
102
|
+
))
|
|
103
|
+
})?;
|
|
104
|
+
let (decoded_s, _, had_errors) = encoding.decode(&bytes);
|
|
105
|
+
if had_errors {
|
|
106
|
+
Err(PyUnicodeDecodeError::new_err(format!(
|
|
107
|
+
"Failed to decode file {file_name} with encoding '{enc_name}'"
|
|
108
|
+
)))
|
|
109
|
+
} else {
|
|
110
|
+
Ok(decoded_s.into_owned())
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Default to UTF-8 if no encoding is specified
|
|
114
|
+
String::from_utf8(bytes).map_err(|e| {
|
|
115
|
+
PyUnicodeDecodeError::new_err(format!(
|
|
116
|
+
"Failed to decode file {file_name} as UTF-8: {e}"
|
|
117
|
+
))
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[pymethods]
|
|
124
|
+
impl PyRealBasicFileSystem {
|
|
125
|
+
#[new]
|
|
126
|
+
fn new() -> Self {
|
|
127
|
+
PyRealBasicFileSystem {
|
|
128
|
+
inner: RealBasicFileSystem {},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[getter]
|
|
133
|
+
fn sep(&self) -> String {
|
|
134
|
+
self.inner.sep()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[pyo3(signature = (*components))]
|
|
138
|
+
fn join(&self, components: Vec<String>) -> String {
|
|
139
|
+
self.inner.join(components)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn split(&self, file_name: &str) -> (String, String) {
|
|
143
|
+
self.inner.split(file_name)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn exists(&self, file_name: &str) -> bool {
|
|
147
|
+
self.inner.exists(file_name)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn read(&self, file_name: &str) -> PyResult<String> {
|
|
151
|
+
self.inner.read(file_name)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
type FileSystemContents = HashMap<String, String>;
|
|
156
|
+
|
|
157
|
+
#[derive(Clone)]
|
|
158
|
+
pub struct FakeBasicFileSystem {
|
|
159
|
+
contents: Box<FileSystemContents>,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Implements BasicFileSystem (defined in grimp.application.ports.filesystem.BasicFileSystem).
|
|
163
|
+
#[pyclass(name = "FakeBasicFileSystem")]
|
|
164
|
+
pub struct PyFakeBasicFileSystem {
|
|
165
|
+
pub inner: FakeBasicFileSystem,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
impl FakeBasicFileSystem {
|
|
169
|
+
fn new(contents: Option<&str>, content_map: Option<HashMap<String, String>>) -> PyResult<Self> {
|
|
170
|
+
let mut parsed_contents = match contents {
|
|
171
|
+
Some(contents) => parse_indented_file_system_string(contents),
|
|
172
|
+
None => HashMap::new(),
|
|
173
|
+
};
|
|
174
|
+
if let Some(content_map) = content_map {
|
|
175
|
+
let unindented_map: HashMap<String, String> = content_map
|
|
176
|
+
.into_iter()
|
|
177
|
+
.map(|(key, val)| (key, unindent(&val).trim().to_string()))
|
|
178
|
+
.collect();
|
|
179
|
+
parsed_contents.extend(unindented_map);
|
|
180
|
+
};
|
|
181
|
+
Ok(FakeBasicFileSystem {
|
|
182
|
+
contents: Box::new(parsed_contents),
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
impl FileSystem for FakeBasicFileSystem {
|
|
188
|
+
fn sep(&self) -> String {
|
|
189
|
+
"/".to_string()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn join(&self, components: Vec<String>) -> String {
|
|
193
|
+
let sep = self.sep();
|
|
194
|
+
components
|
|
195
|
+
.into_iter()
|
|
196
|
+
.map(|c| c.trim_end_matches(&sep).to_string())
|
|
197
|
+
.collect::<Vec<String>>()
|
|
198
|
+
.join(&sep)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fn split(&self, file_name: &str) -> (String, String) {
|
|
202
|
+
let path;
|
|
203
|
+
let head;
|
|
204
|
+
let tail;
|
|
205
|
+
if let Some(file_name_without_trailing_slash) = file_name.strip_suffix("/") {
|
|
206
|
+
head = Path::new(file_name_without_trailing_slash);
|
|
207
|
+
tail = OsStr::new("");
|
|
208
|
+
} else {
|
|
209
|
+
path = Path::new(file_name);
|
|
210
|
+
head = path.parent().unwrap_or(Path::new(""));
|
|
211
|
+
tail = path.file_name().unwrap_or(OsStr::new(""));
|
|
212
|
+
}
|
|
213
|
+
(
|
|
214
|
+
head.to_str().unwrap().to_string(),
|
|
215
|
+
tail.to_str().unwrap().to_string(),
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Checks if a file or directory exists within the file system.
|
|
220
|
+
fn exists(&self, file_name: &str) -> bool {
|
|
221
|
+
self.contents.contains_key(file_name)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
fn read(&self, file_name: &str) -> PyResult<String> {
|
|
225
|
+
match self.contents.get(file_name) {
|
|
226
|
+
Some(file_name) => Ok(file_name.clone()),
|
|
227
|
+
None => Err(PyFileNotFoundError::new_err("")),
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[pymethods]
|
|
233
|
+
impl PyFakeBasicFileSystem {
|
|
234
|
+
#[pyo3(signature = (contents=None, content_map=None))]
|
|
235
|
+
#[new]
|
|
236
|
+
fn new(contents: Option<&str>, content_map: Option<HashMap<String, String>>) -> PyResult<Self> {
|
|
237
|
+
Ok(PyFakeBasicFileSystem {
|
|
238
|
+
inner: FakeBasicFileSystem::new(contents, content_map)?,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#[getter]
|
|
243
|
+
fn sep(&self) -> String {
|
|
244
|
+
self.inner.sep()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[pyo3(signature = (*components))]
|
|
248
|
+
fn join(&self, components: Vec<String>) -> String {
|
|
249
|
+
self.inner.join(components)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn split(&self, file_name: &str) -> (String, String) {
|
|
253
|
+
self.inner.split(file_name)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Checks if a file or directory exists within the file system.
|
|
257
|
+
fn exists(&self, file_name: &str) -> bool {
|
|
258
|
+
self.inner.exists(file_name)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
fn read(&self, file_name: &str) -> PyResult<String> {
|
|
262
|
+
self.inner.read(file_name)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Temporary workaround method for Python tests.
|
|
266
|
+
fn convert_to_basic(&self) -> PyResult<Self> {
|
|
267
|
+
Ok(PyFakeBasicFileSystem {
|
|
268
|
+
inner: self.inner.clone()
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// Parses an indented string representing a file system structure
|
|
274
|
+
/// into a HashMap where keys are full file paths.
|
|
275
|
+
/// See tests.adaptors.filesystem.FakeFileSystem for the API.
|
|
276
|
+
pub fn parse_indented_file_system_string(file_system_string: &str) -> HashMap<String, String> {
|
|
277
|
+
let mut file_paths_map: HashMap<String, String> = HashMap::new();
|
|
278
|
+
let mut path_stack: Vec<String> = Vec::new(); // Stores current directory path components
|
|
279
|
+
let mut first_line = true; // Flag to handle the very first path component
|
|
280
|
+
|
|
281
|
+
// Normalize newlines and split into lines
|
|
282
|
+
let buffer = file_system_string.replace("\r\n", "\n");
|
|
283
|
+
let lines: Vec<&str> = buffer.split('\n').collect();
|
|
284
|
+
|
|
285
|
+
for line_raw in lines.clone() {
|
|
286
|
+
let line = line_raw.trim_end(); // Remove trailing whitespace
|
|
287
|
+
if line.is_empty() {
|
|
288
|
+
continue; // Skip empty lines
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let current_indent = line.chars().take_while(|&c| c.is_whitespace()).count();
|
|
292
|
+
let trimmed_line = line.trim_start();
|
|
293
|
+
|
|
294
|
+
// Assuming 4 spaces per indentation level for calculating depth
|
|
295
|
+
// Adjust this if your indentation standard is different (e.g., 2 spaces, tabs)
|
|
296
|
+
let current_depth = current_indent / 4;
|
|
297
|
+
|
|
298
|
+
if first_line {
|
|
299
|
+
// The first non-empty line sets the base path.
|
|
300
|
+
// It might be absolute (/a/b/) or relative (a/b/).
|
|
301
|
+
let root_component = trimmed_line.trim_end_matches('/').to_string();
|
|
302
|
+
path_stack.push(root_component);
|
|
303
|
+
first_line = false;
|
|
304
|
+
} else {
|
|
305
|
+
// Adjust the path_stack based on indentation level
|
|
306
|
+
// Pop elements from the stack until we reach the correct parent directory depth
|
|
307
|
+
while path_stack.len() > current_depth {
|
|
308
|
+
path_stack.pop();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// If the current line is a file, append it to the path for inserting into map,
|
|
312
|
+
// then pop it off so that subsequent siblings are correctly handled.
|
|
313
|
+
// If it's a directory, append it and it stays on the stack for its children.
|
|
314
|
+
let component_to_add = trimmed_line.trim_end_matches('/').to_string();
|
|
315
|
+
if !component_to_add.is_empty() {
|
|
316
|
+
// Avoid pushing empty strings due to lines like just "/"
|
|
317
|
+
path_stack.push(component_to_add);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Construct the full path
|
|
322
|
+
// Join components on the stack. If the first component started with '/',
|
|
323
|
+
// ensure the final path also starts with '/'.
|
|
324
|
+
let full_path = if !path_stack.is_empty() {
|
|
325
|
+
let mut joined = path_stack.join("/");
|
|
326
|
+
// If the original root started with a slash, ensure the final path does too.
|
|
327
|
+
// But be careful not to double-slash if a component is e.g. "/root"
|
|
328
|
+
if lines[0].trim().starts_with('/') && !joined.starts_with('/') {
|
|
329
|
+
joined = format!("/{joined}");
|
|
330
|
+
}
|
|
331
|
+
joined
|
|
332
|
+
} else {
|
|
333
|
+
"".to_string()
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// If it's a file (doesn't end with '/'), add it to the map
|
|
337
|
+
// A file is not a directory, so its name should be removed from the stack after processing
|
|
338
|
+
// so that sibling items are at the correct level.
|
|
339
|
+
if !trimmed_line.ends_with('/') {
|
|
340
|
+
file_paths_map.insert(full_path, String::new()); // Value can be empty or actual content
|
|
341
|
+
if !path_stack.is_empty() {
|
|
342
|
+
path_stack.pop(); // Pop the file name off the stack
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Edge case: If the very first line was a file and it ended up on the stack, it needs to be processed.
|
|
348
|
+
// This handles single-file inputs like "myfile.txt"
|
|
349
|
+
if !path_stack.is_empty()
|
|
350
|
+
&& !path_stack.last().unwrap().ends_with('/')
|
|
351
|
+
&& !file_paths_map.contains_key(&path_stack.join("/"))
|
|
352
|
+
{
|
|
353
|
+
file_paths_map.insert(path_stack.join("/"), String::new());
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
file_paths_map
|
|
357
|
+
}
|
|
@@ -17,7 +17,7 @@ impl Graph {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// TODO(peter) Guarantee order?
|
|
20
|
-
pub fn all_modules(&self) -> impl ModuleIterator {
|
|
20
|
+
pub fn all_modules(&self) -> impl ModuleIterator<'_> {
|
|
21
21
|
self.modules.values()
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -28,7 +28,7 @@ impl Graph {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
pub fn get_module_children(&self, module: ModuleToken) -> impl ModuleIterator {
|
|
31
|
+
pub fn get_module_children(&self, module: ModuleToken) -> impl ModuleIterator<'_> {
|
|
32
32
|
let children = match self.module_children.get(module) {
|
|
33
33
|
Some(children) => children
|
|
34
34
|
.iter()
|
|
@@ -42,7 +42,7 @@ impl Graph {
|
|
|
42
42
|
/// Returns an iterator over the passed modules descendants.
|
|
43
43
|
///
|
|
44
44
|
/// Parent modules will be yielded before their child modules.
|
|
45
|
-
pub fn get_module_descendants(&self, module: ModuleToken) -> impl ModuleIterator {
|
|
45
|
+
pub fn get_module_descendants(&self, module: ModuleToken) -> impl ModuleIterator<'_> {
|
|
46
46
|
let mut descendants = self.get_module_children(module).collect::<Vec<_>>();
|
|
47
47
|
for child in descendants.clone() {
|
|
48
48
|
descendants.extend(self.get_module_descendants(child.token).collect::<Vec<_>>())
|
|
@@ -53,7 +53,7 @@ impl Graph {
|
|
|
53
53
|
pub fn find_matching_modules(
|
|
54
54
|
&self,
|
|
55
55
|
expression: &ModuleExpression,
|
|
56
|
-
) -> impl ModuleIterator + use<'_> {
|
|
56
|
+
) -> impl ModuleIterator<'_> + use<'_> {
|
|
57
57
|
let interner = MODULE_NAMES.read().unwrap();
|
|
58
58
|
let modules: FxHashSet<_> = self
|
|
59
59
|
.modules
|