grimp 3.16__tar.gz → 3.18__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.16 → grimp-3.18}/PKG-INFO +1 -1
- {grimp-3.16 → grimp-3.18}/pyproject.toml +1 -1
- {grimp-3.16 → grimp-3.18}/rust/src/caching.rs +40 -10
- {grimp-3.16 → grimp-3.18}/rust/src/errors.rs +16 -1
- {grimp-3.16 → grimp-3.18}/rust/src/exceptions.rs +1 -0
- {grimp-3.16 → grimp-3.18}/rust/src/filesystem.rs +71 -22
- {grimp-3.16 → grimp-3.18}/rust/src/graph/graph_manipulation.rs +2 -1
- {grimp-3.16 → grimp-3.18}/rust/src/graph/mod.rs +35 -13
- {grimp-3.16 → grimp-3.18}/rust/src/import_parsing.rs +26 -18
- {grimp-3.16 → grimp-3.18}/rust/src/import_scanning.rs +7 -1
- {grimp-3.16 → grimp-3.18}/rust/src/lib.rs +2 -1
- {grimp-3.16 → grimp-3.18}/src/grimp/__init__.py +1 -1
- {grimp-3.16 → grimp-3.18}/src/grimp/adaptors/caching.py +6 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/application/graph.py +8 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/application/usecases.py +1 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/domain/valueobjects.py +4 -1
- {grimp-3.16 → grimp-3.18}/README.rst +0 -0
- {grimp-3.16 → grimp-3.18}/rust/Cargo.lock +0 -0
- {grimp-3.16 → grimp-3.18}/rust/Cargo.toml +0 -0
- {grimp-3.16 → grimp-3.18}/rust/build.rs +0 -0
- {grimp-3.16 → grimp-3.18}/rust/rust-toolchain.toml +0 -0
- {grimp-3.16 → grimp-3.18}/rust/src/graph/cycle_breakers.rs +0 -0
- {grimp-3.16 → grimp-3.18}/rust/src/graph/direct_import_queries.rs +0 -0
- {grimp-3.16 → grimp-3.18}/rust/src/graph/hierarchy_queries.rs +0 -0
- {grimp-3.16 → grimp-3.18}/rust/src/graph/higher_order_queries.rs +0 -0
- {grimp-3.16 → grimp-3.18}/rust/src/graph/import_chain_queries.rs +0 -0
- {grimp-3.16 → grimp-3.18}/rust/src/graph/pathfinding.rs +0 -0
- {grimp-3.16 → grimp-3.18}/rust/src/module_expressions.rs +0 -0
- {grimp-3.16 → grimp-3.18}/rust/src/module_finding.rs +0 -0
- {grimp-3.16 → grimp-3.18}/rust/tests/large.rs +0 -0
- {grimp-3.16 → grimp-3.18}/rust/tests/large_graph.json +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/adaptors/__init__.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/adaptors/filesystem.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/adaptors/modulefinder.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/adaptors/packagefinder.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/adaptors/timing.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/application/__init__.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/application/config.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/application/ports/__init__.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/application/ports/caching.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/application/ports/filesystem.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/application/ports/modulefinder.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/application/ports/packagefinder.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/application/ports/timing.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/application/scanning.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/domain/__init__.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/domain/analysis.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/exceptions.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/main.py +0 -0
- {grimp-3.16 → grimp-3.18}/src/grimp/py.typed +0 -0
|
@@ -75,17 +75,27 @@ impl<'a, 'py> FromPyObject<'a, 'py> for ImportsByModule {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/// The version of the data cache file format.
|
|
79
|
+
///
|
|
80
|
+
/// Bump this whenever the serialized structure changes. On read, a file with a
|
|
81
|
+
/// different (or missing) version is treated as a version mismatch and rebuilt,
|
|
82
|
+
/// rather than being mistaken for a corrupt file.
|
|
83
|
+
const CACHE_SCHEMA_VERSION: u32 = 2;
|
|
84
|
+
|
|
78
85
|
fn serialize_imports_by_module(
|
|
79
86
|
imports_by_module: &HashMap<Module, HashSet<DirectImport>>,
|
|
80
87
|
) -> String {
|
|
81
|
-
|
|
88
|
+
// Fields are ordered to match the `get_import_details` dict (`imported`, `is_lazy`,
|
|
89
|
+
// `line_number`, `line_contents`); `importer` is the map key.
|
|
90
|
+
let raw_map: HashMap<&str, Vec<(&str, bool, usize, &str)>> = imports_by_module
|
|
82
91
|
.iter()
|
|
83
92
|
.map(|(module, imports)| {
|
|
84
|
-
let imports_vec: Vec<(&str, usize, &str)> = imports
|
|
93
|
+
let imports_vec: Vec<(&str, bool, usize, &str)> = imports
|
|
85
94
|
.iter()
|
|
86
95
|
.map(|import| {
|
|
87
96
|
(
|
|
88
97
|
import.imported.as_str(),
|
|
98
|
+
import.is_lazy,
|
|
89
99
|
import.line_number,
|
|
90
100
|
import.line_contents.as_str(),
|
|
91
101
|
)
|
|
@@ -95,16 +105,33 @@ fn serialize_imports_by_module(
|
|
|
95
105
|
})
|
|
96
106
|
.collect();
|
|
97
107
|
|
|
98
|
-
serde_json::
|
|
108
|
+
let envelope = serde_json::json!({
|
|
109
|
+
"version": CACHE_SCHEMA_VERSION,
|
|
110
|
+
"imports_by_module": raw_map,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
serde_json::to_string(&envelope).expect("Failed to serialize to JSON")
|
|
99
114
|
}
|
|
100
115
|
|
|
101
116
|
pub fn parse_json_to_map(
|
|
102
117
|
json_str: &str,
|
|
103
118
|
filename: &str,
|
|
104
119
|
) -> GrimpResult<HashMap<Module, HashSet<DirectImport>>> {
|
|
105
|
-
|
|
120
|
+
// Parse into a generic value first, so we can distinguish genuinely corrupt
|
|
121
|
+
// JSON from a cache file written by a different format version (e.g. by an
|
|
122
|
+
// older Grimp). The latter should be silently rebuilt, not warned about.
|
|
123
|
+
let value: serde_json::Value = serde_json::from_str(json_str)
|
|
106
124
|
.map_err(|_| GrimpError::CorruptCache(filename.to_string()))?;
|
|
107
125
|
|
|
126
|
+
let version = value.get("version").and_then(|v| v.as_u64());
|
|
127
|
+
if version != Some(CACHE_SCHEMA_VERSION as u64) {
|
|
128
|
+
return Err(GrimpError::CacheVersionMismatch(filename.to_string()));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let raw_map: HashMap<String, Vec<(String, bool, usize, String)>> =
|
|
132
|
+
serde_json::from_value(value.get("imports_by_module").cloned().unwrap_or_default())
|
|
133
|
+
.map_err(|_| GrimpError::CorruptCache(filename.to_string()))?;
|
|
134
|
+
|
|
108
135
|
let mut parsed_map: HashMap<Module, HashSet<DirectImport>> = HashMap::new();
|
|
109
136
|
|
|
110
137
|
for (module_name, imports) in raw_map {
|
|
@@ -113,12 +140,15 @@ pub fn parse_json_to_map(
|
|
|
113
140
|
};
|
|
114
141
|
let import_set: HashSet<DirectImport> = imports
|
|
115
142
|
.into_iter()
|
|
116
|
-
.map(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
143
|
+
.map(
|
|
144
|
+
|(imported, is_lazy, line_number, line_contents)| DirectImport {
|
|
145
|
+
importer: module_name.clone(),
|
|
146
|
+
imported,
|
|
147
|
+
line_number,
|
|
148
|
+
line_contents,
|
|
149
|
+
is_lazy,
|
|
150
|
+
},
|
|
151
|
+
)
|
|
122
152
|
.collect();
|
|
123
153
|
parsed_map.insert(module, import_set);
|
|
124
154
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use crate::exceptions;
|
|
2
2
|
use pyo3::PyErr;
|
|
3
|
-
use pyo3::exceptions::PyValueError;
|
|
3
|
+
use pyo3::exceptions::{PyFileNotFoundError, PyUnicodeError, PyValueError};
|
|
4
4
|
use ruff_python_parser::ParseError as RuffParseError;
|
|
5
5
|
use thiserror::Error;
|
|
6
6
|
|
|
@@ -36,6 +36,15 @@ pub enum GrimpError {
|
|
|
36
36
|
|
|
37
37
|
#[error("Could not use corrupt cache file {0}.")]
|
|
38
38
|
CorruptCache(String),
|
|
39
|
+
|
|
40
|
+
#[error("Cache file {0} was written by a different version of Grimp.")]
|
|
41
|
+
CacheVersionMismatch(String),
|
|
42
|
+
|
|
43
|
+
#[error("{0}")]
|
|
44
|
+
FileNotFound(String),
|
|
45
|
+
|
|
46
|
+
#[error("{0}")]
|
|
47
|
+
UndecodableFile(String),
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
pub type GrimpResult<T> = Result<T, GrimpError>;
|
|
@@ -55,6 +64,12 @@ impl From<GrimpError> for PyErr {
|
|
|
55
64
|
line_number, text, ..
|
|
56
65
|
} => PyErr::new::<exceptions::ParseError, _>((line_number, text)),
|
|
57
66
|
GrimpError::CorruptCache(_) => exceptions::CorruptCache::new_err(value.to_string()),
|
|
67
|
+
GrimpError::CacheVersionMismatch(_) => {
|
|
68
|
+
exceptions::CacheVersionMismatch::new_err(value.to_string())
|
|
69
|
+
}
|
|
70
|
+
GrimpError::FileNotFound(_) => PyFileNotFoundError::new_err(value.to_string()),
|
|
71
|
+
// Not UnicodeDecodeError, as that can't be created from just a message.
|
|
72
|
+
GrimpError::UndecodableFile(_) => PyUnicodeError::new_err(value.to_string()),
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
}
|
|
@@ -6,6 +6,7 @@ create_exception!(_rustgrimp, ModuleNotPresent, PyException);
|
|
|
6
6
|
create_exception!(_rustgrimp, NoSuchContainer, PyException);
|
|
7
7
|
create_exception!(_rustgrimp, InvalidModuleExpression, PyException);
|
|
8
8
|
create_exception!(_rustgrimp, CorruptCache, PyException);
|
|
9
|
+
create_exception!(_rustgrimp, CacheVersionMismatch, PyException);
|
|
9
10
|
|
|
10
11
|
// We need to use here `pyclass(extends=PyException)` instead of `create_exception!`
|
|
11
12
|
// since the exception contains custom data. See:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
use crate::errors::{GrimpError, GrimpResult};
|
|
1
2
|
use itertools::Itertools;
|
|
2
|
-
use pyo3::exceptions::
|
|
3
|
+
use pyo3::exceptions::PyTypeError;
|
|
3
4
|
use pyo3::prelude::*;
|
|
4
5
|
use regex::Regex;
|
|
5
6
|
use std::collections::HashMap;
|
|
@@ -14,6 +15,48 @@ use unindent::unindent;
|
|
|
14
15
|
static ENCODING_RE: LazyLock<Regex> =
|
|
15
16
|
LazyLock::new(|| Regex::new(r"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)").unwrap());
|
|
16
17
|
|
|
18
|
+
/// An encoding named in a Python source file's encoding declaration.
|
|
19
|
+
enum Encoding {
|
|
20
|
+
/// True Latin-1 (ISO-8859-1): a straight byte-to-code-point mapping. This is handled
|
|
21
|
+
/// specially because encoding_rs follows the WHATWG standard, under which the `iso-8859-1`
|
|
22
|
+
/// label resolves to the Windows-1252 decoder instead (the two differ in the 0x80..=0x9f
|
|
23
|
+
/// range), and because Windows-1252 has undefined bytes that true Latin-1 decodes fine.
|
|
24
|
+
Latin1,
|
|
25
|
+
/// Any other encoding, delegated to encoding_rs.
|
|
26
|
+
Other(&'static encoding_rs::Encoding),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Look up the encoding named in a Python source file's encoding declaration.
|
|
30
|
+
///
|
|
31
|
+
/// encoding_rs only knows the WHATWG labels, but Python accepts other spellings too, such as
|
|
32
|
+
/// `latin-1`, `utf_8`, `utf-8-sig` or `euc_jp`.
|
|
33
|
+
fn lookup_encoding(name: &str) -> Option<Encoding> {
|
|
34
|
+
// Normalize the name like Python does: see `_get_normal_name` in Lib/tokenize.py, which
|
|
35
|
+
// special cases UTF-8 and Latin-1. Python's codec lookup also ignores the difference
|
|
36
|
+
// between underscores and hyphens.
|
|
37
|
+
//
|
|
38
|
+
// We check these special cases before deferring to encoding_rs, because some of the Latin-1
|
|
39
|
+
// aliases (e.g. `iso-8859-1`) are WHATWG labels that encoding_rs would otherwise resolve to
|
|
40
|
+
// the Windows-1252 decoder.
|
|
41
|
+
let normalized = name.to_ascii_lowercase().replace('_', "-");
|
|
42
|
+
let is_spelling_of = |canonical: &str| {
|
|
43
|
+
normalized == canonical || normalized.starts_with(&format!("{canonical}-"))
|
|
44
|
+
};
|
|
45
|
+
if ["latin-1", "iso-8859-1", "iso-latin-1"]
|
|
46
|
+
.into_iter()
|
|
47
|
+
.any(is_spelling_of)
|
|
48
|
+
{
|
|
49
|
+
return Some(Encoding::Latin1);
|
|
50
|
+
}
|
|
51
|
+
if is_spelling_of("utf-8") {
|
|
52
|
+
return encoding_rs::Encoding::for_label(b"utf-8").map(Encoding::Other);
|
|
53
|
+
}
|
|
54
|
+
// Try the name as given, then its normalized form.
|
|
55
|
+
encoding_rs::Encoding::for_label(name.as_bytes())
|
|
56
|
+
.or_else(|| encoding_rs::Encoding::for_label(normalized.as_bytes()))
|
|
57
|
+
.map(Encoding::Other)
|
|
58
|
+
}
|
|
59
|
+
|
|
17
60
|
pub trait FileSystem: Send + Sync {
|
|
18
61
|
fn sep(&self) -> &str;
|
|
19
62
|
|
|
@@ -23,7 +66,7 @@ pub trait FileSystem: Send + Sync {
|
|
|
23
66
|
|
|
24
67
|
fn exists(&self, file_name: &str) -> bool;
|
|
25
68
|
|
|
26
|
-
fn read(&self, file_name: &str) ->
|
|
69
|
+
fn read(&self, file_name: &str) -> GrimpResult<String>;
|
|
27
70
|
|
|
28
71
|
fn write(&mut self, file_name: &str, contents: &str) -> PyResult<()>;
|
|
29
72
|
}
|
|
@@ -83,7 +126,7 @@ impl FileSystem for RealBasicFileSystem {
|
|
|
83
126
|
Path::new(file_name).is_file()
|
|
84
127
|
}
|
|
85
128
|
|
|
86
|
-
fn read(&self, file_name: &str) ->
|
|
129
|
+
fn read(&self, file_name: &str) -> GrimpResult<String> {
|
|
87
130
|
// Python files are assumed UTF-8 by default (PEP 686), but they can specify an alternative
|
|
88
131
|
// encoding, which we need to take into account here.
|
|
89
132
|
// See https://peps.python.org/pep-0263/
|
|
@@ -92,7 +135,7 @@ impl FileSystem for RealBasicFileSystem {
|
|
|
92
135
|
|
|
93
136
|
let path = Path::new(file_name);
|
|
94
137
|
let bytes = fs::read(path).map_err(|e| {
|
|
95
|
-
|
|
138
|
+
GrimpError::FileNotFound(format!("Failed to read file {file_name}: {e}"))
|
|
96
139
|
})?;
|
|
97
140
|
|
|
98
141
|
let s = String::from_utf8_lossy(&bytes);
|
|
@@ -110,24 +153,30 @@ impl FileSystem for RealBasicFileSystem {
|
|
|
110
153
|
}
|
|
111
154
|
|
|
112
155
|
if let Some(enc_name) = detected_encoding {
|
|
113
|
-
let encoding =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
156
|
+
let encoding = lookup_encoding(&enc_name).ok_or_else(|| {
|
|
157
|
+
GrimpError::UndecodableFile(format!(
|
|
158
|
+
"Failed to decode file {file_name} (unknown encoding '{enc_name}')"
|
|
159
|
+
))
|
|
160
|
+
})?;
|
|
161
|
+
match encoding {
|
|
162
|
+
// True Latin-1 maps every byte to the code point of the same value and so can
|
|
163
|
+
// never fail.
|
|
164
|
+
Encoding::Latin1 => Ok(bytes.iter().map(|&b| b as char).collect()),
|
|
165
|
+
Encoding::Other(encoding) => {
|
|
166
|
+
let (decoded_s, _, had_errors) = encoding.decode(&bytes);
|
|
167
|
+
if had_errors {
|
|
168
|
+
Err(GrimpError::UndecodableFile(format!(
|
|
169
|
+
"Failed to decode file {file_name} with encoding '{enc_name}'"
|
|
170
|
+
)))
|
|
171
|
+
} else {
|
|
172
|
+
Ok(decoded_s.into_owned())
|
|
173
|
+
}
|
|
174
|
+
}
|
|
126
175
|
}
|
|
127
176
|
} else {
|
|
128
177
|
// Default to UTF-8 if no encoding is specified
|
|
129
178
|
String::from_utf8(bytes).map_err(|e| {
|
|
130
|
-
|
|
179
|
+
GrimpError::UndecodableFile(format!(
|
|
131
180
|
"Failed to decode file {file_name} as UTF-8: {e}"
|
|
132
181
|
))
|
|
133
182
|
})
|
|
@@ -173,7 +222,7 @@ impl PyRealBasicFileSystem {
|
|
|
173
222
|
}
|
|
174
223
|
|
|
175
224
|
fn read(&self, file_name: &str) -> PyResult<String> {
|
|
176
|
-
self.inner.read(file_name)
|
|
225
|
+
Ok(self.inner.read(file_name)?)
|
|
177
226
|
}
|
|
178
227
|
|
|
179
228
|
fn write(&mut self, file_name: &str, contents: &str) -> PyResult<()> {
|
|
@@ -253,11 +302,11 @@ impl FileSystem for FakeBasicFileSystem {
|
|
|
253
302
|
self.contents.lock().unwrap().contains_key(file_name)
|
|
254
303
|
}
|
|
255
304
|
|
|
256
|
-
fn read(&self, file_name: &str) ->
|
|
305
|
+
fn read(&self, file_name: &str) -> GrimpResult<String> {
|
|
257
306
|
let contents = self.contents.lock().unwrap();
|
|
258
307
|
match contents.get(file_name) {
|
|
259
308
|
Some(file_contents) => Ok(file_contents.clone()),
|
|
260
|
-
None => Err(
|
|
309
|
+
None => Err(GrimpError::FileNotFound(format!(
|
|
261
310
|
"No such file: {file_name}"
|
|
262
311
|
))),
|
|
263
312
|
}
|
|
@@ -301,7 +350,7 @@ impl PyFakeBasicFileSystem {
|
|
|
301
350
|
}
|
|
302
351
|
|
|
303
352
|
fn read(&self, file_name: &str) -> PyResult<String> {
|
|
304
|
-
self.inner.read(file_name)
|
|
353
|
+
Ok(self.inner.read(file_name)?)
|
|
305
354
|
}
|
|
306
355
|
|
|
307
356
|
fn write(&mut self, file_name: &str, contents: &str) -> PyResult<()> {
|
|
@@ -120,6 +120,7 @@ impl Graph {
|
|
|
120
120
|
imported: ModuleToken,
|
|
121
121
|
line_number: u32,
|
|
122
122
|
line_contents: &str,
|
|
123
|
+
is_lazy: bool,
|
|
123
124
|
) {
|
|
124
125
|
self.imports
|
|
125
126
|
.entry(importer)
|
|
@@ -137,7 +138,7 @@ impl Graph {
|
|
|
137
138
|
self.import_details
|
|
138
139
|
.entry((importer, imported))
|
|
139
140
|
.or_default()
|
|
140
|
-
.insert(PyImportDetails::new(line_number, line_contents));
|
|
141
|
+
.insert(PyImportDetails::new(line_number, line_contents, is_lazy));
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
|
|
@@ -264,29 +264,45 @@ impl GraphWrapper {
|
|
|
264
264
|
Ok(self.get_visible_module_by_name(module)?.is_squashed())
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
#[pyo3(signature = (*, importer, imported, line_number=None, line_contents=None))]
|
|
267
|
+
#[pyo3(signature = (*, importer, imported, is_lazy=None, line_number=None, line_contents=None))]
|
|
268
268
|
pub fn add_import(
|
|
269
269
|
&mut self,
|
|
270
270
|
importer: &str,
|
|
271
271
|
imported: &str,
|
|
272
|
+
is_lazy: Option<bool>,
|
|
272
273
|
line_number: Option<u32>,
|
|
273
274
|
line_contents: Option<&str>,
|
|
274
|
-
) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
self._graph
|
|
280
|
-
.add_detailed_import(importer, imported, line_number, line_contents)
|
|
281
|
-
}
|
|
282
|
-
(None, None) => {
|
|
275
|
+
) -> PyResult<()> {
|
|
276
|
+
match (line_number, line_contents, is_lazy) {
|
|
277
|
+
(None, None, None) => {
|
|
278
|
+
let importer = self._graph.get_or_add_module(importer).token();
|
|
279
|
+
let imported = self._graph.get_or_add_module(imported).token();
|
|
283
280
|
self._graph.add_import(importer, imported);
|
|
284
281
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
282
|
+
(Some(line_number), Some(line_contents), is_lazy) => {
|
|
283
|
+
let is_lazy = is_lazy.unwrap_or(false);
|
|
284
|
+
let importer = self._graph.get_or_add_module(importer).token();
|
|
285
|
+
let imported = self._graph.get_or_add_module(imported).token();
|
|
286
|
+
self._graph.add_detailed_import(
|
|
287
|
+
importer,
|
|
288
|
+
imported,
|
|
289
|
+
line_number,
|
|
290
|
+
line_contents,
|
|
291
|
+
is_lazy,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
(_, _, Some(_is_lazy)) => {
|
|
295
|
+
return Err(PyValueError::new_err(
|
|
296
|
+
"You must provide line_number and line_contents when providing is_lazy.",
|
|
297
|
+
));
|
|
298
|
+
}
|
|
299
|
+
(_, _, None) => {
|
|
300
|
+
return Err(PyValueError::new_err(
|
|
301
|
+
"Expected line_number and line_contents, or neither.",
|
|
302
|
+
));
|
|
288
303
|
}
|
|
289
304
|
}
|
|
305
|
+
Ok(())
|
|
290
306
|
}
|
|
291
307
|
|
|
292
308
|
#[pyo3(signature = (*, importer, imported))]
|
|
@@ -402,6 +418,7 @@ impl GraphWrapper {
|
|
|
402
418
|
imported.name(),
|
|
403
419
|
import_details.line_number(),
|
|
404
420
|
import_details.line_contents(),
|
|
421
|
+
import_details.is_lazy(),
|
|
405
422
|
)
|
|
406
423
|
})
|
|
407
424
|
.sorted()
|
|
@@ -417,6 +434,7 @@ impl GraphWrapper {
|
|
|
417
434
|
"line_contents",
|
|
418
435
|
import_details.line_contents.into_py_any(py).unwrap(),
|
|
419
436
|
),
|
|
437
|
+
("is_lazy", import_details.is_lazy.into_py_any(py).unwrap()),
|
|
420
438
|
]
|
|
421
439
|
.into_py_dict(py)
|
|
422
440
|
.unwrap()
|
|
@@ -667,6 +685,7 @@ struct ImportDetails {
|
|
|
667
685
|
imported: String,
|
|
668
686
|
line_number: u32,
|
|
669
687
|
line_contents: String,
|
|
688
|
+
is_lazy: bool,
|
|
670
689
|
}
|
|
671
690
|
|
|
672
691
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, new)]
|
|
@@ -754,6 +773,9 @@ pub struct PyImportDetails {
|
|
|
754
773
|
|
|
755
774
|
#[getset(get_copy = "pub")]
|
|
756
775
|
interned_line_contents: DefaultSymbol,
|
|
776
|
+
|
|
777
|
+
#[getset(get_copy = "pub")]
|
|
778
|
+
is_lazy: bool,
|
|
757
779
|
}
|
|
758
780
|
|
|
759
781
|
impl PyImportDetails {
|
|
@@ -10,6 +10,7 @@ pub struct ImportedObject {
|
|
|
10
10
|
pub line_number: usize,
|
|
11
11
|
pub line_contents: String,
|
|
12
12
|
pub typechecking_only: bool,
|
|
13
|
+
pub is_lazy: bool,
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
impl ImportedObject {
|
|
@@ -18,12 +19,14 @@ impl ImportedObject {
|
|
|
18
19
|
line_number: usize,
|
|
19
20
|
line_contents: String,
|
|
20
21
|
typechecking_only: bool,
|
|
22
|
+
is_lazy: bool,
|
|
21
23
|
) -> Self {
|
|
22
24
|
Self {
|
|
23
25
|
name,
|
|
24
26
|
line_number,
|
|
25
27
|
line_contents,
|
|
26
28
|
typechecking_only,
|
|
29
|
+
is_lazy,
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
}
|
|
@@ -84,6 +87,7 @@ impl<'a> StatementVisitor<'a> for Visitor<'a> {
|
|
|
84
87
|
line_number.get(),
|
|
85
88
|
self.source_code.line_text(line_number).trim().to_string(),
|
|
86
89
|
self.typechecking_only,
|
|
90
|
+
import_stmt.is_lazy,
|
|
87
91
|
))
|
|
88
92
|
}
|
|
89
93
|
walk_stmt(self, stmt);
|
|
@@ -113,29 +117,24 @@ impl<'a> StatementVisitor<'a> for Visitor<'a> {
|
|
|
113
117
|
line_number.get(),
|
|
114
118
|
self.source_code.line_text(line_number).trim().to_string(),
|
|
115
119
|
self.typechecking_only,
|
|
120
|
+
import_from_stmt.is_lazy,
|
|
116
121
|
))
|
|
117
122
|
}
|
|
118
123
|
walk_stmt(self, stmt);
|
|
119
124
|
}
|
|
120
|
-
Stmt::If(if_stmt) =>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
Stmt::If(if_stmt) => {
|
|
126
|
+
// Only the branches guarded by `TYPE_CHECKING` are type checking only. The other
|
|
127
|
+
// branches (e.g. `else`) run when `TYPE_CHECKING` is false, that is, at runtime.
|
|
128
|
+
let outer_typechecking_only = self.typechecking_only;
|
|
129
|
+
self.typechecking_only = outer_typechecking_only || is_type_checking(&if_stmt.test);
|
|
130
|
+
self.visit_body(&if_stmt.body);
|
|
131
|
+
for clause in &if_stmt.elif_else_clauses {
|
|
132
|
+
self.typechecking_only = outer_typechecking_only
|
|
133
|
+
|| clause.test.as_ref().is_some_and(is_type_checking);
|
|
134
|
+
self.visit_elif_else_clause(clause);
|
|
129
135
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
walk_stmt(self, stmt);
|
|
133
|
-
self.typechecking_only = false;
|
|
134
|
-
}
|
|
135
|
-
_ => {
|
|
136
|
-
walk_stmt(self, stmt);
|
|
137
|
-
}
|
|
138
|
-
},
|
|
136
|
+
self.typechecking_only = outer_typechecking_only;
|
|
137
|
+
}
|
|
139
138
|
_ => {
|
|
140
139
|
walk_stmt(self, stmt);
|
|
141
140
|
}
|
|
@@ -143,6 +142,15 @@ impl<'a> StatementVisitor<'a> for Visitor<'a> {
|
|
|
143
142
|
}
|
|
144
143
|
}
|
|
145
144
|
|
|
145
|
+
/// Whether the expression is `TYPE_CHECKING` or `<something>.TYPE_CHECKING`.
|
|
146
|
+
fn is_type_checking(expr: &Expr) -> bool {
|
|
147
|
+
match expr {
|
|
148
|
+
Expr::Name(name) => name.id == "TYPE_CHECKING",
|
|
149
|
+
Expr::Attribute(attribute) => attribute.attr.id == "TYPE_CHECKING",
|
|
150
|
+
_ => false,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
146
154
|
#[cfg(test)]
|
|
147
155
|
mod tests {
|
|
148
156
|
use super::parse_imports_from_code;
|
|
@@ -16,6 +16,7 @@ pub struct DirectImport {
|
|
|
16
16
|
pub imported: String,
|
|
17
17
|
pub line_number: usize,
|
|
18
18
|
pub line_contents: String,
|
|
19
|
+
pub is_lazy: bool,
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
impl<'a, 'py> FromPyObject<'a, 'py> for DirectImport {
|
|
@@ -26,12 +27,14 @@ impl<'a, 'py> FromPyObject<'a, 'py> for DirectImport {
|
|
|
26
27
|
let imported: String = ob.getattr("imported")?.getattr("name")?.extract()?;
|
|
27
28
|
let line_number: usize = ob.getattr("line_number")?.extract()?;
|
|
28
29
|
let line_contents: String = ob.getattr("line_contents")?.extract()?;
|
|
30
|
+
let is_lazy: bool = ob.getattr("is_lazy")?.extract()?;
|
|
29
31
|
|
|
30
32
|
Ok(DirectImport {
|
|
31
33
|
importer,
|
|
32
34
|
imported,
|
|
33
35
|
line_number,
|
|
34
36
|
line_contents,
|
|
37
|
+
is_lazy,
|
|
35
38
|
})
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -127,7 +130,7 @@ fn scan_for_imports_no_py_single_module(
|
|
|
127
130
|
let found_package_for_module = found_packages_by_module[module];
|
|
128
131
|
let module_filename =
|
|
129
132
|
_determine_module_filename(module, found_package_for_module, file_system).unwrap();
|
|
130
|
-
let module_contents = file_system.read(&module_filename)
|
|
133
|
+
let module_contents = file_system.read(&module_filename)?;
|
|
131
134
|
let imported_objects =
|
|
132
135
|
import_parsing::parse_imports_from_code(&module_contents, &module_filename)?;
|
|
133
136
|
|
|
@@ -152,6 +155,7 @@ fn scan_for_imports_no_py_single_module(
|
|
|
152
155
|
imported: imported_module.name.to_string(),
|
|
153
156
|
line_number: imported_object.line_number,
|
|
154
157
|
line_contents: imported_object.line_contents,
|
|
158
|
+
is_lazy: imported_object.is_lazy,
|
|
155
159
|
});
|
|
156
160
|
}
|
|
157
161
|
None => {
|
|
@@ -165,6 +169,7 @@ fn scan_for_imports_no_py_single_module(
|
|
|
165
169
|
imported: imported_module,
|
|
166
170
|
line_number: imported_object.line_number,
|
|
167
171
|
line_contents: imported_object.line_contents,
|
|
172
|
+
is_lazy: imported_object.is_lazy,
|
|
168
173
|
});
|
|
169
174
|
}
|
|
170
175
|
}
|
|
@@ -196,6 +201,7 @@ fn to_py_direct_imports<'a>(
|
|
|
196
201
|
kwargs
|
|
197
202
|
.set_item("line_contents", &rust_import.line_contents)
|
|
198
203
|
.unwrap();
|
|
204
|
+
kwargs.set_item("is_lazy", rust_import.is_lazy).unwrap();
|
|
199
205
|
let py_direct_import = py_direct_import_class.call((), Some(&kwargs)).unwrap();
|
|
200
206
|
pyset.add(&py_direct_import).unwrap();
|
|
201
207
|
}
|
|
@@ -29,6 +29,7 @@ mod _rustgrimp {
|
|
|
29
29
|
|
|
30
30
|
#[pymodule_export]
|
|
31
31
|
use crate::exceptions::{
|
|
32
|
-
CorruptCache, InvalidModuleExpression, ModuleNotPresent,
|
|
32
|
+
CacheVersionMismatch, CorruptCache, InvalidModuleExpression, ModuleNotPresent,
|
|
33
|
+
NoSuchContainer, ParseError,
|
|
33
34
|
};
|
|
34
35
|
}
|
|
@@ -203,6 +203,12 @@ class Cache(AbstractCache):
|
|
|
203
203
|
except rust.CorruptCache:
|
|
204
204
|
logger.warning(f"Could not use corrupt cache file {data_cache_filename}.")
|
|
205
205
|
return {}
|
|
206
|
+
except rust.CacheVersionMismatch:
|
|
207
|
+
logger.info(
|
|
208
|
+
f"Cache file {data_cache_filename} was written by a different version of Grimp; "
|
|
209
|
+
"rebuilding."
|
|
210
|
+
)
|
|
211
|
+
return {}
|
|
206
212
|
|
|
207
213
|
logger.info(f"Used cache data file {data_cache_filename}.")
|
|
208
214
|
return imports_by_module
|
|
@@ -27,6 +27,7 @@ ImportTuple = tuple[str, str]
|
|
|
27
27
|
class DetailedImport(Import):
|
|
28
28
|
line_number: int
|
|
29
29
|
line_contents: str
|
|
30
|
+
is_lazy: bool
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class ImportGraph:
|
|
@@ -138,10 +139,15 @@ class ImportGraph:
|
|
|
138
139
|
imported: str,
|
|
139
140
|
line_number: int | None = None,
|
|
140
141
|
line_contents: str | None = None,
|
|
142
|
+
is_lazy: bool | None = None,
|
|
141
143
|
) -> None:
|
|
142
144
|
"""
|
|
143
145
|
Add a direct import between two modules to the graph. If the modules are not already
|
|
144
146
|
present, they will be added to the graph.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ValueError if only one of line_number and line_contents is supplied, or if
|
|
150
|
+
is_lazy is supplied without line_number and line_contents.
|
|
145
151
|
"""
|
|
146
152
|
self._cached_modules = None
|
|
147
153
|
self._rustgraph.add_import(
|
|
@@ -149,6 +155,7 @@ class ImportGraph:
|
|
|
149
155
|
imported=imported,
|
|
150
156
|
line_number=line_number,
|
|
151
157
|
line_contents=line_contents,
|
|
158
|
+
is_lazy=is_lazy,
|
|
152
159
|
)
|
|
153
160
|
|
|
154
161
|
def remove_import(self, *, importer: str, imported: str) -> None:
|
|
@@ -249,6 +256,7 @@ class ImportGraph:
|
|
|
249
256
|
'imported': 'mypackage.imported',
|
|
250
257
|
'line_number': 5,
|
|
251
258
|
'line_contents': 'from mypackage import imported',
|
|
259
|
+
'is_lazy': False,
|
|
252
260
|
},
|
|
253
261
|
(additional imports here)
|
|
254
262
|
]
|
|
@@ -55,9 +55,12 @@ class DirectImport:
|
|
|
55
55
|
imported: Module
|
|
56
56
|
line_number: int
|
|
57
57
|
line_contents: str
|
|
58
|
+
# Import in the form `lazy import` or `lazy from import`.
|
|
59
|
+
is_lazy: bool = False
|
|
58
60
|
|
|
59
61
|
def __str__(self) -> str:
|
|
60
|
-
|
|
62
|
+
lazy_label = ", lazy" if self.is_lazy else ""
|
|
63
|
+
return f"{self.importer} -> {self.imported} (l. {self.line_number}{lazy_label})"
|
|
61
64
|
|
|
62
65
|
|
|
63
66
|
@dataclass(frozen=True, order=True)
|
|
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
|