grimp 3.16__tar.gz → 3.17__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 (50) hide show
  1. {grimp-3.16 → grimp-3.17}/PKG-INFO +1 -1
  2. {grimp-3.16 → grimp-3.17}/pyproject.toml +1 -1
  3. {grimp-3.16 → grimp-3.17}/rust/src/caching.rs +40 -10
  4. {grimp-3.16 → grimp-3.17}/rust/src/errors.rs +6 -0
  5. {grimp-3.16 → grimp-3.17}/rust/src/exceptions.rs +1 -0
  6. {grimp-3.16 → grimp-3.17}/rust/src/graph/graph_manipulation.rs +2 -1
  7. {grimp-3.16 → grimp-3.17}/rust/src/graph/mod.rs +35 -13
  8. {grimp-3.16 → grimp-3.17}/rust/src/import_parsing.rs +5 -0
  9. {grimp-3.16 → grimp-3.17}/rust/src/import_scanning.rs +6 -0
  10. {grimp-3.16 → grimp-3.17}/rust/src/lib.rs +2 -1
  11. {grimp-3.16 → grimp-3.17}/src/grimp/__init__.py +1 -1
  12. {grimp-3.16 → grimp-3.17}/src/grimp/adaptors/caching.py +6 -0
  13. {grimp-3.16 → grimp-3.17}/src/grimp/application/graph.py +8 -0
  14. {grimp-3.16 → grimp-3.17}/src/grimp/application/usecases.py +1 -0
  15. {grimp-3.16 → grimp-3.17}/src/grimp/domain/valueobjects.py +4 -1
  16. {grimp-3.16 → grimp-3.17}/README.rst +0 -0
  17. {grimp-3.16 → grimp-3.17}/rust/Cargo.lock +0 -0
  18. {grimp-3.16 → grimp-3.17}/rust/Cargo.toml +0 -0
  19. {grimp-3.16 → grimp-3.17}/rust/build.rs +0 -0
  20. {grimp-3.16 → grimp-3.17}/rust/rust-toolchain.toml +0 -0
  21. {grimp-3.16 → grimp-3.17}/rust/src/filesystem.rs +0 -0
  22. {grimp-3.16 → grimp-3.17}/rust/src/graph/cycle_breakers.rs +0 -0
  23. {grimp-3.16 → grimp-3.17}/rust/src/graph/direct_import_queries.rs +0 -0
  24. {grimp-3.16 → grimp-3.17}/rust/src/graph/hierarchy_queries.rs +0 -0
  25. {grimp-3.16 → grimp-3.17}/rust/src/graph/higher_order_queries.rs +0 -0
  26. {grimp-3.16 → grimp-3.17}/rust/src/graph/import_chain_queries.rs +0 -0
  27. {grimp-3.16 → grimp-3.17}/rust/src/graph/pathfinding.rs +0 -0
  28. {grimp-3.16 → grimp-3.17}/rust/src/module_expressions.rs +0 -0
  29. {grimp-3.16 → grimp-3.17}/rust/src/module_finding.rs +0 -0
  30. {grimp-3.16 → grimp-3.17}/rust/tests/large.rs +0 -0
  31. {grimp-3.16 → grimp-3.17}/rust/tests/large_graph.json +0 -0
  32. {grimp-3.16 → grimp-3.17}/src/grimp/adaptors/__init__.py +0 -0
  33. {grimp-3.16 → grimp-3.17}/src/grimp/adaptors/filesystem.py +0 -0
  34. {grimp-3.16 → grimp-3.17}/src/grimp/adaptors/modulefinder.py +0 -0
  35. {grimp-3.16 → grimp-3.17}/src/grimp/adaptors/packagefinder.py +0 -0
  36. {grimp-3.16 → grimp-3.17}/src/grimp/adaptors/timing.py +0 -0
  37. {grimp-3.16 → grimp-3.17}/src/grimp/application/__init__.py +0 -0
  38. {grimp-3.16 → grimp-3.17}/src/grimp/application/config.py +0 -0
  39. {grimp-3.16 → grimp-3.17}/src/grimp/application/ports/__init__.py +0 -0
  40. {grimp-3.16 → grimp-3.17}/src/grimp/application/ports/caching.py +0 -0
  41. {grimp-3.16 → grimp-3.17}/src/grimp/application/ports/filesystem.py +0 -0
  42. {grimp-3.16 → grimp-3.17}/src/grimp/application/ports/modulefinder.py +0 -0
  43. {grimp-3.16 → grimp-3.17}/src/grimp/application/ports/packagefinder.py +0 -0
  44. {grimp-3.16 → grimp-3.17}/src/grimp/application/ports/timing.py +0 -0
  45. {grimp-3.16 → grimp-3.17}/src/grimp/application/scanning.py +0 -0
  46. {grimp-3.16 → grimp-3.17}/src/grimp/domain/__init__.py +0 -0
  47. {grimp-3.16 → grimp-3.17}/src/grimp/domain/analysis.py +0 -0
  48. {grimp-3.16 → grimp-3.17}/src/grimp/exceptions.py +0 -0
  49. {grimp-3.16 → grimp-3.17}/src/grimp/main.py +0 -0
  50. {grimp-3.16 → grimp-3.17}/src/grimp/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grimp
3
- Version: 3.16
3
+ Version: 3.17
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,7 @@ module-name = "grimp._rustgrimp"
8
8
 
9
9
  [project]
10
10
  name = "grimp"
11
- version = "3.16"
11
+ version = "3.17"
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 = [
@@ -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
- let raw_map: HashMap<&str, Vec<(&str, usize, &str)>> = imports_by_module
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::to_string(&raw_map).expect("Failed to serialize to 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
- let raw_map: HashMap<String, Vec<(String, usize, String)>> = serde_json::from_str(json_str)
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(|(imported, line_number, line_contents)| DirectImport {
117
- importer: module_name.clone(),
118
- imported,
119
- line_number,
120
- line_contents,
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
  }
@@ -36,6 +36,9 @@ 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),
39
42
  }
40
43
 
41
44
  pub type GrimpResult<T> = Result<T, GrimpError>;
@@ -55,6 +58,9 @@ impl From<GrimpError> for PyErr {
55
58
  line_number, text, ..
56
59
  } => PyErr::new::<exceptions::ParseError, _>((line_number, text)),
57
60
  GrimpError::CorruptCache(_) => exceptions::CorruptCache::new_err(value.to_string()),
61
+ GrimpError::CacheVersionMismatch(_) => {
62
+ exceptions::CacheVersionMismatch::new_err(value.to_string())
63
+ }
58
64
  }
59
65
  }
60
66
  }
@@ -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:
@@ -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
- let importer = self._graph.get_or_add_module(importer).token();
276
- let imported = self._graph.get_or_add_module(imported).token();
277
- match (line_number, line_contents) {
278
- (Some(line_number), Some(line_contents)) => {
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
- // TODO handle better.
287
- panic!("Expected line_number and line_contents, or neither.");
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,6 +117,7 @@ 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);
@@ -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
  }
@@ -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, NoSuchContainer, ParseError,
32
+ CacheVersionMismatch, CorruptCache, InvalidModuleExpression, ModuleNotPresent,
33
+ NoSuchContainer, ParseError,
33
34
  };
34
35
  }
@@ -1,4 +1,4 @@
1
- __version__ = "3.16"
1
+ __version__ = "3.17"
2
2
 
3
3
  from .application.graph import DetailedImport, Import, ImportGraph
4
4
  from .domain.analysis import PackageDependency, Route
@@ -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
  ]
@@ -184,6 +184,7 @@ def _assemble_graph(
184
184
  imported=direct_import.imported.name,
185
185
  line_number=direct_import.line_number,
186
186
  line_contents=direct_import.line_contents,
187
+ is_lazy=direct_import.is_lazy,
187
188
  )
188
189
  return graph
189
190
 
@@ -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
- return f"{self.importer} -> {self.imported} (l. {self.line_number})"
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