mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
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>
This commit is contained in:
parent
64456ce103
commit
05d7fb24bd
19 changed files with 5449 additions and 0 deletions
15
litellm-rust/Cargo.lock
generated
15
litellm-rust/Cargo.lock
generated
|
|
@ -3072,6 +3072,21 @@ dependencies = [
|
|||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litellm-python-compat"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"hex",
|
||||
"num-bigint 0.4.8",
|
||||
"num-traits",
|
||||
"rstest",
|
||||
"serde",
|
||||
"serde-pickle",
|
||||
"serde_json",
|
||||
"thiserror 2.0.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litellm-secrets"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
23
litellm-rust/crates/python-compat/AGENTS.md
Normal file
23
litellm-rust/crates/python-compat/AGENTS.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
- Pure Python *data formats* in Rust, for state Python LiteLLM writes and Rust must read or write byte-compatibly
|
||||
- No PyO3, no live objects: truthiness, `__str__`, descriptors of real Python objects belong to `python-bridge`'s coercion layer
|
||||
- Format *choices* stay with callers: the `{timestamp, response}` envelope, diskcache modes, and the cache-key recipe live in the cache crates and only call into this crate
|
||||
- Intended users
|
||||
- `cache-response` codec: reading `str(dict)` values Python's sync Redis path writes (`literal_eval`)
|
||||
- `cache-disk`: diskcache's pickled values (`pickle`), falsy-is-miss (`truthy`)
|
||||
- Cache-key derivation: sha256 over `str(value)` must match Python byte for byte (`repr::to_str`)
|
||||
- Byte-identical writes where Python compares raw values (`json::dumps`, `repr`)
|
||||
- Relation to [`py_literal`](https://docs.rs/py_literal/latest/py_literal/): replaced, do not reintroduce
|
||||
- Its pest grammar backtracks: parse time doubles per nested `[`/`{` (105 ms at depth 16); ours is linear (19 µs at depth 128)
|
||||
- Its formatter is not `repr` (`2e-1`, always single quotes, escapes non-ASCII); it also lost `-0.0`, `(1+2j)`, `set()`
|
||||
- `cache-response` and `cache-disk` still depend on it; migrate them here
|
||||
- Relation to [`serde-pickle`](https://docs.rs/serde-pickle/latest/serde_pickle/): the pickle codec, used only through its serde interface
|
||||
- Never `serde_pickle::Value`: its `BTreeMap` dicts reorder keys
|
||||
- Accepted limits: ints beyond i64, `tuple`/`set`/`frozenset` decode as lists, class references (`GLOBAL`/`REDUCE`) fail; writes protocol 3
|
||||
- Every behavior is pinned by CPython output, not by reasoning; everything under `generated/` is script output, never hand-edited
|
||||
- Regenerate `generated/values.json` with `scripts/generate_fixtures.py`; add a corpus row before changing behavior
|
||||
- Divergences go in `KNOWN` in `tests/fixtures.rs` with a reason; an entry that starts matching fails until deleted
|
||||
- Regenerate `generated/nonprintable.rs` with `scripts/generate_nonprintable.py` when the target Python's Unicode version changes
|
||||
- `scripts/verify_rust_pickles.py` checks CPython reads Rust pickles, with class resolution disabled; CI does not run Python
|
||||
- Decoders recurse, so they reject nesting beyond `MAX_DEPTH` for stack safety: deliberately stricter than CPython, whose parser takes ~200 levels and whose unpickler has no limit (pinned as `nested_150`)
|
||||
- Formatters (`repr`, `json`) are unbounded; values from the decoders are already capped, a hand-built `Value` is the caller's responsibility
|
||||
- `literal_eval` must stay linear in depth: `tests/limits.rs` times the deepest parse, `benches/formats.rs` measures the curve but is manual, since CI runs no Rust bench
|
||||
24
litellm-rust/crates/python-compat/Cargo.toml
Normal file
24
litellm-rust/crates/python-compat/Cargo.toml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "litellm-python-compat"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Python data formats (repr, literal_eval, json.dumps, pickle) reproduced for interop with persisted LiteLLM state"
|
||||
|
||||
[dependencies]
|
||||
num-bigint = "0.4"
|
||||
num-traits = "0.2"
|
||||
serde.workspace = true
|
||||
serde-pickle = "1.2"
|
||||
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
criterion.workspace = true
|
||||
hex = "0.4"
|
||||
rstest.workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "formats"
|
||||
harness = false
|
||||
111
litellm-rust/crates/python-compat/benches/formats.rs
Normal file
111
litellm-rust/crates/python-compat/benches/formats.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//! Throughput of each format on a cached chat completion, and `literal_eval` cost by nesting.
|
||||
//!
|
||||
//! Run one group with `cargo bench -p litellm-python-compat -- cached_completion`, and compare
|
||||
//! against a stored run with `--save-baseline <name>` / `--baseline <name>`.
|
||||
//!
|
||||
//! `literal_eval/nesting` guards against backtracking: the `py_literal` grammar this parser
|
||||
//! replaced doubled its time per nested `[` or `{` (105 ms at depth 16), so cost must stay
|
||||
//! linear in depth for every container shape.
|
||||
|
||||
use std::{hint::black_box, time::Duration};
|
||||
|
||||
use criterion::{
|
||||
BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main,
|
||||
measurement::WallTime,
|
||||
};
|
||||
use litellm_python_compat::{Value, json, literal::literal_eval, pickle, repr::repr};
|
||||
|
||||
/// `str(entry)` for the `{timestamp, response}` envelope Python's sync Redis path writes.
|
||||
fn cached_completion() -> String {
|
||||
let choices: Vec<String> = (0..4)
|
||||
.map(|index| {
|
||||
format!(
|
||||
"{{'finish_reason': 'stop', 'index': {index}, 'message': {{'content': \
|
||||
'Benchmarks compare the same workload under controlled conditions, so a \
|
||||
change in time reflects the code rather than the environment. café 日本 \
|
||||
{index}', 'role': 'assistant', 'tool_calls': None, 'function_call': None}}, \
|
||||
'logprobs': None}}"
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
format!(
|
||||
"{{'timestamp': 1726000000.123, 'response': {{'id': 'chatcmpl-9x1', 'created': \
|
||||
1726000000, 'model': 'gpt-4o-2024-08-06', 'object': 'chat.completion', \
|
||||
'system_fingerprint': 'fp_1', 'choices': [{}], 'usage': {{'completion_tokens': 120, \
|
||||
'prompt_tokens': 42, 'total_tokens': 162, 'completion_tokens_details': None}}}}}}",
|
||||
choices.join(", ")
|
||||
)
|
||||
}
|
||||
|
||||
/// Every text format, measured against the source bytes it reads or writes.
|
||||
fn text_formats(group: &mut BenchmarkGroup<'_, WallTime>, text: &str, value: &Value) {
|
||||
group.throughput(Throughput::Bytes(text.len() as u64));
|
||||
group.bench_function("literal_eval", |bencher| {
|
||||
bencher.iter(|| literal_eval(black_box(text)))
|
||||
});
|
||||
group.bench_function("repr", |bencher| bencher.iter(|| repr(black_box(value))));
|
||||
group.bench_function("json_dumps", |bencher| {
|
||||
bencher.iter(|| json::dumps(black_box(value)))
|
||||
});
|
||||
group.bench_function("to_json", |bencher| {
|
||||
bencher.iter(|| json::to_json(black_box(value)))
|
||||
});
|
||||
}
|
||||
|
||||
/// Pickle, measured against its own encoding rather than the source text.
|
||||
fn binary_formats(group: &mut BenchmarkGroup<'_, WallTime>, value: &Value, pickled: &[u8]) {
|
||||
group.throughput(Throughput::Bytes(pickled.len() as u64));
|
||||
group.bench_function("pickle_dumps", |bencher| {
|
||||
bencher.iter(|| pickle::dumps(black_box(value)))
|
||||
});
|
||||
group.bench_function("pickle_loads", |bencher| {
|
||||
bencher.iter(|| pickle::loads(black_box(pickled)))
|
||||
});
|
||||
}
|
||||
|
||||
fn formats(c: &mut Criterion) {
|
||||
let text = cached_completion();
|
||||
let value = literal_eval(&text).expect("benchmark payload is a literal");
|
||||
let pickled = pickle::dumps(&value).expect("benchmark payload pickles");
|
||||
let dumped = json::dumps(&value).expect("benchmark payload is JSON serializable");
|
||||
|
||||
let mut group = c.benchmark_group("cached_completion");
|
||||
text_formats(&mut group, &text, &value);
|
||||
binary_formats(&mut group, &value, &pickled);
|
||||
// `from_json` consumes its input, so each iteration gets a freshly parsed one.
|
||||
group.throughput(Throughput::Bytes(dumped.len() as u64));
|
||||
group.bench_function("from_json", |bencher| {
|
||||
bencher.iter_batched(
|
||||
|| serde_json::from_str::<serde_json::Value>(&dumped).expect("dumps output parses"),
|
||||
json::from_json,
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// One nesting level of each container shape, as `(name, open, close)`.
|
||||
const SHAPES: [(&str, &str, &str); 3] = [
|
||||
("list", "[", "]"),
|
||||
("dict", "{'a': ", "}"),
|
||||
("tuple", "(", ",)"),
|
||||
];
|
||||
|
||||
fn literal_nesting(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("literal_eval/nesting");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(3));
|
||||
for depth in [4, 16, 64, 128] {
|
||||
for (shape, open, close) in SHAPES {
|
||||
let text = format!("{}1{}", open.repeat(depth), close.repeat(depth));
|
||||
group.throughput(Throughput::Bytes(text.len() as u64));
|
||||
group.bench_with_input(BenchmarkId::new(shape, depth), &text, |bencher, text| {
|
||||
bencher.iter(|| literal_eval(black_box(text)))
|
||||
});
|
||||
}
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, formats, literal_nesting);
|
||||
criterion_main!(benches);
|
||||
745
litellm-rust/crates/python-compat/generated/nonprintable.rs
Normal file
745
litellm-rust/crates/python-compat/generated/nonprintable.rs
Normal file
|
|
@ -0,0 +1,745 @@
|
|||
// Generated by scripts/generate_nonprintable.py from Python 3.14.7
|
||||
// (Unicode 16.0.0). Do not edit by hand.
|
||||
|
||||
pub(crate) const UNICODE_VERSION: &str = "16.0.0";
|
||||
|
||||
/// Inclusive code point ranges for which Python's `str.isprintable()` is false.
|
||||
pub(crate) const NONPRINTABLE: [(u32, u32); 737] = [
|
||||
(0x0000, 0x001F),
|
||||
(0x007F, 0x00A0),
|
||||
(0x00AD, 0x00AD),
|
||||
(0x0378, 0x0379),
|
||||
(0x0380, 0x0383),
|
||||
(0x038B, 0x038B),
|
||||
(0x038D, 0x038D),
|
||||
(0x03A2, 0x03A2),
|
||||
(0x0530, 0x0530),
|
||||
(0x0557, 0x0558),
|
||||
(0x058B, 0x058C),
|
||||
(0x0590, 0x0590),
|
||||
(0x05C8, 0x05CF),
|
||||
(0x05EB, 0x05EE),
|
||||
(0x05F5, 0x0605),
|
||||
(0x061C, 0x061C),
|
||||
(0x06DD, 0x06DD),
|
||||
(0x070E, 0x070F),
|
||||
(0x074B, 0x074C),
|
||||
(0x07B2, 0x07BF),
|
||||
(0x07FB, 0x07FC),
|
||||
(0x082E, 0x082F),
|
||||
(0x083F, 0x083F),
|
||||
(0x085C, 0x085D),
|
||||
(0x085F, 0x085F),
|
||||
(0x086B, 0x086F),
|
||||
(0x088F, 0x0896),
|
||||
(0x08E2, 0x08E2),
|
||||
(0x0984, 0x0984),
|
||||
(0x098D, 0x098E),
|
||||
(0x0991, 0x0992),
|
||||
(0x09A9, 0x09A9),
|
||||
(0x09B1, 0x09B1),
|
||||
(0x09B3, 0x09B5),
|
||||
(0x09BA, 0x09BB),
|
||||
(0x09C5, 0x09C6),
|
||||
(0x09C9, 0x09CA),
|
||||
(0x09CF, 0x09D6),
|
||||
(0x09D8, 0x09DB),
|
||||
(0x09DE, 0x09DE),
|
||||
(0x09E4, 0x09E5),
|
||||
(0x09FF, 0x0A00),
|
||||
(0x0A04, 0x0A04),
|
||||
(0x0A0B, 0x0A0E),
|
||||
(0x0A11, 0x0A12),
|
||||
(0x0A29, 0x0A29),
|
||||
(0x0A31, 0x0A31),
|
||||
(0x0A34, 0x0A34),
|
||||
(0x0A37, 0x0A37),
|
||||
(0x0A3A, 0x0A3B),
|
||||
(0x0A3D, 0x0A3D),
|
||||
(0x0A43, 0x0A46),
|
||||
(0x0A49, 0x0A4A),
|
||||
(0x0A4E, 0x0A50),
|
||||
(0x0A52, 0x0A58),
|
||||
(0x0A5D, 0x0A5D),
|
||||
(0x0A5F, 0x0A65),
|
||||
(0x0A77, 0x0A80),
|
||||
(0x0A84, 0x0A84),
|
||||
(0x0A8E, 0x0A8E),
|
||||
(0x0A92, 0x0A92),
|
||||
(0x0AA9, 0x0AA9),
|
||||
(0x0AB1, 0x0AB1),
|
||||
(0x0AB4, 0x0AB4),
|
||||
(0x0ABA, 0x0ABB),
|
||||
(0x0AC6, 0x0AC6),
|
||||
(0x0ACA, 0x0ACA),
|
||||
(0x0ACE, 0x0ACF),
|
||||
(0x0AD1, 0x0ADF),
|
||||
(0x0AE4, 0x0AE5),
|
||||
(0x0AF2, 0x0AF8),
|
||||
(0x0B00, 0x0B00),
|
||||
(0x0B04, 0x0B04),
|
||||
(0x0B0D, 0x0B0E),
|
||||
(0x0B11, 0x0B12),
|
||||
(0x0B29, 0x0B29),
|
||||
(0x0B31, 0x0B31),
|
||||
(0x0B34, 0x0B34),
|
||||
(0x0B3A, 0x0B3B),
|
||||
(0x0B45, 0x0B46),
|
||||
(0x0B49, 0x0B4A),
|
||||
(0x0B4E, 0x0B54),
|
||||
(0x0B58, 0x0B5B),
|
||||
(0x0B5E, 0x0B5E),
|
||||
(0x0B64, 0x0B65),
|
||||
(0x0B78, 0x0B81),
|
||||
(0x0B84, 0x0B84),
|
||||
(0x0B8B, 0x0B8D),
|
||||
(0x0B91, 0x0B91),
|
||||
(0x0B96, 0x0B98),
|
||||
(0x0B9B, 0x0B9B),
|
||||
(0x0B9D, 0x0B9D),
|
||||
(0x0BA0, 0x0BA2),
|
||||
(0x0BA5, 0x0BA7),
|
||||
(0x0BAB, 0x0BAD),
|
||||
(0x0BBA, 0x0BBD),
|
||||
(0x0BC3, 0x0BC5),
|
||||
(0x0BC9, 0x0BC9),
|
||||
(0x0BCE, 0x0BCF),
|
||||
(0x0BD1, 0x0BD6),
|
||||
(0x0BD8, 0x0BE5),
|
||||
(0x0BFB, 0x0BFF),
|
||||
(0x0C0D, 0x0C0D),
|
||||
(0x0C11, 0x0C11),
|
||||
(0x0C29, 0x0C29),
|
||||
(0x0C3A, 0x0C3B),
|
||||
(0x0C45, 0x0C45),
|
||||
(0x0C49, 0x0C49),
|
||||
(0x0C4E, 0x0C54),
|
||||
(0x0C57, 0x0C57),
|
||||
(0x0C5B, 0x0C5C),
|
||||
(0x0C5E, 0x0C5F),
|
||||
(0x0C64, 0x0C65),
|
||||
(0x0C70, 0x0C76),
|
||||
(0x0C8D, 0x0C8D),
|
||||
(0x0C91, 0x0C91),
|
||||
(0x0CA9, 0x0CA9),
|
||||
(0x0CB4, 0x0CB4),
|
||||
(0x0CBA, 0x0CBB),
|
||||
(0x0CC5, 0x0CC5),
|
||||
(0x0CC9, 0x0CC9),
|
||||
(0x0CCE, 0x0CD4),
|
||||
(0x0CD7, 0x0CDC),
|
||||
(0x0CDF, 0x0CDF),
|
||||
(0x0CE4, 0x0CE5),
|
||||
(0x0CF0, 0x0CF0),
|
||||
(0x0CF4, 0x0CFF),
|
||||
(0x0D0D, 0x0D0D),
|
||||
(0x0D11, 0x0D11),
|
||||
(0x0D45, 0x0D45),
|
||||
(0x0D49, 0x0D49),
|
||||
(0x0D50, 0x0D53),
|
||||
(0x0D64, 0x0D65),
|
||||
(0x0D80, 0x0D80),
|
||||
(0x0D84, 0x0D84),
|
||||
(0x0D97, 0x0D99),
|
||||
(0x0DB2, 0x0DB2),
|
||||
(0x0DBC, 0x0DBC),
|
||||
(0x0DBE, 0x0DBF),
|
||||
(0x0DC7, 0x0DC9),
|
||||
(0x0DCB, 0x0DCE),
|
||||
(0x0DD5, 0x0DD5),
|
||||
(0x0DD7, 0x0DD7),
|
||||
(0x0DE0, 0x0DE5),
|
||||
(0x0DF0, 0x0DF1),
|
||||
(0x0DF5, 0x0E00),
|
||||
(0x0E3B, 0x0E3E),
|
||||
(0x0E5C, 0x0E80),
|
||||
(0x0E83, 0x0E83),
|
||||
(0x0E85, 0x0E85),
|
||||
(0x0E8B, 0x0E8B),
|
||||
(0x0EA4, 0x0EA4),
|
||||
(0x0EA6, 0x0EA6),
|
||||
(0x0EBE, 0x0EBF),
|
||||
(0x0EC5, 0x0EC5),
|
||||
(0x0EC7, 0x0EC7),
|
||||
(0x0ECF, 0x0ECF),
|
||||
(0x0EDA, 0x0EDB),
|
||||
(0x0EE0, 0x0EFF),
|
||||
(0x0F48, 0x0F48),
|
||||
(0x0F6D, 0x0F70),
|
||||
(0x0F98, 0x0F98),
|
||||
(0x0FBD, 0x0FBD),
|
||||
(0x0FCD, 0x0FCD),
|
||||
(0x0FDB, 0x0FFF),
|
||||
(0x10C6, 0x10C6),
|
||||
(0x10C8, 0x10CC),
|
||||
(0x10CE, 0x10CF),
|
||||
(0x1249, 0x1249),
|
||||
(0x124E, 0x124F),
|
||||
(0x1257, 0x1257),
|
||||
(0x1259, 0x1259),
|
||||
(0x125E, 0x125F),
|
||||
(0x1289, 0x1289),
|
||||
(0x128E, 0x128F),
|
||||
(0x12B1, 0x12B1),
|
||||
(0x12B6, 0x12B7),
|
||||
(0x12BF, 0x12BF),
|
||||
(0x12C1, 0x12C1),
|
||||
(0x12C6, 0x12C7),
|
||||
(0x12D7, 0x12D7),
|
||||
(0x1311, 0x1311),
|
||||
(0x1316, 0x1317),
|
||||
(0x135B, 0x135C),
|
||||
(0x137D, 0x137F),
|
||||
(0x139A, 0x139F),
|
||||
(0x13F6, 0x13F7),
|
||||
(0x13FE, 0x13FF),
|
||||
(0x1680, 0x1680),
|
||||
(0x169D, 0x169F),
|
||||
(0x16F9, 0x16FF),
|
||||
(0x1716, 0x171E),
|
||||
(0x1737, 0x173F),
|
||||
(0x1754, 0x175F),
|
||||
(0x176D, 0x176D),
|
||||
(0x1771, 0x1771),
|
||||
(0x1774, 0x177F),
|
||||
(0x17DE, 0x17DF),
|
||||
(0x17EA, 0x17EF),
|
||||
(0x17FA, 0x17FF),
|
||||
(0x180E, 0x180E),
|
||||
(0x181A, 0x181F),
|
||||
(0x1879, 0x187F),
|
||||
(0x18AB, 0x18AF),
|
||||
(0x18F6, 0x18FF),
|
||||
(0x191F, 0x191F),
|
||||
(0x192C, 0x192F),
|
||||
(0x193C, 0x193F),
|
||||
(0x1941, 0x1943),
|
||||
(0x196E, 0x196F),
|
||||
(0x1975, 0x197F),
|
||||
(0x19AC, 0x19AF),
|
||||
(0x19CA, 0x19CF),
|
||||
(0x19DB, 0x19DD),
|
||||
(0x1A1C, 0x1A1D),
|
||||
(0x1A5F, 0x1A5F),
|
||||
(0x1A7D, 0x1A7E),
|
||||
(0x1A8A, 0x1A8F),
|
||||
(0x1A9A, 0x1A9F),
|
||||
(0x1AAE, 0x1AAF),
|
||||
(0x1ACF, 0x1AFF),
|
||||
(0x1B4D, 0x1B4D),
|
||||
(0x1BF4, 0x1BFB),
|
||||
(0x1C38, 0x1C3A),
|
||||
(0x1C4A, 0x1C4C),
|
||||
(0x1C8B, 0x1C8F),
|
||||
(0x1CBB, 0x1CBC),
|
||||
(0x1CC8, 0x1CCF),
|
||||
(0x1CFB, 0x1CFF),
|
||||
(0x1F16, 0x1F17),
|
||||
(0x1F1E, 0x1F1F),
|
||||
(0x1F46, 0x1F47),
|
||||
(0x1F4E, 0x1F4F),
|
||||
(0x1F58, 0x1F58),
|
||||
(0x1F5A, 0x1F5A),
|
||||
(0x1F5C, 0x1F5C),
|
||||
(0x1F5E, 0x1F5E),
|
||||
(0x1F7E, 0x1F7F),
|
||||
(0x1FB5, 0x1FB5),
|
||||
(0x1FC5, 0x1FC5),
|
||||
(0x1FD4, 0x1FD5),
|
||||
(0x1FDC, 0x1FDC),
|
||||
(0x1FF0, 0x1FF1),
|
||||
(0x1FF5, 0x1FF5),
|
||||
(0x1FFF, 0x200F),
|
||||
(0x2028, 0x202F),
|
||||
(0x205F, 0x206F),
|
||||
(0x2072, 0x2073),
|
||||
(0x208F, 0x208F),
|
||||
(0x209D, 0x209F),
|
||||
(0x20C1, 0x20CF),
|
||||
(0x20F1, 0x20FF),
|
||||
(0x218C, 0x218F),
|
||||
(0x242A, 0x243F),
|
||||
(0x244B, 0x245F),
|
||||
(0x2B74, 0x2B75),
|
||||
(0x2B96, 0x2B96),
|
||||
(0x2CF4, 0x2CF8),
|
||||
(0x2D26, 0x2D26),
|
||||
(0x2D28, 0x2D2C),
|
||||
(0x2D2E, 0x2D2F),
|
||||
(0x2D68, 0x2D6E),
|
||||
(0x2D71, 0x2D7E),
|
||||
(0x2D97, 0x2D9F),
|
||||
(0x2DA7, 0x2DA7),
|
||||
(0x2DAF, 0x2DAF),
|
||||
(0x2DB7, 0x2DB7),
|
||||
(0x2DBF, 0x2DBF),
|
||||
(0x2DC7, 0x2DC7),
|
||||
(0x2DCF, 0x2DCF),
|
||||
(0x2DD7, 0x2DD7),
|
||||
(0x2DDF, 0x2DDF),
|
||||
(0x2E5E, 0x2E7F),
|
||||
(0x2E9A, 0x2E9A),
|
||||
(0x2EF4, 0x2EFF),
|
||||
(0x2FD6, 0x2FEF),
|
||||
(0x3000, 0x3000),
|
||||
(0x3040, 0x3040),
|
||||
(0x3097, 0x3098),
|
||||
(0x3100, 0x3104),
|
||||
(0x3130, 0x3130),
|
||||
(0x318F, 0x318F),
|
||||
(0x31E6, 0x31EE),
|
||||
(0x321F, 0x321F),
|
||||
(0xA48D, 0xA48F),
|
||||
(0xA4C7, 0xA4CF),
|
||||
(0xA62C, 0xA63F),
|
||||
(0xA6F8, 0xA6FF),
|
||||
(0xA7CE, 0xA7CF),
|
||||
(0xA7D2, 0xA7D2),
|
||||
(0xA7D4, 0xA7D4),
|
||||
(0xA7DD, 0xA7F1),
|
||||
(0xA82D, 0xA82F),
|
||||
(0xA83A, 0xA83F),
|
||||
(0xA878, 0xA87F),
|
||||
(0xA8C6, 0xA8CD),
|
||||
(0xA8DA, 0xA8DF),
|
||||
(0xA954, 0xA95E),
|
||||
(0xA97D, 0xA97F),
|
||||
(0xA9CE, 0xA9CE),
|
||||
(0xA9DA, 0xA9DD),
|
||||
(0xA9FF, 0xA9FF),
|
||||
(0xAA37, 0xAA3F),
|
||||
(0xAA4E, 0xAA4F),
|
||||
(0xAA5A, 0xAA5B),
|
||||
(0xAAC3, 0xAADA),
|
||||
(0xAAF7, 0xAB00),
|
||||
(0xAB07, 0xAB08),
|
||||
(0xAB0F, 0xAB10),
|
||||
(0xAB17, 0xAB1F),
|
||||
(0xAB27, 0xAB27),
|
||||
(0xAB2F, 0xAB2F),
|
||||
(0xAB6C, 0xAB6F),
|
||||
(0xABEE, 0xABEF),
|
||||
(0xABFA, 0xABFF),
|
||||
(0xD7A4, 0xD7AF),
|
||||
(0xD7C7, 0xD7CA),
|
||||
(0xD7FC, 0xF8FF),
|
||||
(0xFA6E, 0xFA6F),
|
||||
(0xFADA, 0xFAFF),
|
||||
(0xFB07, 0xFB12),
|
||||
(0xFB18, 0xFB1C),
|
||||
(0xFB37, 0xFB37),
|
||||
(0xFB3D, 0xFB3D),
|
||||
(0xFB3F, 0xFB3F),
|
||||
(0xFB42, 0xFB42),
|
||||
(0xFB45, 0xFB45),
|
||||
(0xFBC3, 0xFBD2),
|
||||
(0xFD90, 0xFD91),
|
||||
(0xFDC8, 0xFDCE),
|
||||
(0xFDD0, 0xFDEF),
|
||||
(0xFE1A, 0xFE1F),
|
||||
(0xFE53, 0xFE53),
|
||||
(0xFE67, 0xFE67),
|
||||
(0xFE6C, 0xFE6F),
|
||||
(0xFE75, 0xFE75),
|
||||
(0xFEFD, 0xFF00),
|
||||
(0xFFBF, 0xFFC1),
|
||||
(0xFFC8, 0xFFC9),
|
||||
(0xFFD0, 0xFFD1),
|
||||
(0xFFD8, 0xFFD9),
|
||||
(0xFFDD, 0xFFDF),
|
||||
(0xFFE7, 0xFFE7),
|
||||
(0xFFEF, 0xFFFB),
|
||||
(0xFFFE, 0xFFFF),
|
||||
(0x1000C, 0x1000C),
|
||||
(0x10027, 0x10027),
|
||||
(0x1003B, 0x1003B),
|
||||
(0x1003E, 0x1003E),
|
||||
(0x1004E, 0x1004F),
|
||||
(0x1005E, 0x1007F),
|
||||
(0x100FB, 0x100FF),
|
||||
(0x10103, 0x10106),
|
||||
(0x10134, 0x10136),
|
||||
(0x1018F, 0x1018F),
|
||||
(0x1019D, 0x1019F),
|
||||
(0x101A1, 0x101CF),
|
||||
(0x101FE, 0x1027F),
|
||||
(0x1029D, 0x1029F),
|
||||
(0x102D1, 0x102DF),
|
||||
(0x102FC, 0x102FF),
|
||||
(0x10324, 0x1032C),
|
||||
(0x1034B, 0x1034F),
|
||||
(0x1037B, 0x1037F),
|
||||
(0x1039E, 0x1039E),
|
||||
(0x103C4, 0x103C7),
|
||||
(0x103D6, 0x103FF),
|
||||
(0x1049E, 0x1049F),
|
||||
(0x104AA, 0x104AF),
|
||||
(0x104D4, 0x104D7),
|
||||
(0x104FC, 0x104FF),
|
||||
(0x10528, 0x1052F),
|
||||
(0x10564, 0x1056E),
|
||||
(0x1057B, 0x1057B),
|
||||
(0x1058B, 0x1058B),
|
||||
(0x10593, 0x10593),
|
||||
(0x10596, 0x10596),
|
||||
(0x105A2, 0x105A2),
|
||||
(0x105B2, 0x105B2),
|
||||
(0x105BA, 0x105BA),
|
||||
(0x105BD, 0x105BF),
|
||||
(0x105F4, 0x105FF),
|
||||
(0x10737, 0x1073F),
|
||||
(0x10756, 0x1075F),
|
||||
(0x10768, 0x1077F),
|
||||
(0x10786, 0x10786),
|
||||
(0x107B1, 0x107B1),
|
||||
(0x107BB, 0x107FF),
|
||||
(0x10806, 0x10807),
|
||||
(0x10809, 0x10809),
|
||||
(0x10836, 0x10836),
|
||||
(0x10839, 0x1083B),
|
||||
(0x1083D, 0x1083E),
|
||||
(0x10856, 0x10856),
|
||||
(0x1089F, 0x108A6),
|
||||
(0x108B0, 0x108DF),
|
||||
(0x108F3, 0x108F3),
|
||||
(0x108F6, 0x108FA),
|
||||
(0x1091C, 0x1091E),
|
||||
(0x1093A, 0x1093E),
|
||||
(0x10940, 0x1097F),
|
||||
(0x109B8, 0x109BB),
|
||||
(0x109D0, 0x109D1),
|
||||
(0x10A04, 0x10A04),
|
||||
(0x10A07, 0x10A0B),
|
||||
(0x10A14, 0x10A14),
|
||||
(0x10A18, 0x10A18),
|
||||
(0x10A36, 0x10A37),
|
||||
(0x10A3B, 0x10A3E),
|
||||
(0x10A49, 0x10A4F),
|
||||
(0x10A59, 0x10A5F),
|
||||
(0x10AA0, 0x10ABF),
|
||||
(0x10AE7, 0x10AEA),
|
||||
(0x10AF7, 0x10AFF),
|
||||
(0x10B36, 0x10B38),
|
||||
(0x10B56, 0x10B57),
|
||||
(0x10B73, 0x10B77),
|
||||
(0x10B92, 0x10B98),
|
||||
(0x10B9D, 0x10BA8),
|
||||
(0x10BB0, 0x10BFF),
|
||||
(0x10C49, 0x10C7F),
|
||||
(0x10CB3, 0x10CBF),
|
||||
(0x10CF3, 0x10CF9),
|
||||
(0x10D28, 0x10D2F),
|
||||
(0x10D3A, 0x10D3F),
|
||||
(0x10D66, 0x10D68),
|
||||
(0x10D86, 0x10D8D),
|
||||
(0x10D90, 0x10E5F),
|
||||
(0x10E7F, 0x10E7F),
|
||||
(0x10EAA, 0x10EAA),
|
||||
(0x10EAE, 0x10EAF),
|
||||
(0x10EB2, 0x10EC1),
|
||||
(0x10EC5, 0x10EFB),
|
||||
(0x10F28, 0x10F2F),
|
||||
(0x10F5A, 0x10F6F),
|
||||
(0x10F8A, 0x10FAF),
|
||||
(0x10FCC, 0x10FDF),
|
||||
(0x10FF7, 0x10FFF),
|
||||
(0x1104E, 0x11051),
|
||||
(0x11076, 0x1107E),
|
||||
(0x110BD, 0x110BD),
|
||||
(0x110C3, 0x110CF),
|
||||
(0x110E9, 0x110EF),
|
||||
(0x110FA, 0x110FF),
|
||||
(0x11135, 0x11135),
|
||||
(0x11148, 0x1114F),
|
||||
(0x11177, 0x1117F),
|
||||
(0x111E0, 0x111E0),
|
||||
(0x111F5, 0x111FF),
|
||||
(0x11212, 0x11212),
|
||||
(0x11242, 0x1127F),
|
||||
(0x11287, 0x11287),
|
||||
(0x11289, 0x11289),
|
||||
(0x1128E, 0x1128E),
|
||||
(0x1129E, 0x1129E),
|
||||
(0x112AA, 0x112AF),
|
||||
(0x112EB, 0x112EF),
|
||||
(0x112FA, 0x112FF),
|
||||
(0x11304, 0x11304),
|
||||
(0x1130D, 0x1130E),
|
||||
(0x11311, 0x11312),
|
||||
(0x11329, 0x11329),
|
||||
(0x11331, 0x11331),
|
||||
(0x11334, 0x11334),
|
||||
(0x1133A, 0x1133A),
|
||||
(0x11345, 0x11346),
|
||||
(0x11349, 0x1134A),
|
||||
(0x1134E, 0x1134F),
|
||||
(0x11351, 0x11356),
|
||||
(0x11358, 0x1135C),
|
||||
(0x11364, 0x11365),
|
||||
(0x1136D, 0x1136F),
|
||||
(0x11375, 0x1137F),
|
||||
(0x1138A, 0x1138A),
|
||||
(0x1138C, 0x1138D),
|
||||
(0x1138F, 0x1138F),
|
||||
(0x113B6, 0x113B6),
|
||||
(0x113C1, 0x113C1),
|
||||
(0x113C3, 0x113C4),
|
||||
(0x113C6, 0x113C6),
|
||||
(0x113CB, 0x113CB),
|
||||
(0x113D6, 0x113D6),
|
||||
(0x113D9, 0x113E0),
|
||||
(0x113E3, 0x113FF),
|
||||
(0x1145C, 0x1145C),
|
||||
(0x11462, 0x1147F),
|
||||
(0x114C8, 0x114CF),
|
||||
(0x114DA, 0x1157F),
|
||||
(0x115B6, 0x115B7),
|
||||
(0x115DE, 0x115FF),
|
||||
(0x11645, 0x1164F),
|
||||
(0x1165A, 0x1165F),
|
||||
(0x1166D, 0x1167F),
|
||||
(0x116BA, 0x116BF),
|
||||
(0x116CA, 0x116CF),
|
||||
(0x116E4, 0x116FF),
|
||||
(0x1171B, 0x1171C),
|
||||
(0x1172C, 0x1172F),
|
||||
(0x11747, 0x117FF),
|
||||
(0x1183C, 0x1189F),
|
||||
(0x118F3, 0x118FE),
|
||||
(0x11907, 0x11908),
|
||||
(0x1190A, 0x1190B),
|
||||
(0x11914, 0x11914),
|
||||
(0x11917, 0x11917),
|
||||
(0x11936, 0x11936),
|
||||
(0x11939, 0x1193A),
|
||||
(0x11947, 0x1194F),
|
||||
(0x1195A, 0x1199F),
|
||||
(0x119A8, 0x119A9),
|
||||
(0x119D8, 0x119D9),
|
||||
(0x119E5, 0x119FF),
|
||||
(0x11A48, 0x11A4F),
|
||||
(0x11AA3, 0x11AAF),
|
||||
(0x11AF9, 0x11AFF),
|
||||
(0x11B0A, 0x11BBF),
|
||||
(0x11BE2, 0x11BEF),
|
||||
(0x11BFA, 0x11BFF),
|
||||
(0x11C09, 0x11C09),
|
||||
(0x11C37, 0x11C37),
|
||||
(0x11C46, 0x11C4F),
|
||||
(0x11C6D, 0x11C6F),
|
||||
(0x11C90, 0x11C91),
|
||||
(0x11CA8, 0x11CA8),
|
||||
(0x11CB7, 0x11CFF),
|
||||
(0x11D07, 0x11D07),
|
||||
(0x11D0A, 0x11D0A),
|
||||
(0x11D37, 0x11D39),
|
||||
(0x11D3B, 0x11D3B),
|
||||
(0x11D3E, 0x11D3E),
|
||||
(0x11D48, 0x11D4F),
|
||||
(0x11D5A, 0x11D5F),
|
||||
(0x11D66, 0x11D66),
|
||||
(0x11D69, 0x11D69),
|
||||
(0x11D8F, 0x11D8F),
|
||||
(0x11D92, 0x11D92),
|
||||
(0x11D99, 0x11D9F),
|
||||
(0x11DAA, 0x11EDF),
|
||||
(0x11EF9, 0x11EFF),
|
||||
(0x11F11, 0x11F11),
|
||||
(0x11F3B, 0x11F3D),
|
||||
(0x11F5B, 0x11FAF),
|
||||
(0x11FB1, 0x11FBF),
|
||||
(0x11FF2, 0x11FFE),
|
||||
(0x1239A, 0x123FF),
|
||||
(0x1246F, 0x1246F),
|
||||
(0x12475, 0x1247F),
|
||||
(0x12544, 0x12F8F),
|
||||
(0x12FF3, 0x12FFF),
|
||||
(0x13430, 0x1343F),
|
||||
(0x13456, 0x1345F),
|
||||
(0x143FB, 0x143FF),
|
||||
(0x14647, 0x160FF),
|
||||
(0x1613A, 0x167FF),
|
||||
(0x16A39, 0x16A3F),
|
||||
(0x16A5F, 0x16A5F),
|
||||
(0x16A6A, 0x16A6D),
|
||||
(0x16ABF, 0x16ABF),
|
||||
(0x16ACA, 0x16ACF),
|
||||
(0x16AEE, 0x16AEF),
|
||||
(0x16AF6, 0x16AFF),
|
||||
(0x16B46, 0x16B4F),
|
||||
(0x16B5A, 0x16B5A),
|
||||
(0x16B62, 0x16B62),
|
||||
(0x16B78, 0x16B7C),
|
||||
(0x16B90, 0x16D3F),
|
||||
(0x16D7A, 0x16E3F),
|
||||
(0x16E9B, 0x16EFF),
|
||||
(0x16F4B, 0x16F4E),
|
||||
(0x16F88, 0x16F8E),
|
||||
(0x16FA0, 0x16FDF),
|
||||
(0x16FE5, 0x16FEF),
|
||||
(0x16FF2, 0x16FFF),
|
||||
(0x187F8, 0x187FF),
|
||||
(0x18CD6, 0x18CFE),
|
||||
(0x18D09, 0x1AFEF),
|
||||
(0x1AFF4, 0x1AFF4),
|
||||
(0x1AFFC, 0x1AFFC),
|
||||
(0x1AFFF, 0x1AFFF),
|
||||
(0x1B123, 0x1B131),
|
||||
(0x1B133, 0x1B14F),
|
||||
(0x1B153, 0x1B154),
|
||||
(0x1B156, 0x1B163),
|
||||
(0x1B168, 0x1B16F),
|
||||
(0x1B2FC, 0x1BBFF),
|
||||
(0x1BC6B, 0x1BC6F),
|
||||
(0x1BC7D, 0x1BC7F),
|
||||
(0x1BC89, 0x1BC8F),
|
||||
(0x1BC9A, 0x1BC9B),
|
||||
(0x1BCA0, 0x1CBFF),
|
||||
(0x1CCFA, 0x1CCFF),
|
||||
(0x1CEB4, 0x1CEFF),
|
||||
(0x1CF2E, 0x1CF2F),
|
||||
(0x1CF47, 0x1CF4F),
|
||||
(0x1CFC4, 0x1CFFF),
|
||||
(0x1D0F6, 0x1D0FF),
|
||||
(0x1D127, 0x1D128),
|
||||
(0x1D173, 0x1D17A),
|
||||
(0x1D1EB, 0x1D1FF),
|
||||
(0x1D246, 0x1D2BF),
|
||||
(0x1D2D4, 0x1D2DF),
|
||||
(0x1D2F4, 0x1D2FF),
|
||||
(0x1D357, 0x1D35F),
|
||||
(0x1D379, 0x1D3FF),
|
||||
(0x1D455, 0x1D455),
|
||||
(0x1D49D, 0x1D49D),
|
||||
(0x1D4A0, 0x1D4A1),
|
||||
(0x1D4A3, 0x1D4A4),
|
||||
(0x1D4A7, 0x1D4A8),
|
||||
(0x1D4AD, 0x1D4AD),
|
||||
(0x1D4BA, 0x1D4BA),
|
||||
(0x1D4BC, 0x1D4BC),
|
||||
(0x1D4C4, 0x1D4C4),
|
||||
(0x1D506, 0x1D506),
|
||||
(0x1D50B, 0x1D50C),
|
||||
(0x1D515, 0x1D515),
|
||||
(0x1D51D, 0x1D51D),
|
||||
(0x1D53A, 0x1D53A),
|
||||
(0x1D53F, 0x1D53F),
|
||||
(0x1D545, 0x1D545),
|
||||
(0x1D547, 0x1D549),
|
||||
(0x1D551, 0x1D551),
|
||||
(0x1D6A6, 0x1D6A7),
|
||||
(0x1D7CC, 0x1D7CD),
|
||||
(0x1DA8C, 0x1DA9A),
|
||||
(0x1DAA0, 0x1DAA0),
|
||||
(0x1DAB0, 0x1DEFF),
|
||||
(0x1DF1F, 0x1DF24),
|
||||
(0x1DF2B, 0x1DFFF),
|
||||
(0x1E007, 0x1E007),
|
||||
(0x1E019, 0x1E01A),
|
||||
(0x1E022, 0x1E022),
|
||||
(0x1E025, 0x1E025),
|
||||
(0x1E02B, 0x1E02F),
|
||||
(0x1E06E, 0x1E08E),
|
||||
(0x1E090, 0x1E0FF),
|
||||
(0x1E12D, 0x1E12F),
|
||||
(0x1E13E, 0x1E13F),
|
||||
(0x1E14A, 0x1E14D),
|
||||
(0x1E150, 0x1E28F),
|
||||
(0x1E2AF, 0x1E2BF),
|
||||
(0x1E2FA, 0x1E2FE),
|
||||
(0x1E300, 0x1E4CF),
|
||||
(0x1E4FA, 0x1E5CF),
|
||||
(0x1E5FB, 0x1E5FE),
|
||||
(0x1E600, 0x1E7DF),
|
||||
(0x1E7E7, 0x1E7E7),
|
||||
(0x1E7EC, 0x1E7EC),
|
||||
(0x1E7EF, 0x1E7EF),
|
||||
(0x1E7FF, 0x1E7FF),
|
||||
(0x1E8C5, 0x1E8C6),
|
||||
(0x1E8D7, 0x1E8FF),
|
||||
(0x1E94C, 0x1E94F),
|
||||
(0x1E95A, 0x1E95D),
|
||||
(0x1E960, 0x1EC70),
|
||||
(0x1ECB5, 0x1ED00),
|
||||
(0x1ED3E, 0x1EDFF),
|
||||
(0x1EE04, 0x1EE04),
|
||||
(0x1EE20, 0x1EE20),
|
||||
(0x1EE23, 0x1EE23),
|
||||
(0x1EE25, 0x1EE26),
|
||||
(0x1EE28, 0x1EE28),
|
||||
(0x1EE33, 0x1EE33),
|
||||
(0x1EE38, 0x1EE38),
|
||||
(0x1EE3A, 0x1EE3A),
|
||||
(0x1EE3C, 0x1EE41),
|
||||
(0x1EE43, 0x1EE46),
|
||||
(0x1EE48, 0x1EE48),
|
||||
(0x1EE4A, 0x1EE4A),
|
||||
(0x1EE4C, 0x1EE4C),
|
||||
(0x1EE50, 0x1EE50),
|
||||
(0x1EE53, 0x1EE53),
|
||||
(0x1EE55, 0x1EE56),
|
||||
(0x1EE58, 0x1EE58),
|
||||
(0x1EE5A, 0x1EE5A),
|
||||
(0x1EE5C, 0x1EE5C),
|
||||
(0x1EE5E, 0x1EE5E),
|
||||
(0x1EE60, 0x1EE60),
|
||||
(0x1EE63, 0x1EE63),
|
||||
(0x1EE65, 0x1EE66),
|
||||
(0x1EE6B, 0x1EE6B),
|
||||
(0x1EE73, 0x1EE73),
|
||||
(0x1EE78, 0x1EE78),
|
||||
(0x1EE7D, 0x1EE7D),
|
||||
(0x1EE7F, 0x1EE7F),
|
||||
(0x1EE8A, 0x1EE8A),
|
||||
(0x1EE9C, 0x1EEA0),
|
||||
(0x1EEA4, 0x1EEA4),
|
||||
(0x1EEAA, 0x1EEAA),
|
||||
(0x1EEBC, 0x1EEEF),
|
||||
(0x1EEF2, 0x1EFFF),
|
||||
(0x1F02C, 0x1F02F),
|
||||
(0x1F094, 0x1F09F),
|
||||
(0x1F0AF, 0x1F0B0),
|
||||
(0x1F0C0, 0x1F0C0),
|
||||
(0x1F0D0, 0x1F0D0),
|
||||
(0x1F0F6, 0x1F0FF),
|
||||
(0x1F1AE, 0x1F1E5),
|
||||
(0x1F203, 0x1F20F),
|
||||
(0x1F23C, 0x1F23F),
|
||||
(0x1F249, 0x1F24F),
|
||||
(0x1F252, 0x1F25F),
|
||||
(0x1F266, 0x1F2FF),
|
||||
(0x1F6D8, 0x1F6DB),
|
||||
(0x1F6ED, 0x1F6EF),
|
||||
(0x1F6FD, 0x1F6FF),
|
||||
(0x1F777, 0x1F77A),
|
||||
(0x1F7DA, 0x1F7DF),
|
||||
(0x1F7EC, 0x1F7EF),
|
||||
(0x1F7F1, 0x1F7FF),
|
||||
(0x1F80C, 0x1F80F),
|
||||
(0x1F848, 0x1F84F),
|
||||
(0x1F85A, 0x1F85F),
|
||||
(0x1F888, 0x1F88F),
|
||||
(0x1F8AE, 0x1F8AF),
|
||||
(0x1F8BC, 0x1F8BF),
|
||||
(0x1F8C2, 0x1F8FF),
|
||||
(0x1FA54, 0x1FA5F),
|
||||
(0x1FA6E, 0x1FA6F),
|
||||
(0x1FA7D, 0x1FA7F),
|
||||
(0x1FA8A, 0x1FA8E),
|
||||
(0x1FAC7, 0x1FACD),
|
||||
(0x1FADD, 0x1FADE),
|
||||
(0x1FAEA, 0x1FAEF),
|
||||
(0x1FAF9, 0x1FAFF),
|
||||
(0x1FB93, 0x1FB93),
|
||||
(0x1FBFA, 0x1FFFF),
|
||||
(0x2A6E0, 0x2A6FF),
|
||||
(0x2B73A, 0x2B73F),
|
||||
(0x2B81E, 0x2B81F),
|
||||
(0x2CEA2, 0x2CEAF),
|
||||
(0x2EBE1, 0x2EBEF),
|
||||
(0x2EE5E, 0x2F7FF),
|
||||
(0x2FA1E, 0x2FFFF),
|
||||
(0x3134B, 0x3134F),
|
||||
(0x323B0, 0xE00FF),
|
||||
(0xE01F0, 0x10FFFF),
|
||||
];
|
||||
2164
litellm-rust/crates/python-compat/generated/values.json
Normal file
2164
litellm-rust/crates/python-compat/generated/values.json
Normal file
File diff suppressed because it is too large
Load diff
352
litellm-rust/crates/python-compat/scripts/generate_fixtures.py
Normal file
352
litellm-rust/crates/python-compat/scripts/generate_fixtures.py
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
"""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")
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
"""Regenerate generated/nonprintable.rs from this interpreter's `str.isprintable`.
|
||||
|
||||
`repr(str)` escapes exactly the characters for which `str.isprintable()` is false, so the
|
||||
table must come from the Python version the gateway interoperates with.
|
||||
|
||||
python scripts/generate_nonprintable.py > generated/nonprintable.rs
|
||||
"""
|
||||
|
||||
import sys
|
||||
import unicodedata
|
||||
|
||||
ranges = []
|
||||
start = None
|
||||
for code in range(0x110000):
|
||||
printable = chr(code).isprintable()
|
||||
if not printable and start is None:
|
||||
start = code
|
||||
elif printable and start is not None:
|
||||
ranges.append((start, code - 1))
|
||||
start = None
|
||||
if start is not None:
|
||||
ranges.append((start, 0x10FFFF))
|
||||
|
||||
lines = [
|
||||
f"// Generated by scripts/generate_nonprintable.py from Python {sys.version.split()[0]}",
|
||||
f"// (Unicode {unicodedata.unidata_version}). Do not edit by hand.",
|
||||
"",
|
||||
f'pub(crate) const UNICODE_VERSION: &str = "{unicodedata.unidata_version}";',
|
||||
"",
|
||||
"/// Inclusive code point ranges for which Python's `str.isprintable()` is false.",
|
||||
f"pub(crate) const NONPRINTABLE: [(u32, u32); {len(ranges)}] = [",
|
||||
*(f" (0x{low:04X}, 0x{high:04X})," for low, high in ranges),
|
||||
"];",
|
||||
]
|
||||
sys.stdout.write("\n".join(lines) + "\n")
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
"""Check that CPython unpickles what `pickle::dumps` writes, to the value it was given.
|
||||
|
||||
PYTHON_COMPAT_RUST_PICKLES=rust.tsv cargo test -p litellm-python-compat --test fixtures
|
||||
python scripts/verify_rust_pickles.py rust.tsv
|
||||
|
||||
The rows are plain data by construction, so this refuses to resolve any class rather than
|
||||
handing file-controlled bytes to an unrestricted `pickle.loads`.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import io
|
||||
import pickle
|
||||
import sys
|
||||
|
||||
|
||||
class PlainDataUnpickler(pickle.Unpickler):
|
||||
"""An unpickler with `GLOBAL`/`REDUCE` disabled, mirroring `pickle::loads` in Rust."""
|
||||
|
||||
def find_class(self, module, name):
|
||||
raise pickle.UnpicklingError(f"refusing to resolve {module}.{name}")
|
||||
|
||||
|
||||
def loads(data):
|
||||
return PlainDataUnpickler(io.BytesIO(data)).load()
|
||||
|
||||
|
||||
def main(path):
|
||||
failures = 0
|
||||
rows = 0
|
||||
with open(path, encoding="utf-8") as lines:
|
||||
for line in lines:
|
||||
data, expected = line.rstrip("\n").split("\t", 1)
|
||||
rows += 1
|
||||
actual = repr(loads(bytes.fromhex(data)))
|
||||
if actual != repr(ast.literal_eval(expected)):
|
||||
failures += 1
|
||||
sys.stdout.write(f"mismatch: expected {expected}, got {actual}\n")
|
||||
sys.stdout.write(f"{rows} Rust pickles checked, {failures} mismatches\n")
|
||||
return 1 if failures or not rows else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1]))
|
||||
25
litellm-rust/crates/python-compat/src/error.rs
Normal file
25
litellm-rust/crates/python-compat/src/error.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use crate::MAX_DEPTH;
|
||||
|
||||
/// A failure to read or write a Python format. Messages quote CPython's own wording where
|
||||
/// the Python side raises (`TypeError`, `ValueError`), so callers can log them as is.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("malformed Python literal at byte {0}")]
|
||||
InvalidLiteral(usize),
|
||||
#[error("unhashable type: '{0}'")]
|
||||
Unhashable(&'static str),
|
||||
#[error("value nests deeper than {MAX_DEPTH} levels")]
|
||||
TooDeep,
|
||||
#[error("invalid pickle: {0}")]
|
||||
InvalidPickle(String),
|
||||
#[error("Object of type {0} is not JSON serializable")]
|
||||
NotJsonSerializable(&'static str),
|
||||
#[error("keys must be str, int, float, bool or None, not {0}")]
|
||||
InvalidJsonKey(&'static str),
|
||||
#[error("Out of range float values are not JSON compliant")]
|
||||
NonFiniteFloat,
|
||||
#[error("integer does not fit in the JSON number range")]
|
||||
IntegerOutOfRange,
|
||||
#[error("Object of type {0} cannot be pickled as plain data")]
|
||||
NotPicklable(&'static str),
|
||||
}
|
||||
168
litellm-rust/crates/python-compat/src/json.rs
Normal file
168
litellm-rust/crates/python-compat/src/json.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
//! `json.dumps` with CPython's default options, and the JSON value `json.loads` returns.
|
||||
//!
|
||||
//! Defaults are `ensure_ascii=True`, `allow_nan=True`, separators `(", ", ": ")`, and no key
|
||||
//! sorting. Python's `json.loads` is mapped by [`from_json`]; a `serde_json` number that does
|
||||
//! not fit `i64` or `u64` arrives as a float, where Python would keep an `int`.
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use serde_json::{Map, Number};
|
||||
|
||||
use crate::{Error, Value, repr::float_repr};
|
||||
|
||||
/// `json.dumps(value)`.
|
||||
pub fn dumps(value: &Value) -> Result<String, Error> {
|
||||
let mut out = String::new();
|
||||
write_value(&mut out, value)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The Python value `json.loads` returns for a JSON document.
|
||||
pub fn from_json(value: serde_json::Value) -> Value {
|
||||
match value {
|
||||
serde_json::Value::Null => Value::None,
|
||||
serde_json::Value::Bool(value) => Value::Bool(value),
|
||||
serde_json::Value::Number(number) => {
|
||||
if let Some(value) = number.as_i64() {
|
||||
Value::Int(value.into())
|
||||
} else if let Some(value) = number.as_u64() {
|
||||
Value::Int(value.into())
|
||||
} else {
|
||||
Value::Float(number.as_f64().unwrap_or(f64::NAN))
|
||||
}
|
||||
}
|
||||
serde_json::Value::String(text) => Value::Str(text),
|
||||
serde_json::Value::Array(values) => {
|
||||
Value::List(values.into_iter().map(from_json).collect())
|
||||
}
|
||||
serde_json::Value::Object(entries) => Value::Dict(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|(key, value)| (Value::Str(key), from_json(value)))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// `json.loads(json.dumps(value))` as a `serde_json` value: tuples become arrays and dict
|
||||
/// keys are coerced to strings as `json.dumps` does. Non-finite floats, which Python writes
|
||||
/// as `NaN` and `Infinity`, have no `serde_json` form and fail with [`Error::NonFiniteFloat`].
|
||||
pub fn to_json(value: &Value) -> Result<serde_json::Value, Error> {
|
||||
Ok(match value {
|
||||
Value::None => serde_json::Value::Null,
|
||||
Value::Bool(value) => serde_json::Value::Bool(*value),
|
||||
Value::Int(value) => {
|
||||
let text = value.to_string();
|
||||
serde_json::Value::Number(
|
||||
text.parse::<i64>()
|
||||
.map(Number::from)
|
||||
.or_else(|_| text.parse::<u64>().map(Number::from))
|
||||
.map_err(|_| Error::IntegerOutOfRange)?,
|
||||
)
|
||||
}
|
||||
Value::Float(value) => {
|
||||
serde_json::Value::Number(Number::from_f64(*value).ok_or(Error::NonFiniteFloat)?)
|
||||
}
|
||||
Value::Str(text) => serde_json::Value::String(text.clone()),
|
||||
Value::List(values) | Value::Tuple(values) => {
|
||||
serde_json::Value::Array(values.iter().map(to_json).collect::<Result<Vec<_>, _>>()?)
|
||||
}
|
||||
Value::Dict(entries) => serde_json::Value::Object(
|
||||
entries
|
||||
.iter()
|
||||
.map(|(key, value)| Ok((json_key(key)?, to_json(value)?)))
|
||||
.collect::<Result<Map<_, _>, Error>>()?,
|
||||
),
|
||||
value @ (Value::Bytes(_) | Value::Set(_) | Value::Complex { .. }) => {
|
||||
return Err(Error::NotJsonSerializable(value.type_name()));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn write_value(out: &mut String, value: &Value) -> Result<(), Error> {
|
||||
match value {
|
||||
Value::None => out.push_str("null"),
|
||||
Value::Bool(true) => out.push_str("true"),
|
||||
Value::Bool(false) => out.push_str("false"),
|
||||
Value::Int(value) => {
|
||||
let _ = write!(out, "{value}");
|
||||
}
|
||||
Value::Float(value) => out.push_str(&float_text(*value)),
|
||||
Value::Str(text) => write_string(out, text),
|
||||
Value::List(values) | Value::Tuple(values) => {
|
||||
out.push('[');
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
if index > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_value(out, value)?;
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
Value::Dict(entries) => {
|
||||
out.push('{');
|
||||
for (index, (key, value)) in entries.iter().enumerate() {
|
||||
if index > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_string(out, &json_key(key)?);
|
||||
out.push_str(": ");
|
||||
write_value(out, value)?;
|
||||
}
|
||||
out.push('}');
|
||||
}
|
||||
value @ (Value::Bytes(_) | Value::Set(_) | Value::Complex { .. }) => {
|
||||
return Err(Error::NotJsonSerializable(value.type_name()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `json.encoder.JSONEncoder.iterencode`'s `floatstr` with `allow_nan=True`.
|
||||
fn float_text(value: f64) -> String {
|
||||
if value.is_nan() {
|
||||
"NaN".to_owned()
|
||||
} else if value.is_infinite() {
|
||||
if value > 0.0 { "Infinity" } else { "-Infinity" }.to_owned()
|
||||
} else {
|
||||
float_repr(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dict key coercion in `json.dumps`: scalars become their JSON text, other keys fail.
|
||||
fn json_key(key: &Value) -> Result<String, Error> {
|
||||
Ok(match key {
|
||||
Value::Str(text) => text.clone(),
|
||||
Value::Int(value) => value.to_string(),
|
||||
Value::Float(value) => float_text(*value),
|
||||
Value::Bool(true) => "true".to_owned(),
|
||||
Value::Bool(false) => "false".to_owned(),
|
||||
Value::None => "null".to_owned(),
|
||||
key => return Err(Error::InvalidJsonKey(key.type_name())),
|
||||
})
|
||||
}
|
||||
|
||||
/// `py_encode_basestring_ascii`: escape `"`, `\`, control characters, and everything outside
|
||||
/// printable ASCII as `\uXXXX`, with surrogate pairs above the BMP.
|
||||
fn write_string(out: &mut String, text: &str) {
|
||||
out.push('"');
|
||||
for ch in text.chars() {
|
||||
match ch {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\u{8}' => out.push_str("\\b"),
|
||||
'\u{c}' => out.push_str("\\f"),
|
||||
' '..='~' => out.push(ch),
|
||||
ch => {
|
||||
let mut units = [0u16; 2];
|
||||
for unit in ch.encode_utf16(&mut units) {
|
||||
let _ = write!(out, "\\u{unit:04x}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
39
litellm-rust/crates/python-compat/src/lib.rs
Normal file
39
litellm-rust/crates/python-compat/src/lib.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
//! Python data formats reproduced in Rust, for state that Python LiteLLM writes and reads.
|
||||
//!
|
||||
//! Each module mirrors one Python operation over plain data values, and its tests replay
|
||||
//! fixtures generated by that operation in CPython (`scripts/generate_fixtures.py`):
|
||||
//!
|
||||
//! | Module | Python operation |
|
||||
//! |---|---|
|
||||
//! | [`literal`] | `ast.literal_eval(text)` |
|
||||
//! | [`repr`] | `repr(value)` and `str(value)` |
|
||||
//! | [`json`] | `json.dumps(value)`, and `json.loads(json.dumps(value))` as a JSON value |
|
||||
//! | [`pickle`] | `pickle.loads(data)` and `pickle.dumps(value)` for plain data |
|
||||
//! | [`truthy`] | `bool(value)` |
|
||||
//!
|
||||
//! [`Value`] is the closed data model these formats share. Live Python objects
|
||||
//! (descriptors, `__bool__`, `__str__`, callbacks) are out of scope: those belong to the
|
||||
//! PyO3 boundary in `litellm-python-bridge`, which runs the real protocol.
|
||||
//!
|
||||
//! Known limits, each pinned by a test:
|
||||
//! - `set` iteration order follows Python's hash order, which this crate does not model
|
||||
//! (string hashes are randomized per process). Sets keep their literal order.
|
||||
//! - `str` values are Rust `String`s, so lone surrogates cannot be represented.
|
||||
//! - [`pickle::loads`] decodes `tuple`, `set`, and `frozenset` as lists.
|
||||
|
||||
mod error;
|
||||
pub mod json;
|
||||
pub mod literal;
|
||||
pub mod pickle;
|
||||
pub mod repr;
|
||||
pub mod truthy;
|
||||
mod value;
|
||||
|
||||
pub use error::Error;
|
||||
pub use num_bigint::BigInt;
|
||||
pub use value::Value;
|
||||
|
||||
/// Nesting limit for the decoders, which recurse. It keeps untrusted persisted data from
|
||||
/// overflowing the Rust stack, and is deliberately stricter than CPython, whose parser takes
|
||||
/// about 200 nested brackets and whose unpickler has no limit.
|
||||
pub const MAX_DEPTH: usize = 128;
|
||||
745
litellm-rust/crates/python-compat/src/literal.rs
Normal file
745
litellm-rust/crates/python-compat/src/literal.rs
Normal file
|
|
@ -0,0 +1,745 @@
|
|||
//! `ast.literal_eval(text)`, as a single-pass recursive-descent parser.
|
||||
//!
|
||||
//! Tokens follow CPython's tokenizer (string prefixes, escapes, implicit concatenation,
|
||||
//! numeric underscores and radixes, comments, line continuations). Expressions follow
|
||||
//! `ast.literal_eval`'s evaluator:
|
||||
//!
|
||||
//! - one unary `+`/`-`, applied only to a numeric constant (`-(1)` is fine, `--1` is not)
|
||||
//! - `a + b` / `a - b` only as a signed real plus or minus a complex constant, with 3.14's
|
||||
//! mixed-mode rules (`1 - 0j` is `(1-0j)`)
|
||||
//! - parentheses group without making a tuple; `set()` is the only call
|
||||
//! - dict keys and set members are deduplicated with Python equality (`1 == 1.0 == True`)
|
||||
//!
|
||||
//! Not supported, each pinned by a fixture: `\N{NAME}` escapes, `...`, and escapes that
|
||||
//! produce lone surrogates.
|
||||
|
||||
use std::collections::{HashMap, hash_map::Entry};
|
||||
|
||||
use num_bigint::BigInt;
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
|
||||
use crate::{Error, MAX_DEPTH, Value};
|
||||
|
||||
pub fn literal_eval(text: &str) -> Result<Value, Error> {
|
||||
let mut parser = Parser {
|
||||
bytes: text.trim_start_matches([' ', '\t']).as_bytes(),
|
||||
offset: text.len() - text.trim_start_matches([' ', '\t']).len(),
|
||||
pos: 0,
|
||||
depth: 0,
|
||||
brackets: 0,
|
||||
};
|
||||
parser.skip_leading_lines()?;
|
||||
let value = parser.top_level()?.value;
|
||||
parser.skip_trivia(true);
|
||||
if parser.pos != parser.bytes.len() {
|
||||
return Err(parser.error());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// How a parsed term may take part in `+`/`-`, per `ast.literal_eval`'s `_convert_num`
|
||||
/// (`Constant`) and `_convert_signed_num` (`Signed`). `Other` is any other node.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Kind {
|
||||
Constant,
|
||||
Signed,
|
||||
Other,
|
||||
}
|
||||
|
||||
struct Term {
|
||||
value: Value,
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
impl Term {
|
||||
fn other(value: Value) -> Self {
|
||||
Self {
|
||||
value,
|
||||
kind: Kind::Other,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_number(&self) -> bool {
|
||||
matches!(
|
||||
self.value,
|
||||
Value::Int(_) | Value::Float(_) | Value::Complex { .. }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Parser<'a> {
|
||||
bytes: &'a [u8],
|
||||
/// Bytes stripped before `bytes` starts, so errors report offsets into the input.
|
||||
offset: usize,
|
||||
pos: usize,
|
||||
depth: usize,
|
||||
/// Open brackets: newlines are insignificant only inside them.
|
||||
brackets: usize,
|
||||
}
|
||||
|
||||
impl Parser<'_> {
|
||||
fn error(&self) -> Error {
|
||||
Error::InvalidLiteral(self.offset + self.pos)
|
||||
}
|
||||
|
||||
fn peek(&self) -> Option<u8> {
|
||||
self.bytes.get(self.pos).copied()
|
||||
}
|
||||
|
||||
fn peek_at(&self, ahead: usize) -> Option<u8> {
|
||||
self.bytes.get(self.pos + ahead).copied()
|
||||
}
|
||||
|
||||
fn expect(&mut self, byte: u8) -> Result<(), Error> {
|
||||
if self.peek() != Some(byte) {
|
||||
return Err(self.error());
|
||||
}
|
||||
self.pos += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enter(&mut self) -> Result<(), Error> {
|
||||
self.depth += 1;
|
||||
if self.depth > MAX_DEPTH {
|
||||
return Err(Error::TooDeep);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whitespace, comments, and backslash continuations; newlines too when `newlines`.
|
||||
fn skip_trivia(&mut self, newlines: bool) {
|
||||
while let Some(byte) = self.peek() {
|
||||
match byte {
|
||||
b' ' | b'\t' | b'\x0c' => self.pos += 1,
|
||||
b'#' => {
|
||||
while !matches!(self.peek(), None | Some(b'\n' | b'\r')) {
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
// A continuation joins two lines; one that ends the input is an EOF error.
|
||||
b'\\' if matches!(self.peek_at(1), Some(b'\n' | b'\r')) => {
|
||||
let len = match (self.peek_at(1), self.peek_at(2)) {
|
||||
(Some(b'\r'), Some(b'\n')) => 3,
|
||||
_ => 2,
|
||||
};
|
||||
if self.pos + len >= self.bytes.len() {
|
||||
break;
|
||||
}
|
||||
self.pos += len;
|
||||
}
|
||||
b'\n' | b'\r' if newlines => self.pos += 1,
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Blank and comment-only lines may precede the expression, whose own line must not be
|
||||
/// indented (CPython raises `IndentationError`).
|
||||
fn skip_leading_lines(&mut self) -> Result<(), Error> {
|
||||
loop {
|
||||
let line_start = self.pos;
|
||||
self.skip_trivia(false);
|
||||
match self.peek() {
|
||||
Some(b'\n' | b'\r') => self.pos += 1,
|
||||
Some(_) if line_start > 0 && self.pos > line_start => {
|
||||
self.pos = line_start;
|
||||
return Err(self.error());
|
||||
}
|
||||
_ => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn at_logical_line_end(&self) -> bool {
|
||||
matches!(self.peek(), None | Some(b'\n' | b'\r'))
|
||||
}
|
||||
|
||||
/// The `eval` input: an expression, or a tuple without parentheses.
|
||||
fn top_level(&mut self) -> Result<Term, Error> {
|
||||
let first = self.expression()?;
|
||||
self.skip_trivia(false);
|
||||
if self.peek() != Some(b',') {
|
||||
return Ok(first);
|
||||
}
|
||||
let mut values = vec![first.value];
|
||||
while self.peek() == Some(b',') {
|
||||
self.pos += 1;
|
||||
self.skip_trivia(false);
|
||||
if self.at_logical_line_end() {
|
||||
break;
|
||||
}
|
||||
values.push(self.expression()?.value);
|
||||
self.skip_trivia(false);
|
||||
}
|
||||
Ok(Term::other(Value::Tuple(values)))
|
||||
}
|
||||
|
||||
/// A sum of unary terms, checked as `ast.literal_eval` checks `BinOp`.
|
||||
fn expression(&mut self) -> Result<Term, Error> {
|
||||
let mut left = self.unary()?;
|
||||
loop {
|
||||
self.skip_trivia(self.brackets > 0);
|
||||
let subtract = match self.peek() {
|
||||
Some(b'+') => false,
|
||||
Some(b'-') => true,
|
||||
_ => return Ok(left),
|
||||
};
|
||||
let at = self.pos;
|
||||
self.pos += 1;
|
||||
let right = self.unary()?;
|
||||
left = complex_sum(left, subtract, right)
|
||||
.ok_or(Error::InvalidLiteral(self.offset + at))?;
|
||||
}
|
||||
}
|
||||
|
||||
fn unary(&mut self) -> Result<Term, Error> {
|
||||
self.skip_trivia(self.brackets > 0);
|
||||
let negative = match self.peek() {
|
||||
Some(b'+') => false,
|
||||
Some(b'-') => true,
|
||||
_ => return self.primary(),
|
||||
};
|
||||
let at = self.pos;
|
||||
self.pos += 1;
|
||||
self.enter()?;
|
||||
let operand = self.unary()?;
|
||||
self.depth -= 1;
|
||||
if operand.kind != Kind::Constant || !operand.is_number() {
|
||||
return Err(Error::InvalidLiteral(self.offset + at));
|
||||
}
|
||||
let value = if !negative {
|
||||
operand.value
|
||||
} else {
|
||||
match operand.value {
|
||||
Value::Int(value) => Value::Int(-value),
|
||||
Value::Float(value) => Value::Float(-value),
|
||||
Value::Complex { re, im } => Value::Complex { re: -re, im: -im },
|
||||
_ => unreachable!("checked numeric above"),
|
||||
}
|
||||
};
|
||||
Ok(Term {
|
||||
value,
|
||||
kind: Kind::Signed,
|
||||
})
|
||||
}
|
||||
|
||||
fn primary(&mut self) -> Result<Term, Error> {
|
||||
match self.peek() {
|
||||
Some(b'(') => self.parenthesized(),
|
||||
Some(b'[') => self.list(),
|
||||
Some(b'{') => self.braced(),
|
||||
Some(b'0'..=b'9') => self.number(),
|
||||
Some(b'.') if matches!(self.peek_at(1), Some(b'0'..=b'9')) => self.number(),
|
||||
Some(b'\'' | b'"') => self.strings(),
|
||||
Some(byte) if byte.is_ascii_alphabetic() || byte == b'_' => {
|
||||
if self.string_prefix_len().is_some() {
|
||||
return self.strings();
|
||||
}
|
||||
self.name()
|
||||
}
|
||||
_ => Err(self.error()),
|
||||
}
|
||||
}
|
||||
|
||||
fn open(&mut self) -> Result<(), Error> {
|
||||
self.enter()?;
|
||||
self.brackets += 1;
|
||||
self.pos += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close(&mut self, byte: u8) -> Result<(), Error> {
|
||||
self.skip_trivia(true);
|
||||
self.expect(byte)?;
|
||||
self.brackets -= 1;
|
||||
self.depth -= 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Comma-separated expressions up to `close`, with an optional trailing comma.
|
||||
fn elements(&mut self, close: u8) -> Result<Vec<Value>, Error> {
|
||||
let mut values = Vec::new();
|
||||
loop {
|
||||
self.skip_trivia(true);
|
||||
if self.peek() == Some(close) {
|
||||
return Ok(values);
|
||||
}
|
||||
values.push(self.expression()?.value);
|
||||
self.skip_trivia(true);
|
||||
if self.peek() != Some(b',') {
|
||||
return Ok(values);
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn parenthesized(&mut self) -> Result<Term, Error> {
|
||||
self.open()?;
|
||||
self.skip_trivia(true);
|
||||
if self.peek() == Some(b')') {
|
||||
self.close(b')')?;
|
||||
return Ok(Term::other(Value::Tuple(Vec::new())));
|
||||
}
|
||||
let first = self.expression()?;
|
||||
self.skip_trivia(true);
|
||||
if self.peek() != Some(b',') {
|
||||
self.close(b')')?;
|
||||
return Ok(first);
|
||||
}
|
||||
self.pos += 1;
|
||||
let mut values = vec![first.value];
|
||||
values.extend(self.elements(b')')?);
|
||||
self.close(b')')?;
|
||||
Ok(Term::other(Value::Tuple(values)))
|
||||
}
|
||||
|
||||
fn list(&mut self) -> Result<Term, Error> {
|
||||
self.open()?;
|
||||
let values = self.elements(b']')?;
|
||||
self.close(b']')?;
|
||||
Ok(Term::other(Value::List(values)))
|
||||
}
|
||||
|
||||
fn braced(&mut self) -> Result<Term, Error> {
|
||||
self.open()?;
|
||||
self.skip_trivia(true);
|
||||
if self.peek() == Some(b'}') {
|
||||
self.close(b'}')?;
|
||||
return Ok(Term::other(Value::Dict(Vec::new())));
|
||||
}
|
||||
let first = self.expression()?.value;
|
||||
self.skip_trivia(true);
|
||||
if self.peek() != Some(b':') {
|
||||
let mut members = UniqueValues::default();
|
||||
members.insert(first, None)?;
|
||||
if self.peek() == Some(b',') {
|
||||
self.pos += 1;
|
||||
for member in self.elements(b'}')? {
|
||||
members.insert(member, None)?;
|
||||
}
|
||||
}
|
||||
self.close(b'}')?;
|
||||
return Ok(Term::other(Value::Set(members.keys)));
|
||||
}
|
||||
let mut entries = UniqueValues::default();
|
||||
let mut key = first;
|
||||
loop {
|
||||
self.expect(b':')?;
|
||||
let value = self.expression()?.value;
|
||||
entries.insert(key, Some(value))?;
|
||||
self.skip_trivia(true);
|
||||
if self.peek() != Some(b',') {
|
||||
break;
|
||||
}
|
||||
self.pos += 1;
|
||||
self.skip_trivia(true);
|
||||
if self.peek() == Some(b'}') {
|
||||
break;
|
||||
}
|
||||
key = self.expression()?.value;
|
||||
self.skip_trivia(true);
|
||||
}
|
||||
self.close(b'}')?;
|
||||
Ok(Term::other(Value::Dict(entries.into_entries())))
|
||||
}
|
||||
|
||||
fn identifier(&mut self) -> &[u8] {
|
||||
let start = self.pos;
|
||||
while matches!(self.peek(), Some(byte) if byte.is_ascii_alphanumeric() || byte == b'_') {
|
||||
self.pos += 1;
|
||||
}
|
||||
&self.bytes[start..self.pos]
|
||||
}
|
||||
|
||||
fn name(&mut self) -> Result<Term, Error> {
|
||||
let at = self.pos;
|
||||
let value = match self.identifier() {
|
||||
b"True" => Value::Bool(true),
|
||||
b"False" => Value::Bool(false),
|
||||
b"None" => Value::None,
|
||||
b"set" => {
|
||||
self.skip_trivia(self.brackets > 0);
|
||||
self.expect(b'(')?;
|
||||
self.skip_trivia(true);
|
||||
self.expect(b')')?;
|
||||
return Ok(Term::other(Value::Set(Vec::new())));
|
||||
}
|
||||
_ => return Err(Error::InvalidLiteral(self.offset + at)),
|
||||
};
|
||||
Ok(Term {
|
||||
value,
|
||||
kind: Kind::Constant,
|
||||
})
|
||||
}
|
||||
|
||||
/// Digits with single underscores between them, as CPython's `digitpart`.
|
||||
fn digits(&mut self, radix: u32, out: &mut String) -> Result<(), Error> {
|
||||
let start = out.len();
|
||||
loop {
|
||||
match self.peek() {
|
||||
Some(byte) if (byte as char).is_digit(radix) => {
|
||||
out.push(byte as char);
|
||||
self.pos += 1;
|
||||
}
|
||||
Some(b'_')
|
||||
if out.len() > start
|
||||
&& matches!(self.peek_at(1), Some(next) if (next as char).is_digit(radix)) =>
|
||||
{
|
||||
self.pos += 1;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
if out.len() == start {
|
||||
return Err(self.error());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn number(&mut self) -> Result<Term, Error> {
|
||||
let start = self.pos;
|
||||
let radix = match (
|
||||
self.peek(),
|
||||
self.peek_at(1).map(|byte| byte.to_ascii_lowercase()),
|
||||
) {
|
||||
(Some(b'0'), Some(b'x')) => Some(16),
|
||||
(Some(b'0'), Some(b'o')) => Some(8),
|
||||
(Some(b'0'), Some(b'b')) => Some(2),
|
||||
_ => None,
|
||||
};
|
||||
let mut text = String::new();
|
||||
let value = if let Some(radix) = radix {
|
||||
self.pos += 2;
|
||||
if self.peek() == Some(b'_') {
|
||||
self.pos += 1;
|
||||
}
|
||||
self.digits(radix, &mut text)?;
|
||||
Value::Int(BigInt::parse_bytes(text.as_bytes(), radix).ok_or(self.error())?)
|
||||
} else {
|
||||
let mut is_float = false;
|
||||
if self.peek() != Some(b'.') {
|
||||
self.digits(10, &mut text)?;
|
||||
}
|
||||
let integer_digits = text.clone();
|
||||
if self.peek() == Some(b'.') {
|
||||
is_float = true;
|
||||
self.pos += 1;
|
||||
text.push('.');
|
||||
if matches!(self.peek(), Some(b'0'..=b'9')) {
|
||||
self.digits(10, &mut text)?;
|
||||
}
|
||||
}
|
||||
if matches!(self.peek(), Some(b'e' | b'E')) {
|
||||
is_float = true;
|
||||
self.pos += 1;
|
||||
text.push('e');
|
||||
if let Some(sign @ (b'+' | b'-')) = self.peek() {
|
||||
text.push(sign as char);
|
||||
self.pos += 1;
|
||||
}
|
||||
self.digits(10, &mut text)?;
|
||||
}
|
||||
if matches!(self.peek(), Some(b'j' | b'J')) {
|
||||
self.pos += 1;
|
||||
let im = text.parse::<f64>().map_err(|_| self.error())?;
|
||||
Value::Complex { re: 0.0, im }
|
||||
} else if is_float {
|
||||
Value::Float(text.parse::<f64>().map_err(|_| self.error())?)
|
||||
} else {
|
||||
if integer_digits.len() > 1
|
||||
&& integer_digits.starts_with('0')
|
||||
&& integer_digits.bytes().any(|digit| digit != b'0')
|
||||
{
|
||||
return Err(Error::InvalidLiteral(self.offset + start));
|
||||
}
|
||||
Value::Int(BigInt::parse_bytes(integer_digits.as_bytes(), 10).ok_or(self.error())?)
|
||||
}
|
||||
};
|
||||
if matches!(self.peek(), Some(byte) if byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'.')
|
||||
{
|
||||
return Err(self.error());
|
||||
}
|
||||
Ok(Term {
|
||||
value,
|
||||
kind: Kind::Constant,
|
||||
})
|
||||
}
|
||||
|
||||
/// The length of a valid string prefix (`r`, `u`, `b`, `br`, `rb`, any case) directly
|
||||
/// followed by a quote.
|
||||
fn string_prefix_len(&self) -> Option<usize> {
|
||||
let mut len = 0;
|
||||
while matches!(self.peek_at(len), Some(byte) if byte.is_ascii_alphabetic()) && len < 3 {
|
||||
len += 1;
|
||||
}
|
||||
if !matches!(self.peek_at(len), Some(b'\'' | b'"')) {
|
||||
return None;
|
||||
}
|
||||
let prefix: Vec<u8> = self.bytes[self.pos..self.pos + len]
|
||||
.iter()
|
||||
.map(u8::to_ascii_lowercase)
|
||||
.collect();
|
||||
matches!(prefix.as_slice(), b"" | b"r" | b"u" | b"b" | b"br" | b"rb").then_some(len)
|
||||
}
|
||||
|
||||
/// Adjacent string literals concatenate; `str` and `bytes` cannot mix.
|
||||
fn strings(&mut self) -> Result<Term, Error> {
|
||||
let mut text: Option<String> = None;
|
||||
let mut bytes: Option<Vec<u8>> = None;
|
||||
loop {
|
||||
let at = self.pos;
|
||||
let Some(prefix_len) = self.string_prefix_len() else {
|
||||
break;
|
||||
};
|
||||
let prefix = &self.bytes[self.pos..self.pos + prefix_len];
|
||||
let raw = prefix.iter().any(|byte| byte.eq_ignore_ascii_case(&b'r'));
|
||||
let is_bytes = prefix.iter().any(|byte| byte.eq_ignore_ascii_case(&b'b'));
|
||||
self.pos += prefix_len;
|
||||
let mut out = Vec::new();
|
||||
self.string_body(raw, is_bytes, &mut out)?;
|
||||
if is_bytes {
|
||||
if text.is_some() {
|
||||
return Err(Error::InvalidLiteral(self.offset + at));
|
||||
}
|
||||
bytes.get_or_insert_with(Vec::new).extend(out);
|
||||
} else {
|
||||
if bytes.is_some() {
|
||||
return Err(Error::InvalidLiteral(self.offset + at));
|
||||
}
|
||||
let piece =
|
||||
String::from_utf8(out).map_err(|_| Error::InvalidLiteral(self.offset + at))?;
|
||||
text.get_or_insert_with(String::new).push_str(&piece);
|
||||
}
|
||||
self.skip_trivia(self.brackets > 0);
|
||||
}
|
||||
let value = match (text, bytes) {
|
||||
(Some(text), None) => Value::Str(text),
|
||||
(None, Some(bytes)) => Value::Bytes(bytes),
|
||||
_ => return Err(self.error()),
|
||||
};
|
||||
Ok(Term {
|
||||
value,
|
||||
kind: Kind::Constant,
|
||||
})
|
||||
}
|
||||
|
||||
/// One quoted body, decoded into UTF-8 (`str`) or raw bytes (`bytes`).
|
||||
fn string_body(&mut self, raw: bool, is_bytes: bool, out: &mut Vec<u8>) -> Result<(), Error> {
|
||||
let quote = self.bytes[self.pos];
|
||||
let triple = self.peek_at(1) == Some(quote) && self.peek_at(2) == Some(quote);
|
||||
self.pos += if triple { 3 } else { 1 };
|
||||
loop {
|
||||
let Some(byte) = self.peek() else {
|
||||
return Err(self.error());
|
||||
};
|
||||
if byte == quote
|
||||
&& (!triple || (self.peek_at(1) == Some(quote) && self.peek_at(2) == Some(quote)))
|
||||
{
|
||||
self.pos += if triple { 3 } else { 1 };
|
||||
return Ok(());
|
||||
}
|
||||
match byte {
|
||||
b'\n' | b'\r' if !triple => return Err(self.error()),
|
||||
b'\\' if raw => {
|
||||
let Some(next) = self.peek_at(1) else {
|
||||
return Err(self.error());
|
||||
};
|
||||
out.push(b'\\');
|
||||
self.pos += 1;
|
||||
if next == b'\n' || next == b'\r' || next == quote || next == b'\\' {
|
||||
out.push(next);
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
b'\\' => {
|
||||
self.pos += 1;
|
||||
self.escape(is_bytes, out)?;
|
||||
}
|
||||
byte if is_bytes && !byte.is_ascii() => return Err(self.error()),
|
||||
byte => {
|
||||
out.push(byte);
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escape(&mut self, is_bytes: bool, out: &mut Vec<u8>) -> Result<(), Error> {
|
||||
let Some(byte) = self.peek() else {
|
||||
return Err(self.error());
|
||||
};
|
||||
self.pos += 1;
|
||||
let simple = match byte {
|
||||
b'\n' => return Ok(()),
|
||||
b'\r' => {
|
||||
if self.peek() == Some(b'\n') {
|
||||
self.pos += 1;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
b'\\' | b'\'' | b'"' => byte,
|
||||
b'a' => 0x07,
|
||||
b'b' => 0x08,
|
||||
b'f' => 0x0c,
|
||||
b'n' => b'\n',
|
||||
b'r' => b'\r',
|
||||
b't' => b'\t',
|
||||
b'v' => 0x0b,
|
||||
b'0'..=b'7' => {
|
||||
let mut code = u32::from(byte - b'0');
|
||||
for _ in 0..2 {
|
||||
match self.peek() {
|
||||
Some(digit @ b'0'..=b'7') => {
|
||||
code = code * 8 + u32::from(digit - b'0');
|
||||
self.pos += 1;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
// Bytes keep the low eight bits of `\400`-`\777`, as CPython does.
|
||||
return self.push_code(if is_bytes { code & 0xff } else { code }, is_bytes, out);
|
||||
}
|
||||
b'x' => {
|
||||
let code = self.hex(2)?;
|
||||
return self.push_code(code, is_bytes, out);
|
||||
}
|
||||
b'u' if !is_bytes => {
|
||||
let code = self.hex(4)?;
|
||||
return self.push_code(code, is_bytes, out);
|
||||
}
|
||||
b'U' if !is_bytes => {
|
||||
let code = self.hex(8)?;
|
||||
return self.push_code(code, is_bytes, out);
|
||||
}
|
||||
b'N' if !is_bytes => return Err(self.error()),
|
||||
_ => {
|
||||
// Unknown escapes keep the backslash (a `SyntaxWarning` in CPython).
|
||||
out.push(b'\\');
|
||||
self.pos -= 1;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
out.push(simple);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hex(&mut self, count: usize) -> Result<u32, Error> {
|
||||
let mut code = 0u32;
|
||||
for _ in 0..count {
|
||||
let digit = self
|
||||
.peek()
|
||||
.and_then(|byte| (byte as char).to_digit(16))
|
||||
.ok_or(self.error())?;
|
||||
code = code * 16 + digit;
|
||||
self.pos += 1;
|
||||
}
|
||||
Ok(code)
|
||||
}
|
||||
|
||||
fn push_code(&self, code: u32, is_bytes: bool, out: &mut Vec<u8>) -> Result<(), Error> {
|
||||
if is_bytes {
|
||||
out.push(u8::try_from(code).map_err(|_| self.error())?);
|
||||
return Ok(());
|
||||
}
|
||||
let ch = char::from_u32(code).ok_or(self.error())?;
|
||||
let mut buffer = [0u8; 4];
|
||||
out.extend_from_slice(ch.encode_utf8(&mut buffer).as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// `left + right` or `left - right` as `ast.literal_eval` permits: a signed real on the
|
||||
/// left and an unsigned complex constant on the right, combined with CPython 3.14's
|
||||
/// mixed-mode rules, which leave the imaginary part untouched by the real operand.
|
||||
fn complex_sum(left: Term, subtract: bool, right: Term) -> Option<Term> {
|
||||
if left.kind == Kind::Other || right.kind != Kind::Constant {
|
||||
return None;
|
||||
}
|
||||
let real = match &left.value {
|
||||
Value::Int(value) => value.to_f64().filter(|value| value.is_finite())?,
|
||||
Value::Float(value) => *value,
|
||||
_ => return None,
|
||||
};
|
||||
let Value::Complex { re, im } = right.value else {
|
||||
return None;
|
||||
};
|
||||
let value = if subtract {
|
||||
Value::Complex {
|
||||
re: real - re,
|
||||
im: -im,
|
||||
}
|
||||
} else {
|
||||
Value::Complex { re: real + re, im }
|
||||
};
|
||||
Some(Term::other(value))
|
||||
}
|
||||
|
||||
/// Python equality for hashable literal values: numbers compare by value across `bool`,
|
||||
/// `int`, `float`, and `complex`, so `1`, `1.0`, `True`, and `(1+0j)` are one key.
|
||||
#[derive(Hash, PartialEq, Eq)]
|
||||
enum KeyId {
|
||||
None,
|
||||
Int(BigInt),
|
||||
Float(u64),
|
||||
Complex(u64, u64),
|
||||
Str(String),
|
||||
Bytes(Vec<u8>),
|
||||
Tuple(Vec<KeyId>),
|
||||
}
|
||||
|
||||
fn float_key(value: f64) -> KeyId {
|
||||
if value.fract() == 0.0
|
||||
&& let Some(integer) = BigInt::from_f64(value)
|
||||
{
|
||||
return KeyId::Int(integer);
|
||||
}
|
||||
KeyId::Float(value.to_bits())
|
||||
}
|
||||
|
||||
fn key_id(value: &Value) -> Result<KeyId, Error> {
|
||||
Ok(match value {
|
||||
Value::None => KeyId::None,
|
||||
Value::Bool(value) => KeyId::Int(BigInt::from(u8::from(*value))),
|
||||
Value::Int(value) => KeyId::Int(value.clone()),
|
||||
Value::Float(value) => float_key(*value),
|
||||
Value::Complex { re, im } if *im == 0.0 => float_key(*re),
|
||||
Value::Complex { re, im } => KeyId::Complex((re + 0.0).to_bits(), (im + 0.0).to_bits()),
|
||||
Value::Str(text) => KeyId::Str(text.clone()),
|
||||
Value::Bytes(bytes) => KeyId::Bytes(bytes.clone()),
|
||||
Value::Tuple(values) => KeyId::Tuple(values.iter().map(key_id).collect::<Result<_, _>>()?),
|
||||
value @ (Value::List(_) | Value::Dict(_) | Value::Set(_)) => {
|
||||
return Err(Error::Unhashable(value.type_name()));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Dict entries or set members in first-seen order: a repeated key keeps its first
|
||||
/// position and, for dicts, takes the latest value.
|
||||
#[derive(Default)]
|
||||
struct UniqueValues {
|
||||
keys: Vec<Value>,
|
||||
values: Vec<Value>,
|
||||
index: HashMap<KeyId, usize>,
|
||||
}
|
||||
|
||||
impl UniqueValues {
|
||||
fn insert(&mut self, key: Value, value: Option<Value>) -> Result<(), Error> {
|
||||
match self.index.entry(key_id(&key)?) {
|
||||
Entry::Occupied(slot) => {
|
||||
if let Some(value) = value {
|
||||
self.values[*slot.get()] = value;
|
||||
}
|
||||
}
|
||||
Entry::Vacant(slot) => {
|
||||
slot.insert(self.keys.len());
|
||||
self.keys.push(key);
|
||||
self.values.extend(value);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_entries(self) -> Vec<(Value, Value)> {
|
||||
self.keys.into_iter().zip(self.values).collect()
|
||||
}
|
||||
}
|
||||
187
litellm-rust/crates/python-compat/src/pickle.rs
Normal file
187
litellm-rust/crates/python-compat/src/pickle.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
//! `pickle.loads` and `pickle.dumps` for plain data, as diskcache stores LiteLLM values.
|
||||
//!
|
||||
//! Both directions go through `serde-pickle`'s serde interface rather than
|
||||
//! `serde_pickle::Value`, because that value type keeps dicts in a `BTreeMap` and would
|
||||
//! reorder keys. The serde interface keeps insertion order, at the cost of reporting
|
||||
//! `tuple`, `set`, and `frozenset` as sequences: [`loads`] decodes all three as lists.
|
||||
//! Python objects that need a class (`GLOBAL`/`REDUCE`) and recursive structures fail.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::{
|
||||
Deserializer, Serialize, Serializer,
|
||||
de::{self, DeserializeSeed, MapAccess, SeqAccess, Visitor},
|
||||
ser::{SerializeMap, SerializeSeq, SerializeTuple},
|
||||
};
|
||||
use serde_pickle::{DeOptions, SerOptions};
|
||||
|
||||
use crate::{Error, MAX_DEPTH, Value};
|
||||
|
||||
/// `pickle.loads(data)` for any protocol from 0 to 5.
|
||||
pub fn loads(data: &[u8]) -> Result<Value, Error> {
|
||||
let mut deserializer = serde_pickle::Deserializer::new(data, DeOptions::new());
|
||||
let value = Seed { depth: 0 }
|
||||
.deserialize(&mut deserializer)
|
||||
.map_err(|error| Error::InvalidPickle(error.to_string()))?;
|
||||
deserializer
|
||||
.end()
|
||||
.map_err(|error| Error::InvalidPickle(error.to_string()))?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// `pickle.dumps(value, protocol=3)`. Every Python 3 reads protocol 3, whatever its own
|
||||
/// default. Sets and complex numbers are rejected rather than silently changing type.
|
||||
pub fn dumps(value: &Value) -> Result<Vec<u8>, Error> {
|
||||
check_picklable(value, 0)?;
|
||||
serde_pickle::to_vec(&Pickled(value), SerOptions::new())
|
||||
.map_err(|error| Error::InvalidPickle(error.to_string()))
|
||||
}
|
||||
|
||||
fn check_picklable(value: &Value, depth: usize) -> Result<(), Error> {
|
||||
if depth > MAX_DEPTH {
|
||||
return Err(Error::TooDeep);
|
||||
}
|
||||
match value {
|
||||
Value::Int(value) if i64::try_from(value).is_err() => Err(Error::IntegerOutOfRange),
|
||||
value @ (Value::Set(_) | Value::Complex { .. }) => {
|
||||
Err(Error::NotPicklable(value.type_name()))
|
||||
}
|
||||
Value::List(values) | Value::Tuple(values) => values
|
||||
.iter()
|
||||
.try_for_each(|value| check_picklable(value, depth + 1)),
|
||||
Value::Dict(entries) => entries.iter().try_for_each(|(key, value)| {
|
||||
check_picklable(key, depth + 1)?;
|
||||
check_picklable(value, depth + 1)
|
||||
}),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
struct Pickled<'a>(&'a Value);
|
||||
|
||||
impl Serialize for Pickled<'_> {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match self.0 {
|
||||
Value::None => serializer.serialize_unit(),
|
||||
Value::Bool(value) => serializer.serialize_bool(*value),
|
||||
Value::Int(value) => {
|
||||
let value = i64::try_from(value)
|
||||
.map_err(|_| serde::ser::Error::custom("integer out of i64 range"))?;
|
||||
serializer.serialize_i64(value)
|
||||
}
|
||||
Value::Float(value) => serializer.serialize_f64(*value),
|
||||
Value::Str(text) => serializer.serialize_str(text),
|
||||
Value::Bytes(bytes) => serializer.serialize_bytes(bytes),
|
||||
Value::List(values) => {
|
||||
let mut seq = serializer.serialize_seq(Some(values.len()))?;
|
||||
for value in values {
|
||||
seq.serialize_element(&Pickled(value))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
Value::Tuple(values) => {
|
||||
let mut tuple = serializer.serialize_tuple(values.len())?;
|
||||
for value in values {
|
||||
tuple.serialize_element(&Pickled(value))?;
|
||||
}
|
||||
tuple.end()
|
||||
}
|
||||
Value::Dict(entries) => {
|
||||
let mut map = serializer.serialize_map(Some(entries.len()))?;
|
||||
for (key, value) in entries {
|
||||
map.serialize_entry(&Pickled(key), &Pickled(value))?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
value @ (Value::Set(_) | Value::Complex { .. }) => Err(serde::ser::Error::custom(
|
||||
format!("{} cannot be pickled as plain data", value.type_name()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Seed {
|
||||
depth: usize,
|
||||
}
|
||||
|
||||
impl Seed {
|
||||
fn child<E: de::Error>(self) -> Result<Self, E> {
|
||||
if self.depth >= MAX_DEPTH {
|
||||
return Err(E::custom(Error::TooDeep));
|
||||
}
|
||||
Ok(Self {
|
||||
depth: self.depth + 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeSeed<'de> for Seed {
|
||||
type Value = Value;
|
||||
|
||||
fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<Value, D::Error> {
|
||||
deserializer.deserialize_any(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Visitor<'de> for Seed {
|
||||
type Value = Value;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("a plain Python data value")
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Value, E> {
|
||||
Ok(Value::None)
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, value: bool) -> Result<Value, E> {
|
||||
Ok(Value::Bool(value))
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, value: i64) -> Result<Value, E> {
|
||||
Ok(Value::Int(value.into()))
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, value: u64) -> Result<Value, E> {
|
||||
Ok(Value::Int(value.into()))
|
||||
}
|
||||
|
||||
fn visit_f64<E>(self, value: f64) -> Result<Value, E> {
|
||||
Ok(Value::Float(value))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Value, E> {
|
||||
Ok(Value::Str(value.to_owned()))
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, value: String) -> Result<Value, E> {
|
||||
Ok(Value::Str(value))
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, value: &[u8]) -> Result<Value, E> {
|
||||
Ok(Value::Bytes(value.to_vec()))
|
||||
}
|
||||
|
||||
fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Value, E> {
|
||||
Ok(Value::Bytes(value))
|
||||
}
|
||||
|
||||
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Value, A::Error> {
|
||||
let child = self.child()?;
|
||||
let mut values = Vec::with_capacity(seq.size_hint().unwrap_or(0).min(4096));
|
||||
while let Some(value) = seq.next_element_seed(child)? {
|
||||
values.push(value);
|
||||
}
|
||||
Ok(Value::List(values))
|
||||
}
|
||||
|
||||
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Value, A::Error> {
|
||||
let child = self.child()?;
|
||||
let mut entries = Vec::with_capacity(map.size_hint().unwrap_or(0).min(4096));
|
||||
while let Some(key) = map.next_key_seed(child)? {
|
||||
entries.push((key, map.next_value_seed(child)?));
|
||||
}
|
||||
Ok(Value::Dict(entries))
|
||||
}
|
||||
}
|
||||
237
litellm-rust/crates/python-compat/src/repr.rs
Normal file
237
litellm-rust/crates/python-compat/src/repr.rs
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
//! `repr(value)` and `str(value)` byte for byte.
|
||||
//!
|
||||
//! LiteLLM hashes `str(value)` into cache keys and writes `str(dict)` into Redis, so these
|
||||
//! strings are persisted identifiers rather than display text: every quote choice, escape,
|
||||
//! and float digit must match CPython.
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::Value;
|
||||
|
||||
/// Generated by `scripts/generate_nonprintable.py`; see `generated/nonprintable.rs`.
|
||||
mod nonprintable {
|
||||
include!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/generated/nonprintable.rs"
|
||||
));
|
||||
}
|
||||
|
||||
/// Unicode version of the printable-character table, from the Python that generated it.
|
||||
pub const UNICODE_VERSION: &str = nonprintable::UNICODE_VERSION;
|
||||
|
||||
/// `repr(value)`.
|
||||
pub fn repr(value: &Value) -> String {
|
||||
let mut out = String::new();
|
||||
write_repr(&mut out, value);
|
||||
out
|
||||
}
|
||||
|
||||
/// `str(value)`: a string's own contents, and `repr` for every other value.
|
||||
pub fn to_str(value: &Value) -> String {
|
||||
match value {
|
||||
Value::Str(text) => text.clone(),
|
||||
value => repr(value),
|
||||
}
|
||||
}
|
||||
|
||||
/// `repr(float)`, the shortest round-trip form with CPython's exponent thresholds.
|
||||
pub fn float_repr(value: f64) -> String {
|
||||
format_float(value, true)
|
||||
}
|
||||
|
||||
fn write_repr(out: &mut String, value: &Value) {
|
||||
match value {
|
||||
Value::None => out.push_str("None"),
|
||||
Value::Bool(true) => out.push_str("True"),
|
||||
Value::Bool(false) => out.push_str("False"),
|
||||
Value::Int(value) => {
|
||||
let _ = write!(out, "{value}");
|
||||
}
|
||||
Value::Float(value) => out.push_str(&float_repr(*value)),
|
||||
Value::Complex { re, im } => write_complex(out, *re, *im),
|
||||
Value::Str(text) => write_str(out, text),
|
||||
Value::Bytes(bytes) => write_bytes(out, bytes),
|
||||
Value::List(values) => write_sequence(out, '[', values, ']'),
|
||||
Value::Tuple(values) if values.len() == 1 => {
|
||||
out.push('(');
|
||||
write_repr(out, &values[0]);
|
||||
out.push_str(",)");
|
||||
}
|
||||
Value::Tuple(values) => write_sequence(out, '(', values, ')'),
|
||||
Value::Set(values) if values.is_empty() => out.push_str("set()"),
|
||||
Value::Set(values) => write_sequence(out, '{', values, '}'),
|
||||
Value::Dict(entries) => {
|
||||
out.push('{');
|
||||
for (index, (key, value)) in entries.iter().enumerate() {
|
||||
if index > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_repr(out, key);
|
||||
out.push_str(": ");
|
||||
write_repr(out, value);
|
||||
}
|
||||
out.push('}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_sequence(out: &mut String, open: char, values: &[Value], close: char) {
|
||||
out.push(open);
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
if index > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
write_repr(out, value);
|
||||
}
|
||||
out.push(close);
|
||||
}
|
||||
|
||||
/// `complex.__repr__`: a `+0.0` real part prints only the imaginary part, without parens.
|
||||
fn write_complex(out: &mut String, re: f64, im: f64) {
|
||||
if re == 0.0 && re.is_sign_positive() {
|
||||
out.push_str(&format_float(im, false));
|
||||
out.push('j');
|
||||
return;
|
||||
}
|
||||
out.push('(');
|
||||
out.push_str(&format_float(re, false));
|
||||
let im_text = format_float(im, false);
|
||||
if !im_text.starts_with('-') {
|
||||
out.push('+');
|
||||
}
|
||||
out.push_str(&im_text);
|
||||
out.push_str("j)");
|
||||
}
|
||||
|
||||
/// `PyOS_double_to_string(value, 'r', 0, flags)`: scientific notation below 1e-4 and from
|
||||
/// 1e16 up, with a signed exponent of at least two digits. `add_dot_0` is
|
||||
/// `Py_DTSF_ADD_DOT_0`, which `float` sets and `complex` does not.
|
||||
fn format_float(value: f64, add_dot_0: bool) -> String {
|
||||
if value.is_nan() {
|
||||
return "nan".to_owned();
|
||||
}
|
||||
if value.is_infinite() {
|
||||
return if value > 0.0 { "inf" } else { "-inf" }.to_owned();
|
||||
}
|
||||
// Rust's `{:e}` prints the shortest round-trip digits, like CPython's 'r' mode.
|
||||
let scientific = format!("{value:e}");
|
||||
let (mantissa, exponent) = scientific
|
||||
.split_once('e')
|
||||
.expect("`{:e}` always prints an exponent");
|
||||
let exponent: i32 = exponent.parse().expect("`{:e}` exponent is an integer");
|
||||
let (sign, mantissa) = match mantissa.strip_prefix('-') {
|
||||
Some(mantissa) => ("-", mantissa),
|
||||
None => ("", mantissa),
|
||||
};
|
||||
let digits: String = mantissa.chars().filter(|ch| *ch != '.').collect();
|
||||
|
||||
let mut out = String::from(sign);
|
||||
if !(-4..16).contains(&exponent) {
|
||||
out.push_str(&digits[..1]);
|
||||
if digits.len() > 1 {
|
||||
out.push('.');
|
||||
out.push_str(&digits[1..]);
|
||||
}
|
||||
let _ = write!(
|
||||
out,
|
||||
"e{}{:02}",
|
||||
if exponent < 0 { '-' } else { '+' },
|
||||
exponent.unsigned_abs()
|
||||
);
|
||||
} else if exponent < 0 {
|
||||
out.push_str("0.");
|
||||
out.extend(std::iter::repeat_n('0', (-exponent - 1) as usize));
|
||||
out.push_str(&digits);
|
||||
} else {
|
||||
let integer_digits = exponent as usize + 1;
|
||||
if digits.len() > integer_digits {
|
||||
out.push_str(&digits[..integer_digits]);
|
||||
out.push('.');
|
||||
out.push_str(&digits[integer_digits..]);
|
||||
} else {
|
||||
out.push_str(&digits);
|
||||
out.extend(std::iter::repeat_n('0', integer_digits - digits.len()));
|
||||
if add_dot_0 {
|
||||
out.push_str(".0");
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `unicode_repr`: single quotes unless the text has a `'` and no `"`. Printable non-ASCII
|
||||
/// stays literal; everything `str.isprintable()` rejects is escaped.
|
||||
fn write_str(out: &mut String, text: &str) {
|
||||
let quote = if text.contains('\'') && !text.contains('"') {
|
||||
'"'
|
||||
} else {
|
||||
'\''
|
||||
};
|
||||
out.push(quote);
|
||||
for ch in text.chars() {
|
||||
match ch {
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
ch if ch == quote => {
|
||||
out.push('\\');
|
||||
out.push(ch);
|
||||
}
|
||||
ch if is_printable(ch) => out.push(ch),
|
||||
ch => {
|
||||
let code = ch as u32;
|
||||
let _ = match code {
|
||||
0..=0xff => write!(out, "\\x{code:02x}"),
|
||||
0x100..=0xffff => write!(out, "\\u{code:04x}"),
|
||||
_ => write!(out, "\\U{code:08x}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(quote);
|
||||
}
|
||||
|
||||
/// `bytes.__repr__`: the same quote rule as `str`, with every byte outside printable ASCII
|
||||
/// escaped as `\xhh`.
|
||||
fn write_bytes(out: &mut String, bytes: &[u8]) {
|
||||
let quote = if bytes.contains(&b'\'') && !bytes.contains(&b'"') {
|
||||
b'"'
|
||||
} else {
|
||||
b'\''
|
||||
};
|
||||
out.push('b');
|
||||
out.push(quote as char);
|
||||
for &byte in bytes {
|
||||
match byte {
|
||||
b'\\' => out.push_str("\\\\"),
|
||||
b'\t' => out.push_str("\\t"),
|
||||
b'\n' => out.push_str("\\n"),
|
||||
b'\r' => out.push_str("\\r"),
|
||||
byte if byte == quote => {
|
||||
out.push('\\');
|
||||
out.push(byte as char);
|
||||
}
|
||||
0x20..=0x7e => out.push(byte as char),
|
||||
byte => {
|
||||
let _ = write!(out, "\\x{byte:02x}");
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(quote as char);
|
||||
}
|
||||
|
||||
fn is_printable(ch: char) -> bool {
|
||||
let code = ch as u32;
|
||||
nonprintable::NONPRINTABLE
|
||||
.binary_search_by(|&(low, high)| {
|
||||
if high < code {
|
||||
std::cmp::Ordering::Less
|
||||
} else if low > code {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
})
|
||||
.is_err()
|
||||
}
|
||||
18
litellm-rust/crates/python-compat/src/truthy.rs
Normal file
18
litellm-rust/crates/python-compat/src/truthy.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use num_bigint::Sign;
|
||||
|
||||
use crate::Value;
|
||||
|
||||
/// `bool(value)` for plain data: `None`, `False`, zero, and empty containers are false.
|
||||
pub fn truthy(value: &Value) -> bool {
|
||||
match value {
|
||||
Value::None => false,
|
||||
Value::Bool(value) => *value,
|
||||
Value::Int(value) => value.sign() != Sign::NoSign,
|
||||
Value::Float(value) => *value != 0.0,
|
||||
Value::Complex { re, im } => *re != 0.0 || *im != 0.0,
|
||||
Value::Str(value) => !value.is_empty(),
|
||||
Value::Bytes(value) => !value.is_empty(),
|
||||
Value::Tuple(values) | Value::List(values) | Value::Set(values) => !values.is_empty(),
|
||||
Value::Dict(entries) => !entries.is_empty(),
|
||||
}
|
||||
}
|
||||
53
litellm-rust/crates/python-compat/src/value.rs
Normal file
53
litellm-rust/crates/python-compat/src/value.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
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())
|
||||
}
|
||||
}
|
||||
343
litellm-rust/crates/python-compat/tests/fixtures.rs
Normal file
343
litellm-rust/crates/python-compat/tests/fixtures.rs
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
//! Replays `generated/values.json`, which CPython wrote with `scripts/generate_fixtures.py`.
|
||||
|
||||
use std::{collections::BTreeMap, fs::File, io::Write};
|
||||
|
||||
use litellm_python_compat::{
|
||||
Value, json,
|
||||
literal::literal_eval,
|
||||
pickle,
|
||||
repr::{repr, to_str},
|
||||
truthy::truthy,
|
||||
};
|
||||
use rstest::{fixture, rstest};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Fixtures {
|
||||
rows: Vec<Row>,
|
||||
sources: Vec<Source>,
|
||||
}
|
||||
|
||||
/// `ast.literal_eval(source)`: the `repr` of its result, or the exception class it raised.
|
||||
#[derive(Deserialize)]
|
||||
struct Source {
|
||||
name: String,
|
||||
source: String,
|
||||
repr: Option<String>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Row {
|
||||
name: String,
|
||||
source: String,
|
||||
literal: bool,
|
||||
plain: bool,
|
||||
repr: String,
|
||||
str: String,
|
||||
truthy: bool,
|
||||
json: Option<String>,
|
||||
json_error: Option<String>,
|
||||
pickle: Option<BTreeMap<String, String>>,
|
||||
view: Option<String>,
|
||||
}
|
||||
|
||||
/// Parsed once for the whole test binary.
|
||||
#[fixture]
|
||||
#[once]
|
||||
fn fixtures() -> Fixtures {
|
||||
serde_json::from_str(include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/generated/values.json"
|
||||
)))
|
||||
.expect("values.json matches the fixture schema")
|
||||
}
|
||||
|
||||
/// Accepted differences from CPython: `(fixture source, check prefix, reason)`. Each entry
|
||||
/// must still differ, so a dependency fix that removes one fails the test until it is deleted.
|
||||
const KNOWN: &[(&str, &str, &str)] = &[
|
||||
(
|
||||
"...",
|
||||
"literal_eval source",
|
||||
"`Ellipsis` is not part of the data model",
|
||||
),
|
||||
(
|
||||
r"'\ud800'",
|
||||
"literal_eval source",
|
||||
"a Rust `String` cannot hold a lone surrogate",
|
||||
),
|
||||
(
|
||||
r"'\N{BULLET}'",
|
||||
"literal_eval source",
|
||||
"`\\N{NAME}` needs the Unicode name table",
|
||||
),
|
||||
(
|
||||
"nested_150",
|
||||
"literal_eval",
|
||||
"deeper than MAX_DEPTH: rejected for stack safety, where CPython still parses it",
|
||||
),
|
||||
(
|
||||
"nested_150",
|
||||
"pickle.loads",
|
||||
"deeper than MAX_DEPTH: rejected for stack safety, where CPython has no limit",
|
||||
),
|
||||
(
|
||||
"2**64",
|
||||
"pickle.loads",
|
||||
"serde-pickle's serde interface stops at i64",
|
||||
),
|
||||
(
|
||||
"-(2**70)",
|
||||
"pickle.loads",
|
||||
"serde-pickle's serde interface stops at i64",
|
||||
),
|
||||
(
|
||||
"2**64",
|
||||
"pickle.dumps",
|
||||
"serde-pickle's serde interface stops at i64",
|
||||
),
|
||||
(
|
||||
"-(2**70)",
|
||||
"pickle.dumps",
|
||||
"serde-pickle's serde interface stops at i64",
|
||||
),
|
||||
(
|
||||
"2**64",
|
||||
"to_json",
|
||||
"serde_json has no exact form for integers beyond u64",
|
||||
),
|
||||
(
|
||||
"-(2**70)",
|
||||
"to_json",
|
||||
"serde_json has no exact form for integers beyond i64",
|
||||
),
|
||||
(
|
||||
"b''",
|
||||
"pickle.loads protocol 0",
|
||||
"protocols 0-2 pickle `b''` as a `bytes()` call",
|
||||
),
|
||||
(
|
||||
"b''",
|
||||
"pickle.loads protocol 1",
|
||||
"protocols 0-2 pickle `b''` as a `bytes()` call",
|
||||
),
|
||||
(
|
||||
"b''",
|
||||
"pickle.loads protocol 2",
|
||||
"protocols 0-2 pickle `b''` as a `bytes()` call",
|
||||
),
|
||||
];
|
||||
|
||||
/// Collects every mismatch so one run reports the whole divergence set.
|
||||
#[derive(Default)]
|
||||
struct Mismatches {
|
||||
unexpected: Vec<String>,
|
||||
known_seen: Vec<usize>,
|
||||
known_scope: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Mismatches {
|
||||
fn known(source: &str, what: &str) -> Option<usize> {
|
||||
KNOWN
|
||||
.iter()
|
||||
.position(|(known, prefix, _)| *known == source && what.starts_with(prefix))
|
||||
}
|
||||
|
||||
fn check(&mut self, row: &Row, what: &str, expected: &str, actual: &str) {
|
||||
self.check_source(&row.name, what, expected, actual);
|
||||
}
|
||||
|
||||
/// `name` identifies the fixture row in reports and in [`KNOWN`].
|
||||
fn check_source(&mut self, name: &str, what: &str, expected: &str, actual: &str) {
|
||||
let known = Self::known(name, what);
|
||||
if let Some(index) = known {
|
||||
self.known_scope.push(index);
|
||||
}
|
||||
if expected == actual {
|
||||
return;
|
||||
}
|
||||
match known {
|
||||
Some(index) => self.known_seen.push(index),
|
||||
None => self.unexpected.push(format!(
|
||||
"{name:?} [{what}]\n python: {expected}\n rust: {actual}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) {
|
||||
let resolved: Vec<_> = self
|
||||
.known_scope
|
||||
.iter()
|
||||
.filter(|index| !self.known_seen.contains(index))
|
||||
.map(|&index| format!("{} [{}]", KNOWN[index].0, KNOWN[index].1))
|
||||
.collect();
|
||||
assert!(
|
||||
self.unexpected.is_empty() && resolved.is_empty(),
|
||||
"{} mismatches with CPython:\n{}\nknown divergences that now match (delete them \
|
||||
from KNOWN): {resolved:?}",
|
||||
self.unexpected.len(),
|
||||
self.unexpected.join("\n"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_value(mismatches: &mut Mismatches, row: &Row, value: &Value) {
|
||||
mismatches.check(row, "repr", &row.repr, &repr(value));
|
||||
mismatches.check(row, "str", &row.str, &to_str(value));
|
||||
mismatches.check(
|
||||
row,
|
||||
"bool",
|
||||
&row.truthy.to_string(),
|
||||
&truthy(value).to_string(),
|
||||
);
|
||||
let expected = row.json.clone().or_else(|| {
|
||||
row.json_error
|
||||
.clone()
|
||||
.map(|error| format!("error: {error}"))
|
||||
});
|
||||
let actual = match json::dumps(value) {
|
||||
Ok(text) => text,
|
||||
Err(error) => format!("error: {error}"),
|
||||
};
|
||||
mismatches.check(
|
||||
row,
|
||||
"json.dumps",
|
||||
expected.as_deref().unwrap_or(""),
|
||||
&actual,
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn literal_rows_match_python_repr_str_bool_and_json(fixtures: &Fixtures) {
|
||||
let mut mismatches = Mismatches::default();
|
||||
for row in fixtures.rows.iter().filter(|row| row.literal) {
|
||||
match literal_eval(&row.repr) {
|
||||
Ok(value) => check_value(&mut mismatches, row, &value),
|
||||
Err(error) => mismatches.check(row, "literal_eval", &row.repr, &error.to_string()),
|
||||
}
|
||||
}
|
||||
mismatches.finish();
|
||||
}
|
||||
|
||||
/// Errors compare by outcome only: CPython's exception class is not part of the contract.
|
||||
#[rstest]
|
||||
fn literal_eval_matches_python_on_source_texts(fixtures: &Fixtures) {
|
||||
let mut mismatches = Mismatches::default();
|
||||
for case in &fixtures.sources {
|
||||
let expected = match (&case.repr, &case.error) {
|
||||
(Some(repr), None) => repr.clone(),
|
||||
(None, Some(_)) => "an error".to_owned(),
|
||||
_ => panic!("{:?}: a source records a repr or an error", case.source),
|
||||
};
|
||||
let actual = match literal_eval(&case.source) {
|
||||
Ok(value) => repr(&value),
|
||||
Err(_) => "an error".to_owned(),
|
||||
};
|
||||
mismatches.check_source(&case.name, "literal_eval source", &expected, &actual);
|
||||
}
|
||||
mismatches.finish();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn pickle_loads_matches_python_at_every_protocol(fixtures: &Fixtures) {
|
||||
let mut mismatches = Mismatches::default();
|
||||
for row in &fixtures.rows {
|
||||
let Some(pickles) = &row.pickle else { continue };
|
||||
for (protocol, data) in pickles {
|
||||
let data = hex::decode(data).expect("fixture pickle is hex");
|
||||
let what = format!("pickle.loads protocol {protocol}");
|
||||
match (pickle::loads(&data), row.plain) {
|
||||
(Ok(value), true) => {
|
||||
let view = row.view.as_deref().expect("picklable rows have a view");
|
||||
mismatches.check(row, &what, view, &repr(&value));
|
||||
}
|
||||
(Err(error), true) => {
|
||||
mismatches.check(row, &what, "a value", &format!("error: {error}"))
|
||||
}
|
||||
(Ok(value), false) => {
|
||||
mismatches.check(row, &what, "a class-reference error", &repr(&value))
|
||||
}
|
||||
(Err(pickle_error), false) => assert!(
|
||||
matches!(pickle_error, litellm_python_compat::Error::InvalidPickle(_)),
|
||||
"{}: {pickle_error}",
|
||||
row.source
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
mismatches.finish();
|
||||
}
|
||||
|
||||
/// Non-finite floats have no literal form; pickle is how Rust receives them.
|
||||
#[rstest]
|
||||
fn values_reached_only_through_pickle_match_python(fixtures: &Fixtures) {
|
||||
let mut mismatches = Mismatches::default();
|
||||
for row in fixtures
|
||||
.rows
|
||||
.iter()
|
||||
.filter(|row| !row.literal && row.plain && row.view.as_deref() == Some(&row.repr))
|
||||
{
|
||||
let data = hex::decode(&row.pickle.as_ref().expect("plain rows pickle")["5"])
|
||||
.expect("fixture pickle is hex");
|
||||
let value = pickle::loads(&data).expect("plain pickle decodes");
|
||||
check_value(&mut mismatches, row, &value);
|
||||
}
|
||||
mismatches.finish();
|
||||
}
|
||||
|
||||
/// Byte equality with CPython is not the contract: CPython adds memo opcodes and picks the
|
||||
/// smallest integer opcode. `scripts/verify_rust_pickles.py` checks that CPython reads these
|
||||
/// back; set `PYTHON_COMPAT_RUST_PICKLES` to a file path to export them.
|
||||
#[rstest]
|
||||
fn pickle_dumps_round_trips_every_plain_literal(fixtures: &Fixtures) {
|
||||
// Truncate up front: the verifier must read this run's rows and nothing else.
|
||||
let mut export = std::env::var_os("PYTHON_COMPAT_RUST_PICKLES")
|
||||
.map(|path| File::create(path).expect("the export path is writable"));
|
||||
let mut mismatches = Mismatches::default();
|
||||
for row in fixtures
|
||||
.rows
|
||||
.iter()
|
||||
.filter(|row| row.literal && row.plain && row.view.as_deref() == Some(&row.repr))
|
||||
{
|
||||
// Rows past `MAX_DEPTH` are covered by the literal test's own KNOWN entry.
|
||||
let Ok(value) = literal_eval(&row.repr) else {
|
||||
continue;
|
||||
};
|
||||
let data = match pickle::dumps(&value) {
|
||||
Ok(data) => data,
|
||||
Err(error) => {
|
||||
mismatches.check(row, "pickle.dumps", "a pickle", &format!("error: {error}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let decoded = pickle::loads(&data).expect("rust pickle decodes");
|
||||
mismatches.check(row, "pickle round trip", &row.repr, &repr(&decoded));
|
||||
if let Some(file) = &mut export {
|
||||
writeln!(file, "{}\t{}", hex::encode(&data), repr(&value))
|
||||
.expect("the export file is writable");
|
||||
}
|
||||
}
|
||||
mismatches.finish();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn to_json_matches_python_json_round_trip(fixtures: &Fixtures) {
|
||||
let mut mismatches = Mismatches::default();
|
||||
for row in fixtures.rows.iter().filter(|row| row.literal) {
|
||||
let Some(expected) = &row.json else { continue };
|
||||
let Ok(value) = literal_eval(&row.repr) else {
|
||||
continue;
|
||||
};
|
||||
let expected: serde_json::Value =
|
||||
serde_json::from_str(expected).expect("python json.dumps output parses");
|
||||
match json::to_json(&value) {
|
||||
Ok(actual) => {
|
||||
mismatches.check(row, "to_json", &expected.to_string(), &actual.to_string())
|
||||
}
|
||||
Err(error) => {
|
||||
mismatches.check(row, "to_json", &expected.to_string(), &error.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
mismatches.finish();
|
||||
}
|
||||
122
litellm-rust/crates/python-compat/tests/limits.rs
Normal file
122
litellm-rust/crates/python-compat/tests/limits.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
use litellm_python_compat::{Error, MAX_DEPTH, Value, json, literal::literal_eval, pickle};
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
/// The bracket pair of one container shape, as `(open, close)`.
|
||||
#[fixture]
|
||||
fn shapes() -> [(&'static str, &'static str); 3] {
|
||||
[("[", "]"), ("{'a': ", "}"), ("(", ",)")]
|
||||
}
|
||||
|
||||
fn nested_text(open: &str, close: &str, depth: usize) -> String {
|
||||
format!("{}1{}", open.repeat(depth), close.repeat(depth))
|
||||
}
|
||||
|
||||
fn nested_list(depth: usize) -> Value {
|
||||
(0..depth).fold(Value::from(1), |value, _| Value::List(vec![value]))
|
||||
}
|
||||
|
||||
/// A protocol 3 pickle of `depth` nested lists around `1`: `EMPTY_LIST` per level, then
|
||||
/// `BININT1 1`, then `APPEND` per level. Written by hand because `dumps` refuses the depth.
|
||||
fn nested_list_pickle(depth: usize) -> Vec<u8> {
|
||||
let mut data = vec![0x80, 3];
|
||||
data.extend(std::iter::repeat_n(b']', depth));
|
||||
data.extend([b'K', 1]);
|
||||
data.extend(std::iter::repeat_n(b'a', depth));
|
||||
data.push(b'.');
|
||||
data
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn literal_eval_accepts_the_limit_and_rejects_past_it(shapes: [(&'static str, &'static str); 3]) {
|
||||
for (open, close) in shapes {
|
||||
assert!(literal_eval(&nested_text(open, close, MAX_DEPTH)).is_ok());
|
||||
assert!(matches!(
|
||||
literal_eval(&nested_text(open, close, MAX_DEPTH + 1)),
|
||||
Err(Error::TooDeep)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// A backtracking parser (the `py_literal` grammar this replaced) doubles per nested level
|
||||
/// and takes minutes here; the bound is loose enough to survive a slow debug build.
|
||||
#[rstest]
|
||||
fn literal_eval_stays_linear_in_depth(shapes: [(&'static str, &'static str); 3]) {
|
||||
for (open, close) in shapes {
|
||||
let text = nested_text(open, close, MAX_DEPTH);
|
||||
let start = Instant::now();
|
||||
assert!(literal_eval(&text).is_ok());
|
||||
let elapsed = start.elapsed();
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(50),
|
||||
"{open} nested {MAX_DEPTH} deep took {elapsed:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn literal_eval_ignores_brackets_inside_strings() {
|
||||
let text = format!("'{}'", "[".repeat(MAX_DEPTH + 1));
|
||||
assert!(matches!(literal_eval(&text), Ok(Value::Str(_))));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn pickle_nesting_is_bounded_in_both_directions() {
|
||||
assert_eq!(
|
||||
pickle::loads(&nested_list_pickle(MAX_DEPTH)).unwrap(),
|
||||
nested_list(MAX_DEPTH)
|
||||
);
|
||||
assert!(matches!(
|
||||
pickle::loads(&nested_list_pickle(MAX_DEPTH + 1)),
|
||||
Err(Error::InvalidPickle(_))
|
||||
));
|
||||
assert!(pickle::dumps(&nested_list(MAX_DEPTH)).is_ok());
|
||||
assert!(matches!(
|
||||
pickle::dumps(&nested_list(MAX_DEPTH + 2)),
|
||||
Err(Error::TooDeep)
|
||||
));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("{1, 2}", "set")]
|
||||
#[case("1+2j", "complex")]
|
||||
fn pickle_dumps_refuses_types_it_would_change(#[case] source: &str, #[case] type_name: &str) {
|
||||
let value = literal_eval(source).expect("source is a literal");
|
||||
assert!(matches!(pickle::dumps(&value), Err(Error::NotPicklable(name)) if name == type_name));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(b"\x80\x02c__builtin__\ncomplex\nq\x00.".to_vec(), "a class reference")]
|
||||
#[case({ let mut data = pickle::dumps(&Value::from(1)).unwrap(); data.push(b'.'); data }, "trailing data")]
|
||||
fn pickle_loads_rejects(#[case] data: Vec<u8>, #[case] what: &str) {
|
||||
assert!(
|
||||
matches!(pickle::loads(&data), Err(Error::InvalidPickle(_))),
|
||||
"{what} must not decode"
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Value::Bytes(b"x".to_vec()), "Object of type bytes is not JSON serializable")]
|
||||
#[case(Value::Set(vec![Value::from(1)]), "Object of type set is not JSON serializable")]
|
||||
#[case(Value::Complex { re: 1.0, im: 2.0 }, "Object of type complex is not JSON serializable")]
|
||||
#[case(
|
||||
Value::Float(f64::NAN),
|
||||
"Out of range float values are not JSON compliant"
|
||||
)]
|
||||
fn to_json_reports_what_python_json_dumps_would_reject(
|
||||
#[case] value: Value,
|
||||
#[case] message: &str,
|
||||
) {
|
||||
let error = json::to_json(&value).expect_err("value has no serde_json form");
|
||||
assert_eq!(error.to_string(), message);
|
||||
}
|
||||
|
||||
/// `json.dumps` writes the non-finite floats that `to_json` cannot represent.
|
||||
#[rstest]
|
||||
#[case(f64::NAN, "NaN")]
|
||||
#[case(f64::INFINITY, "Infinity")]
|
||||
#[case(f64::NEG_INFINITY, "-Infinity")]
|
||||
fn json_dumps_writes_non_finite_floats(#[case] value: f64, #[case] text: &str) {
|
||||
assert_eq!(json::dumps(&Value::Float(value)).unwrap(), text);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue