fix(rust): preserve nested optional import failures (#43265)

* fix(rust): preserve nested optional import failures

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(rust): restore Python modules after settings tests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Yujong Lee <yujong@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-09-25 19:41:53 -07:00 • committed by GitHub
parent 2530255624
commit e1a9378093
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -45,8 +45,13 @@ impl PythonSettings {
pub(crate) fn read_or_unset(self, py: Python<'_>) -> PyResult<Option<Snapshot<'_>>> {
match self.read(py) {
Ok(snapshot) => Ok(Some(snapshot)),
Err(error) if error.is_instance_of::<PyModuleNotFoundError>(py) => Ok(None),
Err(error) => Err(error),
Err(error) => {
if missing_module(py, &error, "litellm")? {
Ok(None)
} else {
Err(error)
}
}
}
}
@ -56,9 +61,24 @@ impl PythonSettings {
}
}
fn missing_module(py: Python<'_>, error: &PyErr, expected: &str) -> PyResult<bool> {
if !error.is_instance_of::<PyModuleNotFoundError>(py) {
return Ok(false);
}
Ok(error
.value(py)
.getattr("name")?
.extract::<Option<String>>()?
.is_some_and(|name| name == expected))
}
#[cfg(test)]
mod tests {
use pyo3::{exceptions::PyRuntimeError, prelude::*, types::PyDict};
use pyo3::{
exceptions::{PyImportError, PyModuleNotFoundError, PyRuntimeError},
prelude::*,
types::PyDict,
};
use super::PythonSettings;
use crate::coercion::FieldSpec;
@ -150,4 +170,153 @@ values = (Descriptor(), SimpleNamespace(flag=Truth()))
);
});
}
#[test]
fn read_or_unset_returns_none_when_litellm_is_missing() {
Python::initialize();
Python::attach(|py| {
let locals = PyDict::new(py);
py.run(
c"
import sys
class MissingLitellm:
def find_spec(self, fullname, path=None, target=None):
if fullname == 'litellm':
raise ModuleNotFoundError('No module named litellm', name='litellm')
finder = MissingLitellm()
previous_litellm = sys.modules.get('litellm')
had_litellm = 'litellm' in sys.modules
sys.meta_path.insert(0, finder)
sys.modules.pop('litellm', None)
",
Some(&locals),
Some(&locals),
)
.unwrap();
let result = PythonSettings::Http.read_or_unset(py);
assert!(result.unwrap().is_none());
py.run(
c"
sys.meta_path.remove(finder)
if had_litellm:
sys.modules['litellm'] = previous_litellm
else:
sys.modules.pop('litellm', None)
",
Some(&locals),
Some(&locals),
)
.unwrap();
});
}
#[test]
fn read_or_unset_propagates_nested_module_not_found_errors() {
Python::initialize();
Python::attach(|py| {
let locals = PyDict::new(py);
py.run(
c"
import sys
import types
previous_modules = {
name: sys.modules[name]
for name in ('litellm', 'litellm.rust_bridge', 'litellm.rust_bridge.settings')
if name in sys.modules
}
litellm = types.ModuleType('litellm')
litellm.__path__ = []
rust_bridge = types.ModuleType('litellm.rust_bridge')
rust_bridge.__path__ = []
settings = types.ModuleType('litellm.rust_bridge.settings')
def http_settings():
raise ModuleNotFoundError('No module named certifi', name='certifi')
settings.http_settings = http_settings
litellm.rust_bridge = rust_bridge
rust_bridge.settings = settings
sys.modules['litellm'] = litellm
sys.modules['litellm.rust_bridge'] = rust_bridge
sys.modules['litellm.rust_bridge.settings'] = settings
",
Some(&locals),
Some(&locals),
)
.unwrap();
let error = match PythonSettings::Http.read_or_unset(py) {
Ok(_) => panic!("nested module errors must propagate"),
Err(error) => error,
};
assert!(error.is_instance_of::<PyModuleNotFoundError>(py));
assert_eq!(
error
.value(py)
.getattr("name")
.unwrap()
.extract::<String>()
.unwrap(),
"certifi"
);
py.run(
c"
for name in ('litellm.rust_bridge.settings', 'litellm.rust_bridge', 'litellm'):
sys.modules.pop(name, None)
sys.modules.update(previous_modules)
",
Some(&locals),
Some(&locals),
)
.unwrap();
});
}
#[test]
fn read_or_unset_propagates_import_errors() {
Python::initialize();
Python::attach(|py| {
let locals = PyDict::new(py);
py.run(
c"
import sys
import types
previous_modules = {
name: sys.modules[name]
for name in ('litellm', 'litellm.rust_bridge', 'litellm.rust_bridge.settings')
if name in sys.modules
}
litellm = types.ModuleType('litellm')
litellm.__path__ = []
rust_bridge = types.ModuleType('litellm.rust_bridge')
rust_bridge.__path__ = []
settings = types.ModuleType('litellm.rust_bridge.settings')
def http_settings():
raise ImportError('cannot import name setting')
settings.http_settings = http_settings
litellm.rust_bridge = rust_bridge
rust_bridge.settings = settings
sys.modules['litellm'] = litellm
sys.modules['litellm.rust_bridge'] = rust_bridge
sys.modules['litellm.rust_bridge.settings'] = settings
",
Some(&locals),
Some(&locals),
)
.unwrap();
let error = match PythonSettings::Http.read_or_unset(py) {
Ok(_) => panic!("import errors must propagate"),
Err(error) => error,
};
assert!(error.is_instance_of::<PyImportError>(py));
assert_eq!(error.to_string(), "ImportError: cannot import name setting");
py.run(
c"
for name in ('litellm.rust_bridge.settings', 'litellm.rust_bridge', 'litellm'):
sys.modules.pop(name, None)
sys.modules.update(previous_modules)
",
Some(&locals),
Some(&locals),
)
.unwrap();
});
}
}