litellm/litellm-rust/crates/python-compat/src/value.rs
devin-ai-integration[bot] 05d7fb24bd
feat(rust): add python-compat crate for Python data formats (#42510)
* feat(rust): add python-compat crate for Python data formats

Add litellm-python-compat, a PyO3-free crate that reproduces the Python
data formats LiteLLM persists, so Rust readers and writers can interoperate
with state written by the Python proxy:

- literal::literal_eval: a linear recursive-descent port of
  ast.literal_eval (prefixes, escapes, implicit concatenation, numeric
  underscores and radixes, single unary sign, real +/- complex with 3.14
  mixed-mode rules, set(), Python-equality key dedup)
- repr::{repr, to_str}: byte-exact repr()/str(), with a printable table
  generated from CPython's str.isprintable (Unicode 16.0.0)
- json::{dumps, from_json, to_json}: json.dumps defaults and the
  json.loads mapping
- pickle::{loads, dumps}: plain-data pickles via serde-pickle's serde
  interface, which keeps dict insertion order
- truthy::truthy: bool() for plain data

Tests replay fixtures generated by CPython 3.14 (values across every
format and pickle protocol 0-5, plus 154 literal_eval source texts).
Accepted divergences are pinned in a KNOWN table that fails once one
starts matching. A criterion bench covers each format and literal_eval
cost by nesting depth, guarding the linear parse: the py_literal grammar
doubled per nested bracket (105 ms at 16 nested dicts; 19 us at 128 now).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(rust): split python-compat modules and harden the pickle verifier

- Disable class resolution in scripts/verify_rust_pickles.py, and truncate
  the export file once instead of removing and appending to it, so the
  verifier cannot be pointed at a pre-created file whose rows execute code
  through pickle.loads
- Move Error to error.rs and Value to value.rs, leaving lib.rs as the crate
  overview, module list and MAX_DEPTH
- Move the generator and verifier to scripts/, beside the Unicode table
  generator, leaving tests/ to the Rust tests
- Group the bench by measured surface, give every case a Throughput so
  criterion reports bytes per second, and document baseline comparison

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Yujong Lee <yujong@berri.ai>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:16:56 -07:00

53 lines
1.3 KiB
Rust

use num_bigint::BigInt;
/// A Python value built only from literals: what `ast.literal_eval` can return.
#[derive(Clone, Debug, PartialEq)]
pub enum Value {
None,
Bool(bool),
Int(BigInt),
Float(f64),
Complex {
re: f64,
im: f64,
},
Str(String),
Bytes(Vec<u8>),
Tuple(Vec<Value>),
List(Vec<Value>),
/// Insertion-ordered, with Python's key equality already applied.
Dict(Vec<(Value, Value)>),
/// Literal order, with Python's member equality already applied.
Set(Vec<Value>),
}
impl Value {
/// Python's type name, as it appears in `TypeError` messages.
pub fn type_name(&self) -> &'static str {
match self {
Value::None => "NoneType",
Value::Bool(_) => "bool",
Value::Int(_) => "int",
Value::Float(_) => "float",
Value::Complex { .. } => "complex",
Value::Str(_) => "str",
Value::Bytes(_) => "bytes",
Value::Tuple(_) => "tuple",
Value::List(_) => "list",
Value::Dict(_) => "dict",
Value::Set(_) => "set",
}
}
}
impl From<i64> for Value {
fn from(value: i64) -> Self {
Value::Int(value.into())
}
}
impl From<&str> for Value {
fn from(value: &str) -> Self {
Value::Str(value.to_owned())
}
}