Skip to content

Commit 1defe62

Browse files
authored
Add new arguments-v3 schema (#1641)
1 parent 53bdfa6 commit 1defe62

20 files changed

+2029
-3
lines changed

python/pydantic_core/core_schema.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3494,6 +3494,120 @@ def arguments_schema(
34943494
)
34953495

34963496

3497+
class ArgumentsV3Parameter(TypedDict, total=False):
3498+
name: Required[str]
3499+
schema: Required[CoreSchema]
3500+
mode: Literal[
3501+
'positional_only',
3502+
'positional_or_keyword',
3503+
'keyword_only',
3504+
'var_args',
3505+
'var_kwargs_uniform',
3506+
'var_kwargs_unpacked_typed_dict',
3507+
] # default positional_or_keyword
3508+
alias: Union[str, list[Union[str, int]], list[list[Union[str, int]]]]
3509+
3510+
3511+
def arguments_v3_parameter(
3512+
name: str,
3513+
schema: CoreSchema,
3514+
*,
3515+
mode: Literal[
3516+
'positional_only',
3517+
'positional_or_keyword',
3518+
'keyword_only',
3519+
'var_args',
3520+
'var_kwargs_uniform',
3521+
'var_kwargs_unpacked_typed_dict',
3522+
]
3523+
| None = None,
3524+
alias: str | list[str | int] | list[list[str | int]] | None = None,
3525+
) -> ArgumentsV3Parameter:
3526+
"""
3527+
Returns a schema that matches an argument parameter, e.g.:
3528+
3529+
```py
3530+
from pydantic_core import SchemaValidator, core_schema
3531+
3532+
param = core_schema.arguments_v3_parameter(
3533+
name='a', schema=core_schema.str_schema(), mode='positional_only'
3534+
)
3535+
schema = core_schema.arguments_v3_schema([param])
3536+
v = SchemaValidator(schema)
3537+
assert v.validate_python({'a': 'hello'}) == (('hello',), {})
3538+
```
3539+
3540+
Args:
3541+
name: The name to use for the argument parameter
3542+
schema: The schema to use for the argument parameter
3543+
mode: The mode to use for the argument parameter
3544+
alias: The alias to use for the argument parameter
3545+
"""
3546+
return _dict_not_none(name=name, schema=schema, mode=mode, alias=alias)
3547+
3548+
3549+
class ArgumentsV3Schema(TypedDict, total=False):
3550+
type: Required[Literal['arguments-v3']]
3551+
arguments_schema: Required[list[ArgumentsV3Parameter]]
3552+
validate_by_name: bool
3553+
validate_by_alias: bool
3554+
extra_behavior: Literal['forbid', 'ignore'] # 'allow' doesn't make sense here.
3555+
ref: str
3556+
metadata: dict[str, Any]
3557+
serialization: SerSchema
3558+
3559+
3560+
def arguments_v3_schema(
3561+
arguments: list[ArgumentsV3Parameter],
3562+
*,
3563+
validate_by_name: bool | None = None,
3564+
validate_by_alias: bool | None = None,
3565+
extra_behavior: Literal['forbid', 'ignore'] | None = None,
3566+
ref: str | None = None,
3567+
metadata: dict[str, Any] | None = None,
3568+
serialization: SerSchema | None = None,
3569+
) -> ArgumentsV3Schema:
3570+
"""
3571+
Returns a schema that matches an arguments schema, e.g.:
3572+
3573+
```py
3574+
from pydantic_core import SchemaValidator, core_schema
3575+
3576+
param_a = core_schema.arguments_v3_parameter(
3577+
name='a', schema=core_schema.str_schema(), mode='positional_only'
3578+
)
3579+
param_b = core_schema.arguments_v3_parameter(
3580+
name='kwargs', schema=core_schema.bool_schema(), mode='var_kwargs_uniform'
3581+
)
3582+
schema = core_schema.arguments_v3_schema([param_a, param_b])
3583+
v = SchemaValidator(schema)
3584+
assert v.validate_python({'a': 'hi', 'kwargs': {'b': True}}) == (('hi',), {'b': True})
3585+
```
3586+
3587+
This schema is currently not used by other Pydantic components. In V3, it will most likely
3588+
become the default arguments schema for the `'call'` schema.
3589+
3590+
Args:
3591+
arguments: The arguments to use for the arguments schema.
3592+
validate_by_name: Whether to populate by the parameter names, defaults to `False`.
3593+
validate_by_alias: Whether to populate by the parameter aliases, defaults to `True`.
3594+
extra_behavior: The extra behavior to use.
3595+
ref: optional unique identifier of the schema, used to reference the schema in other places.
3596+
metadata: Any other information you want to include with the schema, not used by pydantic-core.
3597+
serialization: Custom serialization schema.
3598+
"""
3599+
return _dict_not_none(
3600+
type='arguments-v3',
3601+
arguments_schema=arguments,
3602+
validate_by_name=validate_by_name,
3603+
validate_by_alias=validate_by_alias,
3604+
extra_behavior=extra_behavior,
3605+
ref=ref,
3606+
metadata=metadata,
3607+
serialization=serialization,
3608+
)
3609+
3610+
34973611
class CallSchema(TypedDict, total=False):
34983612
type: Required[Literal['call']]
34993613
arguments_schema: Required[CoreSchema]
@@ -3921,6 +4035,7 @@ def definition_reference_schema(
39214035
DataclassArgsSchema,
39224036
DataclassSchema,
39234037
ArgumentsSchema,
4038+
ArgumentsV3Schema,
39244039
CallSchema,
39254040
CustomErrorSchema,
39264041
JsonSchema,
@@ -3978,6 +4093,7 @@ def definition_reference_schema(
39784093
'dataclass-args',
39794094
'dataclass',
39804095
'arguments',
4096+
'arguments-v3',
39814097
'call',
39824098
'custom-error',
39834099
'json',

src/input/input_abstract.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ pub trait Input<'py>: fmt::Debug {
8181

8282
fn validate_args(&self) -> ValResult<Self::Arguments<'_>>;
8383

84+
fn validate_args_v3(&self) -> ValResult<Self::Arguments<'_>>;
85+
8486
fn validate_dataclass_args<'a>(&'a self, dataclass_name: &str) -> ValResult<Self::Arguments<'a>>;
8587

8688
fn validate_str(&self, strict: bool, coerce_numbers_to_str: bool) -> ValMatch<EitherString<'_>>;
@@ -265,6 +267,7 @@ pub trait ValidatedList<'py> {
265267
pub trait ValidatedTuple<'py> {
266268
type Item: BorrowInput<'py>;
267269
fn len(&self) -> Option<usize>;
270+
fn try_for_each(self, f: impl FnMut(PyResult<Self::Item>) -> ValResult<()>) -> ValResult<()>;
268271
fn iterate<R>(self, consumer: impl ConsumeIterator<PyResult<Self::Item>, Output = R>) -> ValResult<R>;
269272
}
270273

@@ -313,6 +316,9 @@ impl<'py> ValidatedTuple<'py> for Never {
313316
fn len(&self) -> Option<usize> {
314317
unreachable!()
315318
}
319+
fn try_for_each(self, _f: impl FnMut(PyResult<Self::Item>) -> ValResult<()>) -> ValResult<()> {
320+
unreachable!()
321+
}
316322
fn iterate<R>(self, _consumer: impl ConsumeIterator<PyResult<Self::Item>, Output = R>) -> ValResult<R> {
317323
unreachable!()
318324
}

src/input/input_json.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ impl<'py, 'data> Input<'py> for JsonValue<'data> {
8585
}
8686
}
8787

88+
#[cfg_attr(has_coverage_attribute, coverage(off))]
89+
fn validate_args_v3(&self) -> ValResult<Self::Arguments<'_>> {
90+
Err(ValError::new(ErrorTypeDefaults::ArgumentsType, self))
91+
}
92+
8893
fn validate_dataclass_args<'a>(&'a self, class_name: &str) -> ValResult<JsonArgs<'a, 'data>> {
8994
match self {
9095
JsonValue::Object(object) => Ok(JsonArgs::new(None, Some(object))),
@@ -383,6 +388,11 @@ impl<'py> Input<'py> for str {
383388
Err(ValError::new(ErrorTypeDefaults::ArgumentsType, self))
384389
}
385390

391+
#[cfg_attr(has_coverage_attribute, coverage(off))]
392+
fn validate_args_v3(&self) -> ValResult<Never> {
393+
Err(ValError::new(ErrorTypeDefaults::ArgumentsType, self))
394+
}
395+
386396
#[cfg_attr(has_coverage_attribute, coverage(off))]
387397
fn validate_dataclass_args(&self, class_name: &str) -> ValResult<Never> {
388398
let class_name = class_name.to_string();
@@ -579,6 +589,12 @@ impl<'a, 'data> ValidatedTuple<'_> for &'a JsonArray<'data> {
579589
fn len(&self) -> Option<usize> {
580590
Some(Vec::len(self))
581591
}
592+
fn try_for_each(self, mut f: impl FnMut(PyResult<Self::Item>) -> ValResult<()>) -> ValResult<()> {
593+
for item in self.iter() {
594+
f(Ok(item))?;
595+
}
596+
Ok(())
597+
}
582598
fn iterate<R>(self, consumer: impl ConsumeIterator<PyResult<Self::Item>, Output = R>) -> ValResult<R> {
583599
Ok(consumer.consume_iterator(self.iter().map(Ok)))
584600
}

src/input/input_python.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,16 @@ impl<'py> Input<'py> for Bound<'py, PyAny> {
117117
}
118118
}
119119

120+
fn validate_args_v3(&self) -> ValResult<PyArgs<'py>> {
121+
if let Ok(args_kwargs) = self.extract::<ArgsKwargs>() {
122+
let args = args_kwargs.args.into_bound(self.py());
123+
let kwargs = args_kwargs.kwargs.map(|d| d.into_bound(self.py()));
124+
Ok(PyArgs::new(Some(args), kwargs))
125+
} else {
126+
Err(ValError::new(ErrorTypeDefaults::ArgumentsType, self))
127+
}
128+
}
129+
120130
fn validate_dataclass_args<'a>(&'a self, class_name: &str) -> ValResult<PyArgs<'py>> {
121131
if let Ok(dict) = self.downcast::<PyDict>() {
122132
Ok(PyArgs::new(None, Some(dict.clone())))
@@ -915,7 +925,15 @@ impl<'py> PySequenceIterable<'_, 'py> {
915925
PySequenceIterable::Iterator(iter) => iter.len().ok(),
916926
}
917927
}
918-
928+
fn generic_try_for_each(self, f: impl FnMut(PyResult<Bound<'py, PyAny>>) -> ValResult<()>) -> ValResult<()> {
929+
match self {
930+
PySequenceIterable::List(iter) => iter.iter().map(Ok).try_for_each(f),
931+
PySequenceIterable::Tuple(iter) => iter.iter().map(Ok).try_for_each(f),
932+
PySequenceIterable::Set(iter) => iter.iter().map(Ok).try_for_each(f),
933+
PySequenceIterable::FrozenSet(iter) => iter.iter().map(Ok).try_for_each(f),
934+
PySequenceIterable::Iterator(mut iter) => iter.try_for_each(f),
935+
}
936+
}
919937
fn generic_iterate<R>(
920938
self,
921939
consumer: impl ConsumeIterator<PyResult<Bound<'py, PyAny>>, Output = R>,
@@ -951,6 +969,9 @@ impl<'py> ValidatedTuple<'py> for PySequenceIterable<'_, 'py> {
951969
fn len(&self) -> Option<usize> {
952970
self.generic_len()
953971
}
972+
fn try_for_each(self, f: impl FnMut(PyResult<Self::Item>) -> ValResult<()>) -> ValResult<()> {
973+
self.generic_try_for_each(f)
974+
}
954975
fn iterate<R>(self, consumer: impl ConsumeIterator<PyResult<Self::Item>, Output = R>) -> ValResult<R> {
955976
self.generic_iterate(consumer)
956977
}

src/input/input_string.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ impl<'py> Input<'py> for StringMapping<'py> {
8989
Err(ValError::new(ErrorTypeDefaults::ArgumentsType, self))
9090
}
9191

92+
fn validate_args_v3(&self) -> ValResult<Self::Arguments<'_>> {
93+
// do we want to support this?
94+
Err(ValError::new(ErrorTypeDefaults::ArgumentsType, self))
95+
}
96+
9297
fn validate_dataclass_args<'a>(&'a self, _dataclass_name: &str) -> ValResult<StringMappingDict<'py>> {
9398
match self {
9499
StringMapping::String(_) => Err(ValError::new(ErrorTypeDefaults::ArgumentsType, self)),

0 commit comments

Comments
 (0)