From bb5da36826918ed4ec2d302f9e6720c6d43228d7 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Sat, 26 Sep 2026 01:40:30 +0000 Subject: [PATCH 1/2] fix(rust): preserve nested optional import failures Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../python-bridge/src/python_settings.rs | 156 +++++++++++++++++- 1 file changed, 153 insertions(+), 3 deletions(-) diff --git a/litellm-rust/crates/python-bridge/src/python_settings.rs b/litellm-rust/crates/python-bridge/src/python_settings.rs index f03e5fdce7f1..00ab516e5ca2 100644 --- a/litellm-rust/crates/python-bridge/src/python_settings.rs +++ b/litellm-rust/crates/python-bridge/src/python_settings.rs @@ -45,8 +45,13 @@ impl PythonSettings { pub(crate) fn read_or_unset(self, py: Python<'_>) -> PyResult>> { match self.read(py) { Ok(snapshot) => Ok(Some(snapshot)), - Err(error) if error.is_instance_of::(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 { + if !error.is_instance_of::(py) { + return Ok(false); + } + Ok(error + .value(py) + .getattr("name")? + .extract::>()? + .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,134 @@ 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') +sys.meta_path.insert(0, MissingLitellm()) +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.pop(0) +", + 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 +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::(py)); + assert_eq!( + error + .value(py) + .getattr("name") + .unwrap() + .extract::() + .unwrap(), + "certifi" + ); + py.run( + c" +for name in ('litellm.rust_bridge.settings', 'litellm.rust_bridge', 'litellm'): + sys.modules.pop(name, None) +", + 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 +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::(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) +", + Some(&locals), + Some(&locals), + ) + .unwrap(); + }); + } } From aaea74418dd6a8436000e4650e36981f22dc161a Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Sat, 26 Sep 2026 01:48:36 +0000 Subject: [PATCH 2/2] test(rust): restore Python modules after settings tests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../python-bridge/src/python_settings.rs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/litellm-rust/crates/python-bridge/src/python_settings.rs b/litellm-rust/crates/python-bridge/src/python_settings.rs index 00ab516e5ca2..6f8388471dc1 100644 --- a/litellm-rust/crates/python-bridge/src/python_settings.rs +++ b/litellm-rust/crates/python-bridge/src/python_settings.rs @@ -183,7 +183,10 @@ class MissingLitellm: def find_spec(self, fullname, path=None, target=None): if fullname == 'litellm': raise ModuleNotFoundError('No module named litellm', name='litellm') -sys.meta_path.insert(0, MissingLitellm()) +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), @@ -194,7 +197,11 @@ sys.modules.pop('litellm', None) assert!(result.unwrap().is_none()); py.run( c" -sys.meta_path.pop(0) +sys.meta_path.remove(finder) +if had_litellm: + sys.modules['litellm'] = previous_litellm +else: + sys.modules.pop('litellm', None) ", Some(&locals), Some(&locals), @@ -212,6 +219,11 @@ sys.meta_path.pop(0) 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') @@ -248,6 +260,7 @@ sys.modules['litellm.rust_bridge.settings'] = settings 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), @@ -265,6 +278,11 @@ for name in ('litellm.rust_bridge.settings', 'litellm.rust_bridge', 'litellm'): 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') @@ -293,6 +311,7 @@ sys.modules['litellm.rust_bridge.settings'] = settings 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),