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