Skip to content

Commit 490ab36

Browse files
committed
First pass at implementing support for by_alias and by_name in model validate functions
This approach has now been reverted in favor of a schema-build-time approach due to perf reasons See the next commit for a more robust explanation :).
1 parent 772f706 commit 490ab36

File tree

11 files changed

+215
-87
lines changed

11 files changed

+215
-87
lines changed

python/pydantic_core/_pydantic_core.pyi

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ class SchemaValidator:
9797
context: Any | None = None,
9898
self_instance: Any | None = None,
9999
allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False,
100+
by_alias: bool | None = None,
101+
by_name: bool | None = None,
100102
) -> Any:
101103
"""
102104
Validate a Python object against the schema and return the validated object.
@@ -114,6 +116,8 @@ class SchemaValidator:
114116
allow_partial: Whether to allow partial validation; if `True` errors in the last element of sequences
115117
and mappings are ignored.
116118
`'trailing-strings'` means any final unfinished JSON string is included in the result.
119+
by_alias: Whether to use the field's alias to match the input data to an attribute.
120+
by_name: Whether to use the field's name to match the input data to an attribute.
117121
118122
Raises:
119123
ValidationError: If validation fails.
@@ -130,6 +134,8 @@ class SchemaValidator:
130134
from_attributes: bool | None = None,
131135
context: Any | None = None,
132136
self_instance: Any | None = None,
137+
by_alias: bool | None = None,
138+
by_name: bool | None = None,
133139
) -> bool:
134140
"""
135141
Similar to [`validate_python()`][pydantic_core.SchemaValidator.validate_python] but returns a boolean.
@@ -148,6 +154,8 @@ class SchemaValidator:
148154
context: Any | None = None,
149155
self_instance: Any | None = None,
150156
allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False,
157+
by_alias: bool | None = None,
158+
by_name: bool | None = None,
151159
) -> Any:
152160
"""
153161
Validate JSON data directly against the schema and return the validated Python object.
@@ -168,6 +176,8 @@ class SchemaValidator:
168176
allow_partial: Whether to allow partial validation; if `True` incomplete JSON will be parsed successfully
169177
and errors in the last element of sequences and mappings are ignored.
170178
`'trailing-strings'` means any final unfinished JSON string is included in the result.
179+
by_alias: Whether to use the field's alias to match the input data to an attribute.
180+
by_name: Whether to use the field's name to match the input data to an attribute.
171181
172182
Raises:
173183
ValidationError: If validation fails or if the JSON data is invalid.
@@ -183,6 +193,8 @@ class SchemaValidator:
183193
strict: bool | None = None,
184194
context: Any | None = None,
185195
allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False,
196+
by_alias: bool | None = None,
197+
by_name: bool | None = None,
186198
) -> Any:
187199
"""
188200
Validate a string against the schema and return the validated Python object.
@@ -199,6 +211,8 @@ class SchemaValidator:
199211
allow_partial: Whether to allow partial validation; if `True` errors in the last element of sequences
200212
and mappings are ignored.
201213
`'trailing-strings'` means any final unfinished JSON string is included in the result.
214+
by_alias: Whether to use the field's alias to match the input data to an attribute.
215+
by_name: Whether to use the field's name to match the input data to an attribute.
202216
203217
Raises:
204218
ValidationError: If validation fails or if the JSON data is invalid.
@@ -216,6 +230,8 @@ class SchemaValidator:
216230
strict: bool | None = None,
217231
from_attributes: bool | None = None,
218232
context: Any | None = None,
233+
by_alias: bool | None = None,
234+
by_name: bool | None = None,
219235
) -> dict[str, Any] | tuple[dict[str, Any], dict[str, Any] | None, set[str]]:
220236
"""
221237
Validate an assignment to a field on a model.
@@ -230,6 +246,8 @@ class SchemaValidator:
230246
If `None`, the value of [`CoreConfig.from_attributes`][pydantic_core.core_schema.CoreConfig] is used.
231247
context: The context to use for validation, this is passed to functional validators as
232248
[`info.context`][pydantic_core.core_schema.ValidationInfo.context].
249+
by_alias: Whether to use the field's alias to match the input data to an attribute.
250+
by_name: Whether to use the field's name to match the input data to an attribute.
233251
234252
Raises:
235253
ValidationError: If validation fails.

src/lookup_key.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -580,19 +580,24 @@ fn py_get_attrs<'py>(obj: &Bound<'py, PyAny>, attr_name: &Py<PyString>) -> PyRes
580580

581581
pub fn get_lookup_key(
582582
py: Python,
583-
validation_alias: Option<Bound<'_, PyAny>>,
583+
validation_alias: Option<&Py<PyAny>>,
584584
validate_by_name: bool,
585585
validate_by_alias: bool,
586586
field_name: &str,
587587
) -> PyResult<LookupKey> {
588-
let lookup_key = match (validation_alias, validate_by_name, validate_by_alias) {
589-
(Some(va), true, true) => LookupKey::from_py(py, &va, Some(field_name))?,
590-
(Some(_va), true, false) => LookupKey::from_string(py, field_name),
591-
(Some(va), false, true) => LookupKey::from_py(py, &va, None)?,
592-
(Some(_va), false, false) => {
593-
return py_schema_err!("`validate_by_name` and `validate_by_alias` cannot both be set to `False`.")
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)
594600
}
595-
(None, _, _) => LookupKey::from_string(py, field_name),
596-
};
597-
Ok(lookup_key)
601+
None => Ok(LookupKey::from_string(py, field_name)),
602+
}
598603
}

src/url.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ impl PyUrl {
4545
pub fn py_new(py: Python, url: &Bound<'_, PyAny>) -> PyResult<Self> {
4646
let schema_obj = SCHEMA_DEFINITION_URL
4747
.get_or_init(py, || build_schema_validator(py, "url"))
48-
.validate_python(py, url, None, None, None, None, false.into())?;
48+
.validate_python(py, url, None, None, None, None, false.into(), None, None)?;
4949
schema_obj.extract(py)
5050
}
5151

@@ -225,7 +225,7 @@ impl PyMultiHostUrl {
225225
pub fn py_new(py: Python, url: &Bound<'_, PyAny>) -> PyResult<Self> {
226226
let schema_obj = SCHEMA_DEFINITION_MULTI_HOST_URL
227227
.get_or_init(py, || build_schema_validator(py, "multi-host-url"))
228-
.validate_python(py, url, None, None, None, None, false.into())?;
228+
.validate_python(py, url, None, None, None, None, false.into(), None, None)?;
229229
schema_obj.extract(py)
230230
}
231231

src/validators/arguments.rs

Lines changed: 32 additions & 24 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, LookupKey};
14+
use crate::lookup_key::get_lookup_key;
1515
use crate::tools::SchemaDict;
1616

1717
use super::validation_state::ValidationState;
@@ -42,9 +42,10 @@ impl FromStr for VarKwargsMode {
4242
struct Parameter {
4343
positional: bool,
4444
name: String,
45-
kw_lookup_key: Option<LookupKey>,
4645
kwarg_key: Option<Py<PyString>>,
4746
validator: CombinedValidator,
47+
alias: Option<Py<PyAny>>,
48+
mode: String,
4849
}
4950

5051
#[derive(Debug)]
@@ -56,6 +57,8 @@ pub struct ArgumentsValidator {
5657
var_kwargs_validator: Option<Box<CombinedValidator>>,
5758
loc_by_alias: bool,
5859
extra: ExtraBehavior,
60+
validate_by_alias: bool,
61+
validate_by_name: bool,
5962
}
6063

6164
impl BuildValidator for ArgumentsValidator {
@@ -75,10 +78,6 @@ impl BuildValidator for ArgumentsValidator {
7578
let mut had_default_arg = false;
7679
let mut had_keyword_only = false;
7780

78-
let validate_by_name = schema_or_config_same(schema, config, intern!(py, "validate_by_name"))?.unwrap_or(false);
79-
let validate_by_alias =
80-
schema_or_config_same(schema, config, intern!(py, "validate_by_alias"))?.unwrap_or(true);
81-
8281
for (arg_index, arg) in arguments_schema.iter().enumerate() {
8382
let arg = arg.downcast::<PyDict>()?;
8483

@@ -99,19 +98,11 @@ impl BuildValidator for ArgumentsValidator {
9998
had_keyword_only = true;
10099
}
101100

102-
let mut kw_lookup_key = None;
103-
let mut kwarg_key = None;
104-
if mode == "keyword_only" || mode == "positional_or_keyword" {
105-
let validation_alias = arg.get_item(intern!(py, "alias"))?;
106-
kw_lookup_key = Some(get_lookup_key(
107-
py,
108-
validation_alias,
109-
validate_by_name,
110-
validate_by_alias,
111-
name.as_str(),
112-
)?);
113-
kwarg_key = Some(py_name.unbind());
114-
}
101+
let kwarg_key = if mode == "keyword_only" || mode == "positional_or_keyword" {
102+
Some(py_name.unbind())
103+
} else {
104+
None
105+
};
115106

116107
let schema = arg.get_as_req(intern!(py, "schema"))?;
117108

@@ -138,9 +129,10 @@ impl BuildValidator for ArgumentsValidator {
138129
parameters.push(Parameter {
139130
positional,
140131
name,
141-
kw_lookup_key,
142132
kwarg_key,
143133
validator,
134+
alias: arg.get_item(intern!(py, "alias"))?.map(std::convert::Into::into),
135+
mode: mode.to_string(),
144136
});
145137
}
146138

@@ -171,6 +163,8 @@ impl BuildValidator for ArgumentsValidator {
171163
var_kwargs_validator,
172164
loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true),
173165
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),
174168
}
175169
.into())
176170
}
@@ -199,7 +193,10 @@ impl Validator for ArgumentsValidator {
199193
let mut output_args: Vec<PyObject> = Vec::with_capacity(self.positional_params_count);
200194
let output_kwargs = PyDict::new(py);
201195
let mut errors: Vec<ValLineError> = Vec::new();
202-
let mut used_kwargs: AHashSet<&str> = AHashSet::with_capacity(self.parameters.len());
196+
let mut used_kwargs: AHashSet<String> = AHashSet::with_capacity(self.parameters.len());
197+
198+
let validate_by_alias = state.validate_by_alias_or(self.validate_by_alias);
199+
let validate_by_name = state.validate_by_name_or(self.validate_by_name);
203200

204201
// go through arguments getting the value from args or kwargs and validating it
205202
for (index, parameter) in self.parameters.iter().enumerate() {
@@ -210,10 +207,21 @@ impl Validator for ArgumentsValidator {
210207
}
211208
}
212209
let mut kw_value = None;
210+
let mut kw_lookup_key = None;
211+
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+
)?);
219+
}
220+
213221
if let Some(kwargs) = args.kwargs() {
214-
if let Some(ref lookup_key) = parameter.kw_lookup_key {
222+
if let Some(ref lookup_key) = kw_lookup_key {
215223
if let Some((lookup_path, value)) = kwargs.get_item(lookup_key)? {
216-
used_kwargs.insert(lookup_path.first_key());
224+
used_kwargs.insert(lookup_path.first_key().to_string());
217225
kw_value = Some((lookup_path, value));
218226
}
219227
}
@@ -257,7 +265,7 @@ impl Validator for ArgumentsValidator {
257265
} else {
258266
output_args.push(value);
259267
}
260-
} else if let Some(ref lookup_key) = parameter.kw_lookup_key {
268+
} else if let Some(lookup_key) = kw_lookup_key {
261269
let error_type = if parameter.positional {
262270
ErrorTypeDefaults::MissingArgument
263271
} else {

src/validators/dataclass.rs

Lines changed: 24 additions & 12 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, LookupKey};
15+
use crate::lookup_key::get_lookup_key;
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-
lookup_key: LookupKey,
30+
alias: Option<Py<PyAny>>,
3131
validator: CombinedValidator,
3232
frozen: bool,
3333
}
@@ -42,6 +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,
4547
}
4648

4749
impl BuildValidator for DataclassArgsValidator {
@@ -54,8 +56,6 @@ impl BuildValidator for DataclassArgsValidator {
5456
) -> PyResult<CombinedValidator> {
5557
let py = schema.py();
5658

57-
let validate_by_name = config.get_as(intern!(py, "validate_by_name"))?.unwrap_or(false);
58-
let validate_by_alias = config.get_as(intern!(py, "validate_by_alias"))?.unwrap_or(true);
5959
let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?;
6060

6161
let extras_validator = match (schema.get_item(intern!(py, "extras_schema"))?, &extra_behavior) {
@@ -75,9 +75,6 @@ impl BuildValidator for DataclassArgsValidator {
7575
let py_name: Bound<'_, PyString> = field.get_as_req(intern!(py, "name"))?;
7676
let name: String = py_name.extract()?;
7777

78-
let validation_alias = field.get_item(intern!(py, "validation_alias"))?;
79-
let lookup_key = get_lookup_key(py, validation_alias, validate_by_name, validate_by_alias, name.as_str())?;
80-
8178
let schema = field.get_as_req(intern!(py, "schema"))?;
8279

8380
let validator = match build_validator(&schema, config, definitions) {
@@ -100,7 +97,9 @@ impl BuildValidator for DataclassArgsValidator {
10097
kw_only,
10198
name,
10299
py_name: py_name.into(),
103-
lookup_key,
100+
alias: field
101+
.get_item(intern!(py, "validation_alias"))?
102+
.map(std::convert::Into::into),
104103
validator,
105104
init: field.get_as(intern!(py, "init"))?.unwrap_or(true),
106105
init_only: field.get_as(intern!(py, "init_only"))?.unwrap_or(false),
@@ -125,6 +124,8 @@ impl BuildValidator for DataclassArgsValidator {
125124
extra_behavior,
126125
extras_validator,
127126
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),
128129
}
129130
.into())
130131
}
@@ -150,9 +151,13 @@ impl Validator for DataclassArgsValidator {
150151
let mut init_only_args = self.init_only_count.map(Vec::with_capacity);
151152

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

155156
let state = &mut state.rebind_extra(|extra| extra.data = Some(output_dict.clone()));
157+
158+
let validate_by_alias = state.validate_by_alias_or(self.validate_by_alias);
159+
let validate_by_name = state.validate_by_name_or(self.validate_by_name);
160+
156161
let mut fields_set_count: usize = 0;
157162

158163
macro_rules! set_item {
@@ -195,10 +200,17 @@ impl Validator for DataclassArgsValidator {
195200
}
196201
}
197202

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+
)?;
198210
let mut kw_value = None;
199211
if let Some(kwargs) = args.kwargs() {
200-
if let Some((lookup_path, value)) = kwargs.get_item(&field.lookup_key)? {
201-
used_keys.insert(lookup_path.first_key());
212+
if let Some((lookup_path, value)) = kwargs.get_item(&lookup_key)? {
213+
used_keys.insert(lookup_path.first_key().to_string());
202214
kw_value = Some((lookup_path, value));
203215
}
204216
}
@@ -248,7 +260,7 @@ impl Validator for DataclassArgsValidator {
248260
}
249261
Ok(None) => {
250262
// This means there was no default value
251-
errors.push(field.lookup_key.error(
263+
errors.push(lookup_key.error(
252264
ErrorTypeDefaults::Missing,
253265
input,
254266
self.loc_by_alias,

src/validators/generator.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ impl InternalValidator {
279279
context: self.context.as_ref().map(|data| data.bind(py)),
280280
self_instance: self.self_instance.as_ref().map(|data| data.bind(py)),
281281
cache_str: self.cache_str,
282+
by_alias: None,
283+
by_name: None,
282284
};
283285
let mut state = ValidationState::new(extra, &mut self.recursion_guard, false.into());
284286
state.exactness = self.exactness;
@@ -314,6 +316,8 @@ impl InternalValidator {
314316
context: self.context.as_ref().map(|data| data.bind(py)),
315317
self_instance: self.self_instance.as_ref().map(|data| data.bind(py)),
316318
cache_str: self.cache_str,
319+
by_alias: None,
320+
by_name: None,
317321
};
318322
let mut state = ValidationState::new(extra, &mut self.recursion_guard, false.into());
319323
state.exactness = self.exactness;

0 commit comments

Comments
 (0)