litellm/litellm-rust/crates/python-compat/scripts/generate_fixtures.py
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

352 lines
7.8 KiB
Python

"""Regenerate generated/values.json: what CPython produces for each value in CORPUS.
python scripts/generate_fixtures.py > generated/values.json
Each row records `repr`, `str`, `json.dumps` (or its error), `bool`, and `pickle.dumps` at
every protocol. `literal` says whether `ast.literal_eval(repr(value))` gives the value back,
which is how Python reads `str(dict)` text back from a cache; the Rust tests reach the other
rows only through pickle. `view` is `repr` of the value as `pickle::loads` decodes it, with
tuples, sets, and frozensets rendered as lists. `sources` records `ast.literal_eval` on raw
source texts: its result, or the exception it raises.
"""
import ast
import json
import pickle
import sys
import warnings
# Entries are source texts, or `(name, source)` when the source is too long to read in a
# test report. `name` is what the Rust `KNOWN` table keys on.
CORPUS = [
# Scalars
"None",
"True",
"False",
"0",
"-7",
"2**63 - 1",
"-(2**63)",
"2**64",
"-(2**70)",
# Floats around CPython's repr thresholds
"0.0",
"-0.0",
"0.2",
"1.0",
"-1.5",
"0.1 + 0.2",
"123456789.123",
"1e15",
"1e16",
"1.5e16",
"9999999999999998.0",
"0.0001",
"1e-05",
"1.25e-07",
"5e-324",
"1.7976931348623157e308",
"1e22",
"float('inf')",
"float('-inf')",
"float('nan')",
# Complex
"1j",
"-1j",
"complex(0, -1)",
"1+2j",
"-1.5-0.5j",
"complex(0.0, 1e16)",
# Strings: quote selection, escapes, printable and non-printable non-ASCII
"''",
"'plain'",
'"it\'s"',
"'say \"hi\"'",
"'both \\' and \"'",
"'back\\\\slash'",
"'\\t\\n\\r'",
"'\\x00\\x1f\\x7f'",
"'\\x85\\xa0\\xad'",
"'caf\\xe9'",
"'\\u65e5\\u672c'",
"'\\u200b\\u2028\\u3000'",
"'\\U0001f600'",
"'\\U000e0001\\U0010ffff'",
"'\\b\\f'",
# Bytes
"b''",
"b'abc'",
'b"a\'b"',
"b'a\"b\\'c'",
"b'\\x00\\t\\n\\r\\x7f\\x80\\xff'",
# Containers
"[]",
"[1, 'a', None, True]",
"()",
"(1,)",
"(1, (2, 3))",
"{}",
"{'a': 1, 'b': [1.0, 2.5]}",
"{'z': 1, 'a': 2, 'm': 3}",
"{1: 'int', 2.5: 'float', True: 'bool', None: 'none'}",
"{(1, 2): 'tuple key'}",
"{'nested': {'deeper': {'deepest': [{}]}}}",
"{1}",
"set()",
"frozenset({1})",
"[[[[[[[[[[1]]]]]]]]]]",
# Deeper than the Rust decoders allow: CPython's parser accepts ~200 nested brackets
# and its unpickler has no limit, so this row records a deliberate divergence.
("nested_150", "[" * 150 + "1" + "]" * 150),
# The shape LiteLLM caches
'{\'timestamp\': 1726000000.123, \'response\': \'{"id": "chatcmpl-1", "object": "chat.completion"}\'}',
"{'model': 'gpt-4o', 'messages': [{'role': 'user', 'content': 'hi'}], 'temperature': 0.2, 'stream': False}",
]
# Source texts for `literal_eval` itself: tokenizer and evaluator edge cases, recorded with
# CPython's result or the exception it raises. Raw strings keep backslashes literal.
SOURCES = [
# Layout: leading/trailing whitespace, comments, newlines, continuations
"1",
" 1",
"\t1",
"\n1",
" \n 1",
"1\n",
"1 # comment",
"# c\n1",
"1 \\\n",
"1,",
"1, 2",
"1,\n2",
"(1,\n2)",
"[1,\n 2,\n]",
# Containers and grouping
"()",
"(1)",
"((1,))",
"(,)",
"[,]",
"{,}",
"[1,]",
"{'a': 1,}",
"{1,}",
"{'a': 1 'b': 2}",
"{1: 'a', True: 'b'}",
"{1, True, 1.0}",
"{(1, 2): 'x', (1.0, 2): 'y'}",
"{[1]: 2}",
"{{1}}",
"{(1, [2])}",
"set()",
"set( )",
"set([1])",
"frozenset()",
# Names
"True",
"False",
"None",
"Truex",
"true",
"...",
# Integers and floats
"0",
"00",
"0_0",
"01",
"007",
"1_000",
"1_",
"1__0",
"_1",
"0x1F",
"0X_1f",
"0o17",
"0b101",
"0b102",
"0x",
"1e3",
"1E-3",
"1e",
"1.e5",
".5",
"5.",
"1..",
"1.5.2",
"1_0.0_1e1_0",
"1e999",
"-1e999",
"1j",
"1.5J",
"010j",
"010.5",
"1a",
"0x1g",
# Signs and complex sums
"-1",
"+1",
"- 1",
"--1",
"-+1",
"-(1)",
"-(-1)",
"-(1+2j)",
"-True",
"-'a'",
"-[1]",
"1+2j",
"1-2j",
"1 + 2j",
"(1)+(2j)",
"1+2",
"1+2+3j",
"1+-2j",
"2j+1",
"True+1j",
"1-0j",
"0.0-0j",
"-0.0+1j",
"-0.0",
"-0",
"(-0.0)",
"[-0.0, (-0.0)]",
"2**3",
"1*2",
"1 if 1 else 2",
"(1,)(2)",
# String prefixes, quoting, and concatenation
"''",
'""',
"'a' 'b'",
"'a' \"b\" '''c'''",
"'a' b'b'",
"b'a' b'b'",
"u'x'",
"U'x'",
"r'x'",
"R'x'",
"b'x'",
"B'x'",
"br'x'",
"Rb'x'",
"rB'x'",
"ur'x'",
"bu'x'",
"f'x'",
"rf'x'",
"'''a\nb'''",
'"""a\\""""',
"'a\nb'",
"'a\\\nb'",
"r'a\\\nb'",
"'unterminated",
# Escapes
r"'\a\b\f\n\r\t\v'",
r"'\0\12\101\1011'",
r"'\777'",
r"b'\777'",
r"b'\400'",
r"'\x41'",
r"'\x4'",
r"'\u00e9'",
r"'\u00e'",
r"'\U0001F600'",
r"'\U00110000'",
r"'\ud800'",
r"'\N{BULLET}'",
r"'\q'",
r"'\\'",
r"'\''",
r'"\""',
r"b'\u0041'",
r"b'\x41\xff'",
"b'café'",
"'café'",
r"r'\d'",
r"r'\''",
r"rb'\d'",
r"r'\'",
"'\U0001f600'",
]
def named(entry):
"""Split a corpus entry into its report name and its source text."""
if isinstance(entry, tuple):
return entry
return entry, entry
def evaluate(entry):
name, source = named(entry)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
try:
return {"name": name, "source": source, "repr": repr(ast.literal_eval(source))}
except Exception as error: # noqa: BLE001 - recorded, not raised
return {"name": name, "source": source, "error": type(error).__name__}
def view(value):
if isinstance(value, (list, tuple, set, frozenset)):
return "[" + ", ".join(view(item) for item in value) + "]"
if isinstance(value, dict):
return "{" + ", ".join(f"{view(key)}: {view(item)}" for key, item in value.items()) + "}"
return repr(value)
def plain(value):
"""Whether pickle can encode the value without a class reference such as `complex`."""
if isinstance(value, complex):
return False
if isinstance(value, (list, tuple, set, frozenset)):
return all(plain(item) for item in value)
if isinstance(value, dict):
return all(plain(key) and plain(item) for key, item in value.items())
return True
def is_literal(value):
try:
parsed = ast.literal_eval(repr(value))
except (ValueError, SyntaxError):
return False
return repr(parsed) == repr(value)
def row(entry):
name, source = named(entry)
value = eval(source)
entry = {
"name": name,
"source": source,
"literal": is_literal(value),
"plain": plain(value),
"repr": repr(value),
"str": str(value),
"truthy": bool(value),
}
try:
entry["json"] = json.dumps(value)
except (TypeError, ValueError) as error:
entry["json_error"] = str(error)
try:
entry["pickle"] = {str(protocol): pickle.dumps(value, protocol=protocol).hex() for protocol in range(6)}
entry["view"] = view(value)
except Exception as error: # noqa: BLE001 - recorded, not raised
entry["pickle_error"] = f"{type(error).__name__}: {error}"
return entry
if __name__ == "__main__":
json.dump(
{
"python": sys.version.split()[0],
"rows": [row(entry) for entry in CORPUS],
"sources": [evaluate(entry) for entry in SOURCES],
},
sys.stdout,
indent=2,
ensure_ascii=True,
)
sys.stdout.write("\n")