Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 172 additions & 3 deletions litellm-rust/crates/python-bridge/src/python_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}

Expand All @@ -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;
Expand Down Expand Up @@ -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)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
",
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();
});
}
}
Loading