Skip to content

Commit 2c9ac6d

Browse files
committed
Implement support for by_alias and by_name for model validation functions
* This approach (2nd attempt) emphasizes building lookup keys at schema build time for performance reasons * We avoid any LookupKey builds at validation time to avoid perf bottlenecks + redundant builds * We store potentially up to 3 LookupKey instances via LookupKeyCollection, representing name, alias, and alias_then_name lookups based on the combination of config and runtime alias settings. * Adding parametrized tests to check various alias config / runtime setting combinations
1 parent 490ab36 commit 2c9ac6d

File tree

15 files changed

+375
-219
lines changed

15 files changed

+375
-219
lines changed

benches/main.rs

Lines changed: 65 additions & 55 deletions
Large diffs are not rendered by default.

src/lookup_key.rs

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -578,26 +578,46 @@ fn py_get_attrs<'py>(obj: &Bound<'py, PyAny>, attr_name: &Py<PyString>) -> PyRes
578578
}
579579
}
580580

581-
pub fn get_lookup_key(
582-
py: Python,
583-
validation_alias: Option<&Py<PyAny>>,
584-
validate_by_name: bool,
585-
validate_by_alias: bool,
586-
field_name: &str,
587-
) -> PyResult<LookupKey> {
588-
match validation_alias {
589-
Some(va) => {
590-
let va_bound = va.into_bound_py_any(py)?;
591-
let lookup_key = match (validate_by_name, validate_by_alias) {
592-
(true, true) => LookupKey::from_py(py, &va_bound, Some(field_name))?,
593-
(true, false) => LookupKey::from_string(py, field_name),
594-
(false, true) => LookupKey::from_py(py, &va_bound, None)?,
595-
(false, false) => {
596-
return py_schema_err!("`validate_by_name` and `validate_by_alias` cannot both be set to `False`.")
597-
}
598-
};
599-
Ok(lookup_key)
581+
#[derive(Debug)]
582+
#[allow(clippy::struct_field_names)]
583+
pub struct LookupKeyCollection {
584+
by_name: LookupKey,
585+
by_alias: Option<LookupKey>,
586+
by_alias_then_name: Option<LookupKey>,
587+
}
588+
589+
impl LookupKeyCollection {
590+
pub fn new(py: Python, validation_alias: Option<Bound<'_, PyAny>>, field_name: &str) -> PyResult<Self> {
591+
let by_name = LookupKey::from_string(py, field_name);
592+
593+
if let Some(va) = validation_alias {
594+
let by_alias = Some(LookupKey::from_py(py, &va, None)?);
595+
let by_alias_then_name = Some(LookupKey::from_py(py, &va, Some(field_name))?);
596+
Ok(Self {
597+
by_name,
598+
by_alias,
599+
by_alias_then_name,
600+
})
601+
} else {
602+
Ok(Self {
603+
by_name,
604+
by_alias: None,
605+
by_alias_then_name: None,
606+
})
600607
}
601-
None => Ok(LookupKey::from_string(py, field_name)),
608+
}
609+
610+
pub fn select(&self, validate_by_alias: bool, validate_by_name: bool) -> PyResult<&LookupKey> {
611+
let lookup_key_selection = match (validate_by_alias, validate_by_name) {
612+
(true, true) => self.by_alias_then_name.as_ref().unwrap_or(&self.by_name),
613+
(true, false) => self.by_alias.as_ref().unwrap_or(&self.by_name),
614+
(false, true) => &self.by_name,
615+
(false, false) => {
616+
// Note: we shouldn't hit this branch much, as this is enforced in `pydantic` with a `PydanticUserError`
617+
// at config creation time / validation function call time.
618+
return py_schema_err!("`validate_by_name` and `validate_by_alias` cannot both be set to `False`.");
619+
}
620+
};
621+
Ok(lookup_key_selection)
602622
}
603623
}

src/url.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ impl PyMultiHostUrl {
400400
username: username.map(Into::into),
401401
password: password.map(Into::into),
402402
host: host.map(Into::into),
403-
port: port.map(Into::into),
403+
port,
404404
};
405405
format!("{scheme}://{url_host}")
406406
} else {

src/validators/arguments.rs

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::build_tools::py_schema_err;
1111
use crate::build_tools::{schema_or_config_same, ExtraBehavior};
1212
use crate::errors::{ErrorTypeDefaults, ValError, ValLineError, ValResult};
1313
use crate::input::{Arguments, BorrowInput, Input, KeywordArgs, PositionalArgs, ValidationMatch};
14-
use crate::lookup_key::get_lookup_key;
14+
use crate::lookup_key::LookupKeyCollection;
1515
use crate::tools::SchemaDict;
1616

1717
use super::validation_state::ValidationState;
@@ -44,7 +44,7 @@ struct Parameter {
4444
name: String,
4545
kwarg_key: Option<Py<PyString>>,
4646
validator: CombinedValidator,
47-
alias: Option<Py<PyAny>>,
47+
lookup_key_collection: LookupKeyCollection,
4848
mode: String,
4949
}
5050

@@ -57,8 +57,8 @@ pub struct ArgumentsValidator {
5757
var_kwargs_validator: Option<Box<CombinedValidator>>,
5858
loc_by_alias: bool,
5959
extra: ExtraBehavior,
60-
validate_by_alias: bool,
61-
validate_by_name: bool,
60+
validate_by_alias: Option<bool>,
61+
validate_by_name: Option<bool>,
6262
}
6363

6464
impl BuildValidator for ArgumentsValidator {
@@ -126,12 +126,16 @@ impl BuildValidator for ArgumentsValidator {
126126
} else if has_default {
127127
had_default_arg = true;
128128
}
129+
130+
let validation_alias = arg.get_item(intern!(py, "alias"))?;
131+
let lookup_key_collection = LookupKeyCollection::new(py, validation_alias, name.as_str())?;
132+
129133
parameters.push(Parameter {
130134
positional,
131135
name,
132136
kwarg_key,
133137
validator,
134-
alias: arg.get_item(intern!(py, "alias"))?.map(std::convert::Into::into),
138+
lookup_key_collection,
135139
mode: mode.to_string(),
136140
});
137141
}
@@ -163,8 +167,8 @@ impl BuildValidator for ArgumentsValidator {
163167
var_kwargs_validator,
164168
loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true),
165169
extra: ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Forbid)?,
166-
validate_by_alias: schema_or_config_same(schema, config, intern!(py, "validate_by_alias"))?.unwrap_or(true),
167-
validate_by_name: schema_or_config_same(schema, config, intern!(py, "validate_by_name"))?.unwrap_or(false),
170+
validate_by_alias: schema_or_config_same(schema, config, intern!(py, "validate_by_alias"))?,
171+
validate_by_name: schema_or_config_same(schema, config, intern!(py, "validate_by_name"))?,
168172
}
169173
.into())
170174
}
@@ -193,7 +197,7 @@ impl Validator for ArgumentsValidator {
193197
let mut output_args: Vec<PyObject> = Vec::with_capacity(self.positional_params_count);
194198
let output_kwargs = PyDict::new(py);
195199
let mut errors: Vec<ValLineError> = Vec::new();
196-
let mut used_kwargs: AHashSet<String> = AHashSet::with_capacity(self.parameters.len());
200+
let mut used_kwargs: AHashSet<&str> = AHashSet::with_capacity(self.parameters.len());
197201

198202
let validate_by_alias = state.validate_by_alias_or(self.validate_by_alias);
199203
let validate_by_name = state.validate_by_name_or(self.validate_by_name);
@@ -209,19 +213,17 @@ impl Validator for ArgumentsValidator {
209213
let mut kw_value = None;
210214
let mut kw_lookup_key = None;
211215
if parameter.mode == "keyword_only" || parameter.mode == "positional_or_keyword" {
212-
kw_lookup_key = Some(get_lookup_key(
213-
py,
214-
parameter.alias.as_ref(),
215-
validate_by_name,
216-
validate_by_alias,
217-
&parameter.name,
218-
)?);
216+
kw_lookup_key = Some(
217+
parameter
218+
.lookup_key_collection
219+
.select(validate_by_alias, validate_by_name)?,
220+
);
219221
}
220222

221223
if let Some(kwargs) = args.kwargs() {
222-
if let Some(ref lookup_key) = kw_lookup_key {
224+
if let Some(lookup_key) = kw_lookup_key {
223225
if let Some((lookup_path, value)) = kwargs.get_item(lookup_key)? {
224-
used_kwargs.insert(lookup_path.first_key().to_string());
226+
used_kwargs.insert(lookup_path.first_key());
225227
kw_value = Some((lookup_path, value));
226228
}
227229
}

src/validators/dataclass.rs

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValLineError, ValRes
1212
use crate::input::{
1313
input_as_python_instance, Arguments, BorrowInput, Input, InputType, KeywordArgs, PositionalArgs, ValidationMatch,
1414
};
15-
use crate::lookup_key::get_lookup_key;
15+
use crate::lookup_key::LookupKeyCollection;
1616
use crate::tools::SchemaDict;
1717
use crate::validators::function::convert_err;
1818

@@ -27,7 +27,7 @@ struct Field {
2727
py_name: Py<PyString>,
2828
init: bool,
2929
init_only: bool,
30-
alias: Option<Py<PyAny>>,
30+
lookup_key_collection: LookupKeyCollection,
3131
validator: CombinedValidator,
3232
frozen: bool,
3333
}
@@ -42,8 +42,8 @@ pub struct DataclassArgsValidator {
4242
extra_behavior: ExtraBehavior,
4343
extras_validator: Option<Box<CombinedValidator>>,
4444
loc_by_alias: bool,
45-
validate_by_alias: bool,
46-
validate_by_name: bool,
45+
validate_by_alias: Option<bool>,
46+
validate_by_name: Option<bool>,
4747
}
4848

4949
impl BuildValidator for DataclassArgsValidator {
@@ -93,13 +93,14 @@ impl BuildValidator for DataclassArgsValidator {
9393
positional_count += 1;
9494
}
9595

96+
let validation_alias = field.get_item(intern!(py, "validation_alias"))?;
97+
let lookup_key_collection = LookupKeyCollection::new(py, validation_alias, name.as_str())?;
98+
9699
fields.push(Field {
97100
kw_only,
98101
name,
99102
py_name: py_name.into(),
100-
alias: field
101-
.get_item(intern!(py, "validation_alias"))?
102-
.map(std::convert::Into::into),
103+
lookup_key_collection,
103104
validator,
104105
init: field.get_as(intern!(py, "init"))?.unwrap_or(true),
105106
init_only: field.get_as(intern!(py, "init_only"))?.unwrap_or(false),
@@ -124,8 +125,8 @@ impl BuildValidator for DataclassArgsValidator {
124125
extra_behavior,
125126
extras_validator,
126127
loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true),
127-
validate_by_alias: config.get_as(intern!(py, "validate_by_alias"))?.unwrap_or(true),
128-
validate_by_name: config.get_as(intern!(py, "validate_by_name"))?.unwrap_or(false),
128+
validate_by_alias: config.get_as(intern!(py, "validate_by_alias"))?,
129+
validate_by_name: config.get_as(intern!(py, "validate_by_name"))?,
129130
}
130131
.into())
131132
}
@@ -151,7 +152,7 @@ impl Validator for DataclassArgsValidator {
151152
let mut init_only_args = self.init_only_count.map(Vec::with_capacity);
152153

153154
let mut errors: Vec<ValLineError> = Vec::new();
154-
let mut used_keys: AHashSet<String> = AHashSet::with_capacity(self.fields.len());
155+
let mut used_keys: AHashSet<&str> = AHashSet::with_capacity(self.fields.len());
155156

156157
let state = &mut state.rebind_extra(|extra| extra.data = Some(output_dict.clone()));
157158

@@ -200,17 +201,14 @@ impl Validator for DataclassArgsValidator {
200201
}
201202
}
202203

203-
let lookup_key = get_lookup_key(
204-
py,
205-
field.alias.as_ref(),
206-
validate_by_name,
207-
validate_by_alias,
208-
&field.name,
209-
)?;
204+
let lookup_key = field
205+
.lookup_key_collection
206+
.select(validate_by_alias, validate_by_name)?;
207+
210208
let mut kw_value = None;
211209
if let Some(kwargs) = args.kwargs() {
212-
if let Some((lookup_path, value)) = kwargs.get_item(&lookup_key)? {
213-
used_keys.insert(lookup_path.first_key().to_string());
210+
if let Some((lookup_path, value)) = kwargs.get_item(lookup_key)? {
211+
used_keys.insert(lookup_path.first_key());
214212
kw_value = Some((lookup_path, value));
215213
}
216214
}

src/validators/model_fields.rs

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ use crate::errors::LocItem;
1212
use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValLineError, ValResult};
1313
use crate::input::ConsumeIterator;
1414
use crate::input::{BorrowInput, Input, ValidatedDict, ValidationMatch};
15-
use crate::lookup_key::get_lookup_key;
15+
use crate::lookup_key::LookupKeyCollection;
1616
use crate::tools::SchemaDict;
1717

1818
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};
1919

2020
#[derive(Debug)]
2121
struct Field {
2222
name: String,
23-
alias: Option<Py<PyAny>>,
23+
lookup_key_collection: LookupKeyCollection,
2424
name_py: Py<PyString>,
2525
validator: CombinedValidator,
2626
frozen: bool,
@@ -37,8 +37,8 @@ pub struct ModelFieldsValidator {
3737
strict: bool,
3838
from_attributes: bool,
3939
loc_by_alias: bool,
40-
validate_by_alias: bool,
41-
validate_by_name: bool,
40+
validate_by_alias: Option<bool>,
41+
validate_by_name: Option<bool>,
4242
}
4343

4444
impl BuildValidator for ModelFieldsValidator {
@@ -52,9 +52,11 @@ impl BuildValidator for ModelFieldsValidator {
5252
let py = schema.py();
5353

5454
let strict = is_strict(schema, config)?;
55-
let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?;
55+
5656
let from_attributes = schema_or_config_same(schema, config, intern!(py, "from_attributes"))?.unwrap_or(false);
5757

58+
let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?;
59+
5860
let extras_validator = match (schema.get_item(intern!(py, "extras_schema"))?, &extra_behavior) {
5961
(Some(v), ExtraBehavior::Allow) => Some(Box::new(build_validator(&v, config, definitions)?)),
6062
(Some(_), _) => return py_schema_err!("extras_schema can only be used if extra_behavior=allow"),
@@ -79,11 +81,12 @@ impl BuildValidator for ModelFieldsValidator {
7981
Err(err) => return py_schema_err!("Field \"{}\":\n {}", field_name, err),
8082
};
8183

84+
let validation_alias = field_info.get_item(intern!(py, "validation_alias"))?;
85+
let lookup_key_collection = LookupKeyCollection::new(py, validation_alias, field_name)?;
86+
8287
fields.push(Field {
8388
name: field_name.to_string(),
84-
alias: field_info
85-
.get_item(intern!(py, "validation_alias"))?
86-
.map(std::convert::Into::into),
89+
lookup_key_collection,
8790
name_py: field_name_py.into(),
8891
validator,
8992
frozen: field_info.get_as::<bool>(intern!(py, "frozen"))?.unwrap_or(false),
@@ -98,8 +101,8 @@ impl BuildValidator for ModelFieldsValidator {
98101
strict,
99102
from_attributes,
100103
loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true),
101-
validate_by_alias: config.get_as(intern!(py, "validate_by_alias"))?.unwrap_or(true),
102-
validate_by_name: config.get_as(intern!(py, "validate_by_name"))?.unwrap_or(false),
104+
validate_by_alias: config.get_as(intern!(py, "validate_by_alias"))?,
105+
validate_by_name: config.get_as(intern!(py, "validate_by_name"))?,
103106
}
104107
.into())
105108
}
@@ -157,7 +160,7 @@ impl Validator for ModelFieldsValidator {
157160

158161
// we only care about which keys have been used if we're iterating over the object for extra after
159162
// the first pass
160-
let mut used_keys: Option<AHashSet<String>> =
163+
let mut used_keys: Option<AHashSet<&str>> =
161164
if self.extra_behavior == ExtraBehavior::Ignore || dict.is_py_get_attr() {
162165
None
163166
} else {
@@ -168,14 +171,10 @@ impl Validator for ModelFieldsValidator {
168171
let state = &mut state.rebind_extra(|extra| extra.data = Some(model_dict.clone()));
169172

170173
for field in &self.fields {
171-
let lookup_key = get_lookup_key(
172-
py,
173-
field.alias.as_ref(),
174-
validate_by_name,
175-
validate_by_alias,
176-
&field.name,
177-
)?;
178-
let op_key_value = match dict.get_item(&lookup_key) {
174+
let lookup_key = field
175+
.lookup_key_collection
176+
.select(validate_by_alias, validate_by_name)?;
177+
let op_key_value = match dict.get_item(lookup_key) {
179178
Ok(v) => v,
180179
Err(ValError::LineErrors(line_errors)) => {
181180
for err in line_errors {
@@ -189,7 +188,7 @@ impl Validator for ModelFieldsValidator {
189188
if let Some(ref mut used_keys) = used_keys {
190189
// key is "used" whether or not validation passes, since we want to skip this key in
191190
// extra logic either way
192-
used_keys.insert(lookup_path.first_key().to_string());
191+
used_keys.insert(lookup_path.first_key());
193192
}
194193
match field.validator.validate(py, value.borrow_input(), state) {
195194
Ok(value) => {
@@ -240,7 +239,7 @@ impl Validator for ModelFieldsValidator {
240239
if let Some(used_keys) = used_keys {
241240
struct ValidateToModelExtra<'a, 's, 'py> {
242241
py: Python<'py>,
243-
used_keys: AHashSet<String>,
242+
used_keys: AHashSet<&'a str>,
244243
errors: &'a mut Vec<ValLineError>,
245244
fields_set_vec: &'a mut Vec<Py<PyString>>,
246245
extra_behavior: ExtraBehavior,

0 commit comments

Comments
 (0)