Skip to content

Commit f6b14cc

Browse files
authored
make error "duplicate" cheaper (#950)
1 parent 6769140 commit f6b14cc

File tree

9 files changed

+62
-67
lines changed

9 files changed

+62
-67
lines changed

src/errors/line_error.rs

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ impl<'a> ValError<'a> {
6262
}
6363

6464
/// a bit like clone but change the lifetime to match py
65-
pub fn duplicate<'py>(&self, py: Python<'py>) -> ValError<'py> {
65+
pub fn into_owned(self, py: Python<'_>) -> ValError<'_> {
6666
match self {
67-
ValError::LineErrors(errors) => errors.iter().map(|e| e.duplicate(py)).collect::<Vec<_>>().into(),
68-
ValError::InternalErr(err) => ValError::InternalErr(err.clone_ref(py)),
67+
ValError::LineErrors(errors) => errors.into_iter().map(|e| e.into_owned(py)).collect::<Vec<_>>().into(),
68+
ValError::InternalErr(err) => ValError::InternalErr(err),
6969
ValError::Omit => ValError::Omit,
7070
ValError::UseDefault => ValError::UseDefault,
7171
}
@@ -129,28 +129,26 @@ impl<'a> ValLineError<'a> {
129129
self
130130
}
131131

132-
/// a bit like clone but change the lifetime to match py, used by ValError.duplicate above
133-
pub fn duplicate<'py>(&'a self, py: Python<'py>) -> ValLineError<'py> {
132+
/// a bit like clone but change the lifetime to match py, used by ValError.into_owned above
133+
pub fn into_owned(self, py: Python<'_>) -> ValLineError<'_> {
134134
ValLineError {
135-
error_type: self.error_type.clone(),
136-
input_value: InputValue::<'py>::from(self.input_value.to_object(py)),
137-
location: self.location.clone(),
135+
error_type: self.error_type,
136+
input_value: match self.input_value {
137+
InputValue::PyAny(input) => InputValue::PyAny(input.to_object(py).into_ref(py)),
138+
InputValue::JsonInput(input) => InputValue::JsonInput(input),
139+
InputValue::String(input) => InputValue::PyAny(input.to_object(py).into_ref(py)),
140+
},
141+
location: self.location,
138142
}
139143
}
140144
}
141145

142146
#[cfg_attr(debug_assertions, derive(Debug))]
147+
#[derive(Clone)]
143148
pub enum InputValue<'a> {
144149
PyAny(&'a PyAny),
145-
JsonInput(&'a JsonInput),
150+
JsonInput(JsonInput),
146151
String(&'a str),
147-
PyObject(PyObject),
148-
}
149-
150-
impl<'a> From<PyObject> for InputValue<'a> {
151-
fn from(py_object: PyObject) -> Self {
152-
Self::PyObject(py_object)
153-
}
154152
}
155153

156154
impl<'a> ToPyObject for InputValue<'a> {
@@ -159,7 +157,6 @@ impl<'a> ToPyObject for InputValue<'a> {
159157
Self::PyAny(input) => input.into_py(py),
160158
Self::JsonInput(input) => input.to_object(py),
161159
Self::String(input) => input.into_py(py),
162-
Self::PyObject(py_obj) => py_obj.into_py(py),
163160
}
164161
}
165162
}

src/errors/validation_exception.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use super::line_error::ValLineError;
2222
use super::location::Location;
2323
use super::types::{ErrorMode, ErrorType};
2424
use super::value_exception::PydanticCustomError;
25-
use super::ValError;
25+
use super::{InputValue, ValError};
2626

2727
#[pyclass(extends=PyValueError, module="pydantic_core._pydantic_core")]
2828
#[derive(Clone)]
@@ -128,11 +128,11 @@ fn get_url_prefix(py: Python, include_url: bool) -> Option<&str> {
128128
}
129129

130130
// used to convert a validation error back to ValError for wrap functions
131-
impl<'a> IntoPy<ValError<'a>> for ValidationError {
132-
fn into_py(self, py: Python) -> ValError<'a> {
131+
impl ValidationError {
132+
pub(crate) fn into_val_error(self, py: Python<'_>) -> ValError<'_> {
133133
self.line_errors
134134
.into_iter()
135-
.map(|e| e.into_py(py))
135+
.map(|e| e.into_val_line_error(py))
136136
.collect::<Vec<_>>()
137137
.into()
138138
}
@@ -322,13 +322,13 @@ impl<'a> IntoPy<PyLineError> for ValLineError<'a> {
322322
}
323323
}
324324

325-
/// opposite of above, used to extract line errors from a validation error for wrap functions
326-
impl<'a> IntoPy<ValLineError<'a>> for PyLineError {
327-
fn into_py(self, _py: Python) -> ValLineError<'a> {
325+
impl PyLineError {
326+
/// Used to extract line errors from a validation error for wrap functions
327+
fn into_val_line_error(self, py: Python<'_>) -> ValLineError<'_> {
328328
ValLineError {
329329
error_type: self.error_type,
330330
location: self.location,
331-
input_value: self.input_value.into(),
331+
input_value: InputValue::PyAny(self.input_value.into_ref(py)),
332332
}
333333
}
334334
}

src/input/input_json.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ impl<'a> Input<'a> for JsonInput {
3131
}
3232

3333
fn as_error_value(&'a self) -> InputValue<'a> {
34-
InputValue::JsonInput(self)
34+
// cloning JsonInput is cheap due to use of Arc
35+
InputValue::JsonInput(self.clone())
3536
}
3637

3738
fn is_none(&self) -> bool {
@@ -262,7 +263,7 @@ impl<'a> Input<'a> for JsonInput {
262263
JsonInput::String(s) => Ok(string_to_vec(s).into()),
263264
JsonInput::Object(object) => {
264265
// return keys iterator to match python's behavior
265-
let keys: Vec<JsonInput> = object.keys().map(|k| JsonInput::String(k.clone())).collect();
266+
let keys: JsonArray = JsonArray::new(object.keys().map(|k| JsonInput::String(k.clone())).collect());
266267
Ok(keys.into())
267268
}
268269
_ => Err(ValError::new(ErrorTypeDefaults::IterableType, self)),
@@ -550,5 +551,5 @@ impl<'a> Input<'a> for String {
550551
}
551552

552553
fn string_to_vec(s: &str) -> JsonArray {
553-
s.chars().map(|c| JsonInput::String(c.to_string())).collect()
554+
JsonArray::new(s.chars().map(|c| JsonInput::String(c.to_string())).collect())
554555
}

src/input/parse_json.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::fmt;
2+
use std::sync::Arc;
23

34
use num_bigint::BigInt;
45
use pyo3::prelude::*;
56
use pyo3::types::{PyDict, PyList};
67
use serde::de::{Deserialize, DeserializeSeed, Error as SerdeError, MapAccess, SeqAccess, Visitor};
8+
use smallvec::SmallVec;
79

810
use crate::lazy_index_map::LazyIndexMap;
911

@@ -20,8 +22,8 @@ pub enum JsonInput {
2022
Array(JsonArray),
2123
Object(JsonObject),
2224
}
23-
pub type JsonArray = Vec<JsonInput>;
24-
pub type JsonObject = LazyIndexMap<String, JsonInput>;
25+
pub type JsonArray = Arc<SmallVec<[JsonInput; 8]>>;
26+
pub type JsonObject = Arc<LazyIndexMap<String, JsonInput>>;
2527

2628
impl ToPyObject for JsonInput {
2729
fn to_object(&self, py: Python<'_>) -> PyObject {
@@ -111,13 +113,13 @@ impl<'de> Deserialize<'de> for JsonInput {
111113
where
112114
V: SeqAccess<'de>,
113115
{
114-
let mut vec = Vec::new();
116+
let mut vec = SmallVec::new();
115117

116118
while let Some(elem) = visitor.next_element()? {
117119
vec.push(elem);
118120
}
119121

120-
Ok(JsonInput::Array(vec))
122+
Ok(JsonInput::Array(JsonArray::new(vec)))
121123
}
122124

123125
fn visit_map<V>(self, mut visitor: V) -> Result<JsonInput, V::Error>
@@ -171,9 +173,9 @@ impl<'de> Deserialize<'de> for JsonInput {
171173
while let Some((key, value)) = visitor.next_entry()? {
172174
values.insert(key, value);
173175
}
174-
Ok(JsonInput::Object(values))
176+
Ok(JsonInput::Object(Arc::new(values)))
175177
}
176-
None => Ok(JsonInput::Object(LazyIndexMap::new())),
178+
None => Ok(JsonInput::Object(Arc::new(LazyIndexMap::new()))),
177179
}
178180
}
179181
}

src/input/return_enums.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -625,8 +625,8 @@ impl GenericPyIterator {
625625
}
626626
}
627627

628-
pub fn input<'a>(&'a self, py: Python<'a>) -> &'a PyAny {
629-
self.obj.as_ref(py)
628+
pub fn input_as_error_value<'py>(&self, py: Python<'py>) -> InputValue<'py> {
629+
InputValue::PyAny(self.obj.clone_ref(py).into_ref(py))
630630
}
631631

632632
pub fn index(&self) -> usize {
@@ -654,9 +654,8 @@ impl GenericJsonIterator {
654654
}
655655
}
656656

657-
pub fn input<'a>(&'a self, py: Python<'a>) -> &'a PyAny {
658-
let input = JsonInput::Array(self.array.clone());
659-
input.to_object(py).into_ref(py)
657+
pub fn input_as_error_value<'py>(&self, _py: Python<'py>) -> InputValue<'py> {
658+
InputValue::JsonInput(JsonInput::Array(self.array.clone()))
660659
}
661660

662661
pub fn index(&self) -> usize {

src/lazy_index_map.rs

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,36 @@
11
use std::borrow::Borrow;
2-
use std::cell::RefCell;
32
use std::cmp::{Eq, PartialEq};
43
use std::fmt::Debug;
54
use std::hash::Hash;
65
use std::slice::Iter as SliceIter;
6+
use std::sync::OnceLock;
77

88
use ahash::AHashMap;
9+
use smallvec::SmallVec;
910

1011
#[derive(Debug, Clone, Default)]
1112
pub struct LazyIndexMap<K, V> {
12-
vec: Vec<(K, V)>,
13-
map: RefCell<Option<AHashMap<K, usize>>>,
13+
vec: SmallVec<[(K, V); 8]>,
14+
map: OnceLock<AHashMap<K, usize>>,
1415
}
1516

1617
/// Like [IndexMap](https://docs.rs/indexmap/latest/indexmap/) but only builds the lookup map when it's needed.
1718
impl<K, V> LazyIndexMap<K, V>
1819
where
1920
K: Clone + Debug + Eq + Hash,
20-
V: Clone + Debug,
21+
V: Debug,
2122
{
2223
pub fn new() -> Self {
2324
Self {
24-
vec: Vec::new(),
25-
map: RefCell::new(None),
25+
vec: SmallVec::new(),
26+
map: OnceLock::new(),
2627
}
2728
}
2829

2930
pub fn insert(&mut self, key: K, value: V) {
31+
if let Some(map) = self.map.get_mut() {
32+
map.insert(key.clone(), self.vec.len());
33+
}
3034
self.vec.push((key, value));
3135
}
3236

@@ -39,22 +43,14 @@ where
3943
K: Borrow<Q> + PartialEq<Q>,
4044
Q: Hash + Eq,
4145
{
42-
let mut map = self.map.borrow_mut();
43-
if let Some(map) = map.as_ref() {
44-
map.get(key).map(|&i| &self.vec[i].1)
45-
} else {
46-
let mut new_map = AHashMap::with_capacity(self.vec.len());
47-
let mut value = None;
48-
// reverse here so the last value is the one that's returned
49-
for (index, (k, v)) in self.vec.iter().enumerate().rev() {
50-
if value.is_none() && k == key {
51-
value = Some(v);
52-
}
53-
new_map.insert(k.clone(), index);
54-
}
55-
*map = Some(new_map);
56-
value
57-
}
46+
let map = self.map.get_or_init(|| {
47+
self.vec
48+
.iter()
49+
.enumerate()
50+
.map(|(index, (key, _))| (key.clone(), index))
51+
.collect()
52+
});
53+
map.get(key).map(|&i| &self.vec[i].1)
5854
}
5955

6056
pub fn keys(&self) -> impl Iterator<Item = &K> {

src/validators/function.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ pub fn convert_err<'a>(py: Python<'a>, err: PyErr, input: &'a impl Input<'a>) ->
504504
} else if let Ok(pydantic_error_type) = err.value(py).extract::<PydanticKnownError>() {
505505
pydantic_error_type.into_val_error(input)
506506
} else if let Ok(validation_error) = err.value(py).extract::<ValidationError>() {
507-
validation_error.into_py(py)
507+
validation_error.into_val_error(py)
508508
} else {
509509
py_err_string!(err.value(py), ValueError, input)
510510
}

src/validators/generator.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,14 @@ impl ValidatorIterator {
127127
Some(validator) => {
128128
if let Some(max_length) = max_length {
129129
if index >= max_length {
130-
let val_error = ValError::new(
130+
let val_error = ValError::new_custom_input(
131131
ErrorType::TooLong {
132132
field_type: "Generator".to_string(),
133133
max_length,
134134
actual_length: index + 1,
135135
context: None,
136136
},
137-
$iter.input(py),
137+
$iter.input_as_error_value(py),
138138
);
139139
return Err(ValidationError::from_val_error(
140140
py,
@@ -153,14 +153,14 @@ impl ValidatorIterator {
153153
None => {
154154
if let Some(min_length) = min_length {
155155
if $iter.index() < min_length {
156-
let val_error = ValError::new(
156+
let val_error = ValError::new_custom_input(
157157
ErrorType::TooShort {
158158
field_type: "Generator".to_string(),
159159
min_length,
160160
actual_length: $iter.index(),
161161
context: None,
162162
},
163-
$iter.input(py),
163+
$iter.input_as_error_value(py),
164164
);
165165
return Err(ValidationError::from_val_error(
166166
py,

src/validators/json.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ impl Validator for JsonValidator {
5555
match self.validator {
5656
Some(ref validator) => match validator.validate(py, &json_value, state) {
5757
Ok(v) => Ok(v),
58-
Err(err) => Err(err.duplicate(py)),
58+
Err(err) => Err(err.into_owned(py)),
5959
},
6060
None => Ok(json_value.to_object(py)),
6161
}

0 commit comments

Comments
 (0)