grimp 3.17__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.17 → grimp-3.18}/PKG-INFO +1 -1
- {grimp-3.17 → grimp-3.18}/pyproject.toml +1 -1
- {grimp-3.17 → grimp-3.18}/rust/src/errors.rs +10 -1
- {grimp-3.17 → grimp-3.18}/rust/src/filesystem.rs +71 -22
- {grimp-3.17 → grimp-3.18}/rust/src/import_parsing.rs +21 -18
- {grimp-3.17 → grimp-3.18}/rust/src/import_scanning.rs +1 -1
- {grimp-3.17 → grimp-3.18}/src/grimp/__init__.py +1 -1
- {grimp-3.17 → grimp-3.18}/README.rst +0 -0
- {grimp-3.17 → grimp-3.18}/rust/Cargo.lock +0 -0
- {grimp-3.17 → grimp-3.18}/rust/Cargo.toml +0 -0
- {grimp-3.17 → grimp-3.18}/rust/build.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/rust-toolchain.toml +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/caching.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/exceptions.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/graph/cycle_breakers.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/graph/direct_import_queries.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/graph/graph_manipulation.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/graph/hierarchy_queries.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/graph/higher_order_queries.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/graph/import_chain_queries.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/graph/mod.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/graph/pathfinding.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/lib.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/module_expressions.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/src/module_finding.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/tests/large.rs +0 -0
- {grimp-3.17 → grimp-3.18}/rust/tests/large_graph.json +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/adaptors/__init__.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/adaptors/caching.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/adaptors/filesystem.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/adaptors/modulefinder.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/adaptors/packagefinder.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/adaptors/timing.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/application/__init__.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/application/config.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/application/graph.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/application/ports/__init__.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/application/ports/caching.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/application/ports/filesystem.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/application/ports/modulefinder.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/application/ports/packagefinder.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/application/ports/timing.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/application/scanning.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/application/usecases.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/domain/__init__.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/domain/analysis.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/domain/valueobjects.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/exceptions.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/main.py +0 -0
- {grimp-3.17 → grimp-3.18}/src/grimp/py.typed +0 -0
|
@@ -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
|
|
|
@@ -39,6 +39,12 @@ pub enum GrimpError {
|
|
|
39
39
|
|
|
40
40
|
#[error("Cache file {0} was written by a different version of Grimp.")]
|
|
41
41
|
CacheVersionMismatch(String),
|
|
42
|
+
|
|
43
|
+
#[error("{0}")]
|
|
44
|
+
FileNotFound(String),
|
|
45
|
+
|
|
46
|
+
#[error("{0}")]
|
|
47
|
+
UndecodableFile(String),
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
pub type GrimpResult<T> = Result<T, GrimpError>;
|
|
@@ -61,6 +67,9 @@ impl From<GrimpError> for PyErr {
|
|
|
61
67
|
GrimpError::CacheVersionMismatch(_) => {
|
|
62
68
|
exceptions::CacheVersionMismatch::new_err(value.to_string())
|
|
63
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()),
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
}
|
|
@@ -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<()> {
|
|
@@ -122,25 +122,19 @@ impl<'a> StatementVisitor<'a> for Visitor<'a> {
|
|
|
122
122
|
}
|
|
123
123
|
walk_stmt(self, stmt);
|
|
124
124
|
}
|
|
125
|
-
Stmt::If(if_stmt) =>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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);
|
|
134
135
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
walk_stmt(self, stmt);
|
|
138
|
-
self.typechecking_only = false;
|
|
139
|
-
}
|
|
140
|
-
_ => {
|
|
141
|
-
walk_stmt(self, stmt);
|
|
142
|
-
}
|
|
143
|
-
},
|
|
136
|
+
self.typechecking_only = outer_typechecking_only;
|
|
137
|
+
}
|
|
144
138
|
_ => {
|
|
145
139
|
walk_stmt(self, stmt);
|
|
146
140
|
}
|
|
@@ -148,6 +142,15 @@ impl<'a> StatementVisitor<'a> for Visitor<'a> {
|
|
|
148
142
|
}
|
|
149
143
|
}
|
|
150
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
|
+
|
|
151
154
|
#[cfg(test)]
|
|
152
155
|
mod tests {
|
|
153
156
|
use super::parse_imports_from_code;
|
|
@@ -130,7 +130,7 @@ fn scan_for_imports_no_py_single_module(
|
|
|
130
130
|
let found_package_for_module = found_packages_by_module[module];
|
|
131
131
|
let module_filename =
|
|
132
132
|
_determine_module_filename(module, found_package_for_module, file_system).unwrap();
|
|
133
|
-
let module_contents = file_system.read(&module_filename)
|
|
133
|
+
let module_contents = file_system.read(&module_filename)?;
|
|
134
134
|
let imported_objects =
|
|
135
135
|
import_parsing::parse_imports_from_code(&module_contents, &module_filename)?;
|
|
136
136
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|