Skip to content

Commit 2f9b588

Browse files
committed
Wrap up implementation and add tests
1 parent e442d7d commit 2f9b588

File tree

10 files changed

+861
-42
lines changed

10 files changed

+861
-42
lines changed

src/validators/arguments_v3.rs

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,6 @@ impl BuildValidator for ArgumentsV3Validator {
102102

103103
let mode = ParameterMode::from_str(py_mode)?;
104104

105-
// let positional = mode == "positional_only" || mode == "positional_or_keyword";
106-
// if positional {
107-
// positional_params_count = arg_index + 1;
108-
// }
109-
110105
if mode == ParameterMode::KeywordOnly {
111106
had_keyword_only = true;
112107
}
@@ -129,7 +124,7 @@ impl BuildValidator for ArgumentsV3Validator {
129124
};
130125

131126
if had_default_arg && !has_default && !had_keyword_only {
132-
return py_schema_err!("Non-default argument '{}' follows default argument", name);
127+
return py_schema_err!("Required parameter '{}' follows parameter with default", name);
133128
} else if has_default {
134129
had_default_arg = true;
135130
}
@@ -215,19 +210,29 @@ impl ArgumentsV3Validator {
215210
}
216211
ParameterMode::VarArgs => match dict_value.borrow_input().validate_tuple(false) {
217212
Ok(tuple) => {
213+
let mut i: i64 = 0;
218214
tuple.unpack(state).try_for_each(|v| {
219215
match parameter.validator.validate(py, v.unwrap().borrow_input(), state) {
220216
Ok(tuple_value) => {
221217
output_args.push(tuple_value);
218+
i += 1;
222219
Ok(())
223220
}
224221
Err(ValError::LineErrors(line_errors)) => {
225222
errors.extend(line_errors.into_iter().map(|err| {
226-
lookup_path.apply_error_loc(err, self.loc_by_alias, &parameter.name)
223+
lookup_path.apply_error_loc(
224+
err.with_outer_location(i),
225+
self.loc_by_alias,
226+
&parameter.name,
227+
)
227228
}));
229+
i += 1;
228230
Ok(())
229231
}
230-
Err(err) => Err(err),
232+
Err(err) => {
233+
i += 1;
234+
Err(err)
235+
}
231236
}
232237
})?;
233238
}
@@ -292,31 +297,35 @@ impl ArgumentsV3Validator {
292297
}
293298
},
294299
ParameterMode::VarKwargsUnpackedTypedDict => {
295-
let kwargs_dict = dict_value
296-
.borrow_input()
297-
.as_kwargs(py)
298-
.unwrap_or_else(|| PyDict::new(py));
299-
match parameter.validator.validate(py, kwargs_dict.as_any(), state) {
300+
match parameter.validator.validate(py, dict_value.borrow_input(), state) {
300301
Ok(value) => {
301302
output_kwargs.update(value.downcast_bound::<PyDict>(py).unwrap().as_mapping())?;
302303
}
303304
Err(ValError::LineErrors(line_errors)) => {
304-
errors.extend(line_errors);
305+
errors.extend(
306+
line_errors.into_iter().map(|err| {
307+
lookup_path.apply_error_loc(err, self.loc_by_alias, &parameter.name)
308+
}),
309+
);
305310
}
306311
Err(err) => return Err(err),
307312
}
308313
}
309314
}
310-
// No value is present in the mapping, fallback to the default value (and error if no default):
315+
// No value is present in the mapping...
311316
} else {
312317
match parameter.mode {
318+
// ... fallback to the default value (and error if no default):
313319
ParameterMode::PositionalOnly | ParameterMode::PositionalOrKeyword | ParameterMode::KeywordOnly => {
314320
if let Some(value) =
315321
parameter
316322
.validator
317323
.default_value(py, Some(parameter.name.as_str()), state)?
318324
{
319-
if parameter.mode == ParameterMode::PositionalOnly {
325+
if matches!(
326+
parameter.mode,
327+
ParameterMode::PositionalOnly | ParameterMode::PositionalOrKeyword
328+
) {
320329
output_args.push(value);
321330
} else {
322331
output_kwargs.set_item(PyString::new(py, parameter.name.as_str()).unbind(), value)?;
@@ -337,7 +346,23 @@ impl ArgumentsV3Validator {
337346
));
338347
}
339348
}
340-
// Variadic args/kwargs can be empty by definition:
349+
// ... validate the unpacked kwargs against an empty dict:
350+
ParameterMode::VarKwargsUnpackedTypedDict => {
351+
match parameter.validator.validate(py, PyDict::new(py).borrow_input(), state) {
352+
Ok(value) => {
353+
output_kwargs.update(value.downcast_bound::<PyDict>(py).unwrap().as_mapping())?;
354+
}
355+
Err(ValError::LineErrors(line_errors)) => {
356+
errors.extend(
357+
line_errors
358+
.into_iter()
359+
.map(|err| err.with_outer_location(&parameter.name)),
360+
);
361+
}
362+
Err(err) => return Err(err),
363+
}
364+
}
365+
// Variadic args/uniform kwargs can be empty by definition:
341366
_ => (),
342367
}
343368
}
@@ -436,13 +461,10 @@ impl ArgumentsV3Validator {
436461
.validator
437462
.default_value(py, Some(parameter.name.as_str()), state)?
438463
{
439-
if matches!(
440-
parameter.mode,
441-
ParameterMode::PositionalOnly | ParameterMode::PositionalOrKeyword
442-
) {
443-
output_kwargs.set_item(PyString::new(py, parameter.name.as_str()).unbind(), value)?;
444-
} else {
464+
if parameter.mode == ParameterMode::PositionalOnly {
445465
output_args.push(value);
466+
} else {
467+
output_kwargs.set_item(PyString::new(py, parameter.name.as_str()).unbind(), value)?;
446468
}
447469
} else {
448470
// Required and no default, error:
@@ -577,13 +599,12 @@ impl ArgumentsV3Validator {
577599
}
578600
}
579601

580-
if !remaining_kwargs.is_empty() {
581-
// In this case, the unpacked typeddict var kwargs parameter is guaranteed to exist:
582-
let var_kwargs_parameter = self
583-
.parameters
584-
.iter()
585-
.find(|p| p.mode == ParameterMode::VarKwargsUnpackedTypedDict)
586-
.unwrap();
602+
let maybe_var_kwargs_parameter = self
603+
.parameters
604+
.iter()
605+
.find(|p| p.mode == ParameterMode::VarKwargsUnpackedTypedDict);
606+
607+
if let Some(var_kwargs_parameter) = maybe_var_kwargs_parameter {
587608
match var_kwargs_parameter
588609
.validator
589610
.validate(py, remaining_kwargs.as_any(), state)

tests/validators/arguments_v3/__init__.py

Whitespace-only changes.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import re
2+
3+
import pytest
4+
5+
from pydantic_core import ArgsKwargs, ValidationError
6+
from pydantic_core import core_schema as cs
7+
8+
from ...conftest import Err, PyAndJson
9+
10+
11+
@pytest.mark.parametrize(
12+
['input_value', 'expected'],
13+
(
14+
[ArgsKwargs((1,)), ((1,), {})],
15+
[ArgsKwargs((), {'Foo': 1}), ((), {'a': 1})],
16+
[ArgsKwargs((), {'a': 1}), Err('Foo\n Missing required argument [type=missing_argument,')],
17+
[{'Foo': 1}, ((1,), {})],
18+
[{'a': 1}, Err('Foo\n Missing required argument [type=missing_argument,')],
19+
),
20+
ids=repr,
21+
)
22+
def test_alias(py_and_json: PyAndJson, input_value, expected) -> None:
23+
v = py_and_json(
24+
cs.arguments_v3_schema(
25+
[
26+
cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), alias='Foo', mode='positional_or_keyword'),
27+
]
28+
)
29+
)
30+
if isinstance(expected, Err):
31+
with pytest.raises(ValidationError, match=re.escape(expected.message)):
32+
v.validate_test(input_value)
33+
else:
34+
assert v.validate_test(input_value) == expected
35+
36+
37+
@pytest.mark.parametrize(
38+
['input_value', 'expected'],
39+
(
40+
[ArgsKwargs((1,)), ((1,), {})],
41+
[ArgsKwargs((), {'Foo': 1}), ((), {'a': 1})],
42+
[ArgsKwargs((), {'a': 1}), ((), {'a': 1})],
43+
[ArgsKwargs((), {'a': 1, 'b': 2}), Err('b\n Unexpected keyword argument [type=unexpected_keyword_argument,')],
44+
[
45+
ArgsKwargs((), {'a': 1, 'Foo': 2}),
46+
Err('a\n Unexpected keyword argument [type=unexpected_keyword_argument,'),
47+
],
48+
[{'Foo': 1}, ((1,), {})],
49+
[{'a': 1}, ((1,), {})],
50+
),
51+
ids=repr,
52+
)
53+
def test_alias_validate_by_name(py_and_json: PyAndJson, input_value, expected):
54+
v = py_and_json(
55+
cs.arguments_v3_schema(
56+
[
57+
cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), alias='Foo', mode='positional_or_keyword'),
58+
],
59+
validate_by_name=True,
60+
)
61+
)
62+
if isinstance(expected, Err):
63+
with pytest.raises(ValidationError, match=re.escape(expected.message)):
64+
v.validate_test(input_value)
65+
else:
66+
assert v.validate_test(input_value) == expected
67+
68+
69+
def test_only_validate_by_name(py_and_json) -> None:
70+
v = py_and_json(
71+
cs.arguments_v3_schema(
72+
[
73+
cs.arguments_v3_parameter(
74+
name='a', schema=cs.str_schema(), alias='FieldA', mode='positional_or_keyword'
75+
),
76+
],
77+
validate_by_name=True,
78+
validate_by_alias=False,
79+
)
80+
)
81+
82+
assert v.validate_test(ArgsKwargs((), {'a': 'hello'})) == ((), {'a': 'hello'})
83+
assert v.validate_test({'a': 'hello'}) == (('hello',), {})
84+
85+
with pytest.raises(ValidationError, match=r'a\n +Missing required argument \[type=missing_argument,'):
86+
assert v.validate_test(ArgsKwargs((), {'FieldA': 'hello'}))
87+
with pytest.raises(ValidationError, match=r'a\n +Missing required argument \[type=missing_argument,'):
88+
assert v.validate_test({'FieldA': 'hello'})
89+
90+
91+
def test_only_allow_alias(py_and_json) -> None:
92+
v = py_and_json(
93+
cs.arguments_v3_schema(
94+
[
95+
cs.arguments_v3_parameter(
96+
name='a', schema=cs.str_schema(), alias='FieldA', mode='positional_or_keyword'
97+
),
98+
],
99+
validate_by_name=False,
100+
validate_by_alias=True,
101+
)
102+
)
103+
assert v.validate_test(ArgsKwargs((), {'FieldA': 'hello'})) == ((), {'a': 'hello'})
104+
assert v.validate_test({'FieldA': 'hello'}) == (('hello',), {})
105+
106+
with pytest.raises(ValidationError, match=r'FieldA\n +Missing required argument \[type=missing_argument,'):
107+
assert v.validate_test(ArgsKwargs((), {'a': 'hello'}))
108+
with pytest.raises(ValidationError, match=r'FieldA\n +Missing required argument \[type=missing_argument,'):
109+
assert v.validate_test({'a': 'hello'})

0 commit comments

Comments
 (0)