Skip to content

Commit adb1f49

Browse files
committed
enforcing validate_by_alias
1 parent ce843ed commit adb1f49

File tree

9 files changed

+254
-37
lines changed

9 files changed

+254
-37
lines changed

src/lookup_key.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,3 +577,22 @@ fn py_get_attrs<'py>(obj: &Bound<'py, PyAny>, attr_name: &Py<PyString>) -> PyRes
577577
}
578578
}
579579
}
580+
581+
pub fn get_lookup_key(
582+
py: Python,
583+
validation_alias: Option<Bound<'_, PyAny>>,
584+
validate_by_name: bool,
585+
validate_by_alias: bool,
586+
field_name: &str,
587+
) -> 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`.")
594+
}
595+
(None, _, _) => LookupKey::from_string(py, field_name),
596+
};
597+
Ok(lookup_key)
598+
}

src/validators/arguments.rs

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

1717
use super::validation_state::ValidationState;
@@ -68,15 +68,17 @@ impl BuildValidator for ArgumentsValidator {
6868
) -> PyResult<CombinedValidator> {
6969
let py = schema.py();
7070

71-
let validate_by_name = schema_or_config_same(schema, config, intern!(py, "validate_by_name"))?.unwrap_or(false);
72-
7371
let arguments_schema: Bound<'_, PyList> = schema.get_as_req(intern!(py, "arguments_schema"))?;
7472
let mut parameters: Vec<Parameter> = Vec::with_capacity(arguments_schema.len());
7573

7674
let mut positional_params_count = 0;
7775
let mut had_default_arg = false;
7876
let mut had_keyword_only = false;
7977

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+
8082
for (arg_index, arg) in arguments_schema.iter().enumerate() {
8183
let arg = arg.downcast::<PyDict>()?;
8284

@@ -100,13 +102,14 @@ impl BuildValidator for ArgumentsValidator {
100102
let mut kw_lookup_key = None;
101103
let mut kwarg_key = None;
102104
if mode == "keyword_only" || mode == "positional_or_keyword" {
103-
kw_lookup_key = match arg.get_item(intern!(py, "alias"))? {
104-
Some(alias) => {
105-
let alt_alias = if validate_by_name { Some(name.as_str()) } else { None };
106-
Some(LookupKey::from_py(py, &alias, alt_alias)?)
107-
}
108-
None => Some(LookupKey::from_string(py, &name)),
109-
};
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+
)?);
110113
kwarg_key = Some(py_name.unbind());
111114
}
112115

src/validators/dataclass.rs

Lines changed: 4 additions & 9 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::LookupKey;
15+
use crate::lookup_key::{get_lookup_key, LookupKey};
1616
use crate::tools::SchemaDict;
1717
use crate::validators::function::convert_err;
1818

@@ -55,7 +55,7 @@ impl BuildValidator for DataclassArgsValidator {
5555
let py = schema.py();
5656

5757
let validate_by_name = config.get_as(intern!(py, "validate_by_name"))?.unwrap_or(false);
58-
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,13 +75,8 @@ 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 lookup_key = match field.get_item(intern!(py, "validation_alias"))? {
79-
Some(alias) => {
80-
let alt_alias = if validate_by_name { Some(name.as_str()) } else { None };
81-
LookupKey::from_py(py, &alias, alt_alias)?
82-
}
83-
None => LookupKey::from_string(py, &name),
84-
};
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())?;
8580

8681
let schema = field.get_as_req(intern!(py, "schema"))?;
8782

src/validators/model_fields.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ 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::LookupKey;
15+
use crate::lookup_key::{get_lookup_key, LookupKey};
1616
use crate::tools::SchemaDict;
1717

1818
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};
@@ -51,7 +51,9 @@ impl BuildValidator for ModelFieldsValidator {
5151
let strict = is_strict(schema, config)?;
5252

5353
let from_attributes = schema_or_config_same(schema, config, intern!(py, "from_attributes"))?.unwrap_or(false);
54+
5455
let validate_by_name = config.get_as(intern!(py, "validate_by_name"))?.unwrap_or(false);
56+
let validate_by_alias = config.get_as(intern!(py, "validate_by_alias"))?.unwrap_or(true);
5557

5658
let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?;
5759

@@ -79,13 +81,8 @@ impl BuildValidator for ModelFieldsValidator {
7981
Err(err) => return py_schema_err!("Field \"{}\":\n {}", field_name, err),
8082
};
8183

82-
let lookup_key = match field_info.get_item(intern!(py, "validation_alias"))? {
83-
Some(alias) => {
84-
let alt_alias = if validate_by_name { Some(field_name) } else { None };
85-
LookupKey::from_py(py, &alias, alt_alias)?
86-
}
87-
None => LookupKey::from_string(py, field_name),
88-
};
84+
let validation_alias = field_info.get_item(intern!(py, "validation_alias"))?;
85+
let lookup_key = get_lookup_key(py, validation_alias, validate_by_name, validate_by_alias, field_name)?;
8986

9087
fields.push(Field {
9188
name: field_name.to_string(),

src/validators/typed_dict.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::input::BorrowInput;
1010
use crate::input::ConsumeIterator;
1111
use crate::input::ValidationMatch;
1212
use crate::input::{Input, ValidatedDict};
13-
use crate::lookup_key::LookupKey;
13+
use crate::lookup_key::{get_lookup_key, LookupKey};
1414
use crate::tools::SchemaDict;
1515
use ahash::AHashSet;
1616
use jiter::PartialMode;
@@ -55,7 +55,9 @@ impl BuildValidator for TypedDictValidator {
5555

5656
let total =
5757
schema_or_config(schema, config, intern!(py, "total"), intern!(py, "typed_dict_total"))?.unwrap_or(true);
58+
5859
let validate_by_name = config.get_as(intern!(py, "validate_by_name"))?.unwrap_or(false);
60+
let validate_by_alias = config.get_as(intern!(py, "validate_by_alias"))?.unwrap_or(true);
5961

6062
let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?;
6163

@@ -108,13 +110,8 @@ impl BuildValidator for TypedDictValidator {
108110
}
109111
}
110112

111-
let lookup_key = match field_info.get_item(intern!(py, "validation_alias"))? {
112-
Some(alias) => {
113-
let alt_alias = if validate_by_name { Some(field_name) } else { None };
114-
LookupKey::from_py(py, &alias, alt_alias)?
115-
}
116-
None => LookupKey::from_string(py, field_name),
117-
};
113+
let validation_alias = field_info.get_item(intern!(py, "validation_alias"))?;
114+
let lookup_key = get_lookup_key(py, validation_alias, validate_by_name, validate_by_alias, field_name)?;
118115

119116
fields.push(TypedDictField {
120117
name: field_name.to_string(),

tests/validators/test_arguments.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,47 @@ def test_alias_validate_by_name(py_and_json: PyAndJson, input_value, expected):
900900
assert v.validate_test(input_value) == expected
901901

902902

903+
def test_only_validate_by_name(py_and_json) -> None:
904+
schema = core_schema.arguments_schema(
905+
[
906+
core_schema.arguments_parameter(name='a', schema=core_schema.str_schema(), alias='FieldA'),
907+
],
908+
validate_by_name=True,
909+
validate_by_alias=False,
910+
)
911+
v = py_and_json(schema)
912+
assert v.validate_test(ArgsKwargs((), {'a': 'hello'})) == ((), {'a': 'hello'})
913+
with pytest.raises(ValidationError, match=r'a\n +Missing required argument \[type=missing_argument,'):
914+
assert v.validate_test(ArgsKwargs((), {'FieldA': 'hello'}))
915+
916+
917+
def test_only_allow_alias(py_and_json) -> None:
918+
schema = core_schema.arguments_schema(
919+
[
920+
core_schema.arguments_parameter(name='a', schema=core_schema.str_schema(), alias='FieldA'),
921+
],
922+
validate_by_name=False,
923+
validate_by_alias=True,
924+
)
925+
v = py_and_json(schema)
926+
assert v.validate_test(ArgsKwargs((), {'FieldA': 'hello'})) == ((), {'a': 'hello'})
927+
with pytest.raises(ValidationError, match=r'FieldA\n +Missing required argument \[type=missing_argument,'):
928+
assert v.validate_test(ArgsKwargs((), {'a': 'hello'}))
929+
930+
931+
def test_invalid_config_raises() -> None:
932+
with pytest.raises(SchemaError, match='`validate_by_name` and `validate_by_alias` cannot both be set to `False`.'):
933+
SchemaValidator(
934+
core_schema.arguments_schema(
935+
[
936+
core_schema.arguments_parameter(name='a', schema=core_schema.str_schema(), alias='FieldA'),
937+
],
938+
validate_by_name=False,
939+
validate_by_alias=False,
940+
)
941+
)
942+
943+
903944
def validate(config=None):
904945
def decorator(function):
905946
parameters = signature(function).parameters

tests/validators/test_dataclasses.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99
from dirty_equals import IsListOrTuple, IsStr
1010

11-
from pydantic_core import ArgsKwargs, SchemaValidator, ValidationError, core_schema
11+
from pydantic_core import ArgsKwargs, SchemaError, SchemaValidator, ValidationError, core_schema
1212

1313
from ..conftest import Err, PyAndJson, assert_gc
1414

@@ -1708,3 +1708,88 @@ class Foo:
17081708
assert exc_info.value.errors(include_url=False) == expected.errors
17091709
else:
17101710
assert dataclasses.asdict(v.validate_python(input_value)) == expected
1711+
1712+
1713+
@dataclasses.dataclass
1714+
class BasicDataclass:
1715+
a: str
1716+
1717+
1718+
def test_alias_allow_pop(py_and_json: PyAndJson):
1719+
schema = core_schema.dataclass_schema(
1720+
BasicDataclass,
1721+
core_schema.dataclass_args_schema(
1722+
'BasicDataclass',
1723+
[
1724+
core_schema.dataclass_field(name='a', schema=core_schema.str_schema(), validation_alias='FieldA'),
1725+
],
1726+
),
1727+
['a'],
1728+
config=core_schema.CoreConfig(validate_by_name=True, validate_by_alias=True),
1729+
)
1730+
v = py_and_json(schema)
1731+
assert v.validate_test({'FieldA': 'hello'}) == BasicDataclass(a='hello')
1732+
assert v.validate_test({'a': 'hello'}) == BasicDataclass(a='hello')
1733+
assert v.validate_test(
1734+
{
1735+
'FieldA': 'hello',
1736+
'a': 'world',
1737+
}
1738+
) == BasicDataclass(a='hello')
1739+
with pytest.raises(ValidationError, match=r'FieldA\n +Field required \[type=missing,'):
1740+
assert v.validate_test({'foobar': 'hello'})
1741+
1742+
1743+
def test_only_validate_by_name(py_and_json) -> None:
1744+
schema = core_schema.dataclass_schema(
1745+
BasicDataclass,
1746+
core_schema.dataclass_args_schema(
1747+
'BasicDataclass',
1748+
[
1749+
core_schema.dataclass_field(name='a', schema=core_schema.str_schema(), validation_alias='FieldA'),
1750+
],
1751+
),
1752+
['a'],
1753+
config=core_schema.CoreConfig(validate_by_name=True, validate_by_alias=False),
1754+
)
1755+
v = py_and_json(schema)
1756+
assert v.validate_test({'a': 'hello'}) == BasicDataclass(a='hello')
1757+
with pytest.raises(ValidationError, match=r'a\n +Field required \[type=missing,'):
1758+
assert v.validate_test({'FieldA': 'hello'})
1759+
1760+
1761+
def test_only_allow_alias(py_and_json) -> None:
1762+
schema = core_schema.dataclass_schema(
1763+
BasicDataclass,
1764+
core_schema.dataclass_args_schema(
1765+
'BasicDataclass',
1766+
[
1767+
core_schema.dataclass_field(name='a', schema=core_schema.str_schema(), validation_alias='FieldA'),
1768+
],
1769+
),
1770+
['a'],
1771+
config=core_schema.CoreConfig(validate_by_name=False, validate_by_alias=True),
1772+
)
1773+
v = py_and_json(schema)
1774+
assert v.validate_test({'FieldA': 'hello'}) == BasicDataclass(a='hello')
1775+
with pytest.raises(ValidationError, match=r'FieldA\n +Field required \[type=missing,'):
1776+
assert v.validate_test({'a': 'hello'})
1777+
1778+
1779+
def test_invalid_config_raises() -> None:
1780+
with pytest.raises(SchemaError, match='`validate_by_name` and `validate_by_alias` cannot both be set to `False`.'):
1781+
SchemaValidator(
1782+
core_schema.dataclass_schema(
1783+
BasicDataclass,
1784+
core_schema.dataclass_args_schema(
1785+
'BasicDataclass',
1786+
[
1787+
core_schema.dataclass_field(
1788+
name='a', schema=core_schema.str_schema(), validation_alias='FieldA'
1789+
),
1790+
],
1791+
),
1792+
['a'],
1793+
config=core_schema.CoreConfig(validate_by_name=False, validate_by_alias=False),
1794+
)
1795+
)

tests/validators/test_model_fields.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,43 @@ def test_alias_allow_pop(py_and_json: PyAndJson):
517517
assert v.validate_test({'foobar': '123'})
518518

519519

520+
def test_only_validate_by_name(py_and_json) -> None:
521+
v = py_and_json(
522+
{
523+
'type': 'model-fields',
524+
'fields': {'field_a': {'validation_alias': 'FieldA', 'type': 'model-field', 'schema': {'type': 'int'}}},
525+
},
526+
config=CoreConfig(validate_by_name=True, validate_by_alias=False),
527+
)
528+
assert v.validate_test({'field_a': '123'}) == ({'field_a': 123}, None, {'field_a'})
529+
with pytest.raises(ValidationError, match=r'field_a\n +Field required \[type=missing,'):
530+
assert v.validate_test({'FieldA': '123'})
531+
532+
533+
def test_only_allow_alias(py_and_json) -> None:
534+
v = py_and_json(
535+
{
536+
'type': 'model-fields',
537+
'fields': {'field_a': {'validation_alias': 'FieldA', 'type': 'model-field', 'schema': {'type': 'int'}}},
538+
},
539+
config=CoreConfig(validate_by_name=False, validate_by_alias=True),
540+
)
541+
assert v.validate_test({'FieldA': '123'}) == ({'field_a': 123}, None, {'field_a'})
542+
with pytest.raises(ValidationError, match=r'FieldA\n +Field required \[type=missing,'):
543+
assert v.validate_test({'field_a': '123'})
544+
545+
546+
def test_invalid_config_raises() -> None:
547+
with pytest.raises(SchemaError, match='`validate_by_name` and `validate_by_alias` cannot both be set to `False`.'):
548+
SchemaValidator(
549+
{
550+
'type': 'model-fields',
551+
'fields': {'field_a': {'validation_alias': 'FieldA', 'type': 'model-field', 'schema': {'type': 'int'}}},
552+
},
553+
config=CoreConfig(validate_by_name=False, validate_by_alias=False),
554+
)
555+
556+
520557
@pytest.mark.parametrize(
521558
'input_value,expected',
522559
[

0 commit comments

Comments
 (0)