Skip to content

Commit 0c28293

Browse files
committed
Implement error handling in build_graph
1 parent 16b59b2 commit 0c28293

File tree

4 files changed

+106
-41
lines changed

4 files changed

+106
-41
lines changed

rust/src/errors.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::exceptions;
22
use pyo3::PyErr;
3-
use pyo3::exceptions::PyValueError;
3+
use pyo3::exceptions::{PyFileNotFoundError, PyIOError, PyValueError};
44
use ruff_python_parser::ParseError as RuffParseError;
55
use thiserror::Error;
66

@@ -36,6 +36,18 @@ pub enum GrimpError {
3636

3737
#[error("Could not use corrupt cache file {0}.")]
3838
CorruptCache(String),
39+
40+
#[error("Failed to read file {path}: {error}")]
41+
FileReadError { path: String, error: String },
42+
43+
#[error("Failed to get file metadata for {path}: {error}")]
44+
FileMetadataError { path: String, error: String },
45+
46+
#[error("Failed to write cache file {path}: {error}")]
47+
CacheWriteError { path: String, error: String },
48+
49+
#[error("Package directory does not exist: {0}")]
50+
PackageDirectoryNotFound(String),
3951
}
4052

4153
pub type GrimpResult<T> = Result<T, GrimpError>;
@@ -55,6 +67,12 @@ impl From<GrimpError> for PyErr {
5567
line_number, text, ..
5668
} => PyErr::new::<exceptions::ParseError, _>((line_number, text)),
5769
GrimpError::CorruptCache(_) => exceptions::CorruptCache::new_err(value.to_string()),
70+
GrimpError::FileReadError { .. } => PyIOError::new_err(value.to_string()),
71+
GrimpError::FileMetadataError { .. } => PyIOError::new_err(value.to_string()),
72+
GrimpError::CacheWriteError { .. } => PyIOError::new_err(value.to_string()),
73+
GrimpError::PackageDirectoryNotFound(_) => {
74+
PyFileNotFoundError::new_err(value.to_string())
75+
}
5876
}
5977
}
6078
}

rust/src/graph/builder/cache.rs

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
55

66
use bincode::{Decode, Encode};
77

8+
use crate::errors::{GrimpError, GrimpResult};
89
use crate::import_parsing::ImportedObject;
910

1011
#[derive(Debug, Clone, Encode, Decode)]
@@ -48,20 +49,31 @@ pub fn load_cache(cache_dir: &Path, package_name: &str) -> ImportCache {
4849
HashMap::new()
4950
}
5051

51-
pub fn save_cache(cache: &ImportCache, cache_dir: &Path, package_name: &str) {
52-
if let Err(e) = fs::create_dir_all(cache_dir) {
53-
eprintln!("Failed to create cache directory: {}", e);
54-
return;
55-
}
52+
pub fn save_cache(cache: &ImportCache, cache_dir: &Path, package_name: &str) -> GrimpResult<()> {
53+
fs::create_dir_all(cache_dir).map_err(|e| GrimpError::CacheWriteError {
54+
path: cache_dir.display().to_string(),
55+
error: e.to_string(),
56+
})?;
5657

5758
let cache_file = cache_dir.join(format!("{}.imports.bincode", package_name));
5859

59-
match bincode::encode_to_vec(cache, bincode::config::standard()) {
60-
Ok(encoded) => {
61-
if let Ok(mut file) = fs::File::create(&cache_file) {
62-
let _ = file.write_all(&encoded);
63-
}
60+
let encoded = bincode::encode_to_vec(cache, bincode::config::standard()).map_err(|e| {
61+
GrimpError::CacheWriteError {
62+
path: cache_file.display().to_string(),
63+
error: e.to_string(),
6464
}
65-
Err(e) => eprintln!("Failed to encode cache: {}", e),
66-
}
65+
})?;
66+
67+
let mut file = fs::File::create(&cache_file).map_err(|e| GrimpError::CacheWriteError {
68+
path: cache_file.display().to_string(),
69+
error: e.to_string(),
70+
})?;
71+
72+
file.write_all(&encoded)
73+
.map_err(|e| GrimpError::CacheWriteError {
74+
path: cache_file.display().to_string(),
75+
error: e.to_string(),
76+
})?;
77+
78+
Ok(())
6779
}

rust/src/graph/builder/mod.rs

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crossbeam::channel;
88
use derive_new::new;
99
use ignore::WalkBuilder;
1010

11+
use crate::errors::{GrimpError, GrimpResult};
1112
use crate::graph::Graph;
1213
use crate::import_parsing::{ImportedObject, parse_imports_from_code};
1314

@@ -31,22 +32,31 @@ pub fn build_graph(
3132
include_external_packages: bool,
3233
exclude_type_checking_imports: bool,
3334
cache_dir: Option<&PathBuf>,
34-
) -> Graph {
35+
) -> GrimpResult<Graph> {
36+
// Check if package directory exists
37+
if !package.directory.exists() {
38+
return Err(GrimpError::PackageDirectoryNotFound(
39+
package.directory.display().to_string(),
40+
));
41+
}
42+
3543
// Load cache if available
3644
let mut cache = cache_dir
3745
.map(|dir| load_cache(dir, &package.name))
3846
.unwrap_or_default();
3947

4048
// Create channels for communication
41-
let (module_discovery_sender, module_discovery_receiver) = channel::bounded(10000);
42-
let (import_parser_sender, import_parser_receiver) = channel::bounded(10000);
49+
// This way we can start parsing moduels while we're still discovering them.
50+
let (found_module_sender, found_module_receiver) = channel::bounded(10000);
51+
let (parsed_module_sender, parser_module_receiver) = channel::bounded(10000);
52+
let (error_sender, error_receiver) = channel::bounded(1);
4353

4454
let mut thread_handles = Vec::new();
4555

4656
// Thread 1: Discover modules
4757
let package_clone = package.clone();
4858
let handle = thread::spawn(move || {
49-
discover_python_modules(&package_clone, module_discovery_sender);
59+
discover_python_modules(&package_clone, found_module_sender);
5060
});
5161
thread_handles.push(handle);
5262

@@ -55,32 +65,47 @@ pub fn build_graph(
5565
.map(|n| max(n.get() / 2, 1))
5666
.unwrap_or(4);
5767
for _ in 0..num_workers {
58-
let receiver = module_discovery_receiver.clone();
59-
let sender = import_parser_sender.clone();
68+
let receiver = found_module_receiver.clone();
69+
let sender = parsed_module_sender.clone();
70+
let error_sender = error_sender.clone();
6071
let cache = cache.clone();
6172
let handle = thread::spawn(move || {
6273
while let Ok(module) = receiver.recv() {
63-
if let Some(parsed) = parse_module_imports(&module, &cache) {
64-
sender.send(parsed).unwrap();
74+
match parse_module_imports(&module, &cache) {
75+
Ok(parsed) => {
76+
sender.send(parsed).unwrap();
77+
}
78+
Err(e) => {
79+
// Channel has capacity of 1, since we only care to catch one error.
80+
// Drop further errors.
81+
let _ = error_sender.try_send(e);
82+
}
6583
}
6684
}
6785
});
6886
thread_handles.push(handle);
6987
}
70-
drop(module_discovery_receiver); // Close original receiver
71-
drop(import_parser_sender); // Close original sender
72-
73-
// Collect parsed modules
74-
let mut parsed_modules = Vec::new();
75-
while let Ok(parsed) = import_parser_receiver.recv() {
76-
parsed_modules.push(parsed);
77-
}
7888

89+
// Close original found_module_receiver, so that the parser threads can exit.
90+
drop(found_module_receiver);
7991
// Wait for all threads to complete
8092
for handle in thread_handles {
8193
handle.join().unwrap();
8294
}
8395

96+
// Check if any errors occurred
97+
if let Ok(error) = error_receiver.try_recv() {
98+
return Err(error);
99+
}
100+
101+
// Collect parsed modules
102+
let mut parsed_modules = Vec::new();
103+
// Close original parsed_module_sender, so that parser_module_receiver iteration terminates.
104+
drop(parsed_module_sender);
105+
while let Ok(parsed) = parser_module_receiver.recv() {
106+
parsed_modules.push(parsed);
107+
}
108+
84109
// Update and save cache if cache_dir is set
85110
if let Some(cache_dir) = cache_dir {
86111
for parsed in &parsed_modules {
@@ -89,17 +114,17 @@ pub fn build_graph(
89114
CachedImports::new(parsed.module.mtime_secs, parsed.imported_objects.clone()),
90115
);
91116
}
92-
save_cache(&cache, cache_dir, &package.name);
117+
save_cache(&cache, cache_dir, &package.name)?;
93118
}
94119

95-
// Resolve imports and assemble graph (sequential)
120+
// Resolve imports and assemble graph
96121
let imports_by_module = resolve_imports(
97122
&parsed_modules,
98123
include_external_packages,
99124
exclude_type_checking_imports,
100125
);
101126

102-
assemble_graph(&imports_by_module, &package.name)
127+
Ok(assemble_graph(&imports_by_module, &package.name))
103128
}
104129

105130
#[derive(Debug, Clone)]
@@ -154,6 +179,12 @@ fn discover_python_modules(package: &PackageSpec, sender: channel::Sender<FoundM
154179
};
155180

156181
let path = entry.path();
182+
183+
// Only process files, not directories
184+
if !path.is_file() {
185+
return WalkState::Continue;
186+
}
187+
157188
if let Some(module_name) = path_to_module_name(path, &package) {
158189
let is_package = is_package(path);
159190

@@ -181,23 +212,27 @@ fn discover_python_modules(package: &PackageSpec, sender: channel::Sender<FoundM
181212
});
182213
}
183214

184-
fn parse_module_imports(module: &FoundModule, cache: &ImportCache) -> Option<ParsedModule> {
215+
fn parse_module_imports(module: &FoundModule, cache: &ImportCache) -> GrimpResult<ParsedModule> {
185216
// Check if we have a cached version with matching mtime
186217
if let Some(cached) = cache.get(&module.path)
187218
&& module.mtime_secs == cached.mtime_secs()
188219
{
189220
// Cache hit - use cached imports
190-
return Some(ParsedModule {
221+
return Ok(ParsedModule {
191222
module: module.clone(),
192223
imported_objects: cached.imported_objects().to_vec(),
193224
});
194225
}
195226

196227
// Cache miss or file modified - parse the file
197-
let code = fs::read_to_string(&module.path).ok()?;
198-
let imported_objects =
199-
parse_imports_from_code(&code, module.path.to_str().unwrap_or("")).ok()?;
200-
Some(ParsedModule {
228+
let code = fs::read_to_string(&module.path).map_err(|e| GrimpError::FileReadError {
229+
path: module.path.display().to_string(),
230+
error: e.to_string(),
231+
})?;
232+
233+
let imported_objects = parse_imports_from_code(&code, module.path.to_str().unwrap_or(""))?;
234+
235+
Ok(ParsedModule {
201236
module: module.clone(),
202237
imported_objects,
203238
})

rust/src/graph_building.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ pub fn build_graph_rust(
2727
include_external_packages: bool,
2828
exclude_type_checking_imports: bool,
2929
cache_dir: Option<String>,
30-
) -> GraphWrapper {
30+
) -> PyResult<GraphWrapper> {
3131
let cache_path = cache_dir.map(PathBuf::from);
3232
let graph = build_graph(
3333
&package.inner,
3434
include_external_packages,
3535
exclude_type_checking_imports,
3636
cache_path.as_ref(),
37-
);
38-
GraphWrapper::from_graph(graph)
37+
)?;
38+
Ok(GraphWrapper::from_graph(graph))
3939
}

0 commit comments

Comments
 (0)