-
Notifications
You must be signed in to change notification settings - Fork 309
Add support for unpacked TypedDict
to type hint variadic keyword arguments in ArgumentsValidator
#1451
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for unpacked TypedDict
to type hint variadic keyword arguments in ArgumentsValidator
#1451
Changes from 3 commits
5969925
1149fe4
b9dbb34
a5206e5
99d3303
57467bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3372,11 +3372,15 @@ def arguments_parameter( | |
return _dict_not_none(name=name, schema=schema, mode=mode, alias=alias) | ||
|
||
|
||
VarKwargsMode: TypeAlias = Literal['single', 'unpacked-typed-dict'] | ||
|
||
|
||
class ArgumentsSchema(TypedDict, total=False): | ||
type: Required[Literal['arguments']] | ||
arguments_schema: Required[List[ArgumentsParameter]] | ||
populate_by_name: bool | ||
var_args_schema: CoreSchema | ||
var_kwargs_mode: VarKwargsMode | ||
var_kwargs_schema: CoreSchema | ||
ref: str | ||
metadata: Dict[str, Any] | ||
|
@@ -3388,6 +3392,7 @@ def arguments_schema( | |
*, | ||
populate_by_name: bool | None = None, | ||
var_args_schema: CoreSchema | None = None, | ||
var_kwargs_mode: VarKwargsMode | None = None, | ||
var_kwargs_schema: CoreSchema | None = None, | ||
ref: str | None = None, | ||
metadata: Dict[str, Any] | None = None, | ||
|
@@ -3414,6 +3419,9 @@ def arguments_schema( | |
arguments: The arguments to use for the arguments schema | ||
populate_by_name: Whether to populate by name | ||
var_args_schema: The variable args schema to use for the arguments schema | ||
var_kwargs_mode: The validation mode to use for variadic keyword arguments. If `'single'`, every value of the | ||
keyword arguments will be validated against the `var_kwargs_schema` schema. If `'unpacked-typed-dict'`, | ||
the `schema` argument must be a [`typed_dict_schema`][pydantic_core.core_schema.typed_dict_schema] | ||
|
||
var_kwargs_schema: The variable kwargs schema to use for the arguments schema | ||
ref: optional unique identifier of the schema, used to reference the schema in other places | ||
metadata: Any other information you want to include with the schema, not used by pydantic-core | ||
|
@@ -3424,6 +3432,7 @@ def arguments_schema( | |
arguments_schema=arguments, | ||
populate_by_name=populate_by_name, | ||
var_args_schema=var_args_schema, | ||
var_kwargs_mode=var_kwargs_mode, | ||
var_kwargs_schema=var_kwargs_schema, | ||
ref=ref, | ||
metadata=metadata, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
use std::str::FromStr; | ||
|
||
use pyo3::intern; | ||
use pyo3::prelude::*; | ||
use pyo3::types::{PyDict, PyList, PyString, PyTuple}; | ||
|
@@ -15,6 +17,27 @@ use crate::tools::SchemaDict; | |
use super::validation_state::ValidationState; | ||
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator}; | ||
|
||
#[derive(Debug, PartialEq)] | ||
enum VarKwargsMode { | ||
Single, | ||
UnpackedTypedDict, | ||
} | ||
|
||
impl FromStr for VarKwargsMode { | ||
type Err = PyErr; | ||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
match s { | ||
"single" => Ok(Self::Single), | ||
"unpacked-typed-dict" => Ok(Self::UnpackedTypedDict), | ||
s => py_schema_err!( | ||
"Invalid var_kwargs mode: `{}`, expected `single` or `unpacked-typed-dict`", | ||
s | ||
), | ||
} | ||
} | ||
} | ||
|
||
#[derive(Debug)] | ||
struct Parameter { | ||
positional: bool, | ||
|
@@ -29,6 +52,7 @@ pub struct ArgumentsValidator { | |
parameters: Vec<Parameter>, | ||
positional_params_count: usize, | ||
var_args_validator: Option<Box<CombinedValidator>>, | ||
var_kwargs_mode: VarKwargsMode, | ||
var_kwargs_validator: Option<Box<CombinedValidator>>, | ||
loc_by_alias: bool, | ||
extra: ExtraBehavior, | ||
|
@@ -117,17 +141,31 @@ impl BuildValidator for ArgumentsValidator { | |
}); | ||
} | ||
|
||
let py_var_kwargs_mode: Bound<PyString> = schema | ||
.get_as(intern!(py, "var_kwargs_mode"))? | ||
.unwrap_or_else(|| PyString::new_bound(py, "single")); | ||
|
||
let var_kwargs_mode = VarKwargsMode::from_str(py_var_kwargs_mode.to_str()?)?; | ||
let var_kwargs_validator = match schema.get_item(intern!(py, "var_kwargs_schema"))? { | ||
Some(v) => Some(Box::new(build_validator(&v, config, definitions)?)), | ||
None => None, | ||
}; | ||
Viicos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if var_kwargs_mode == VarKwargsMode::UnpackedTypedDict && var_kwargs_validator.is_none() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if we should also check that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you need to, given that you have the conditional checks in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Making it a But I wonder, are there cases where it can be wrapped in a function-after validator (e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The thing is (as described here)
only config can be attached to typed dicts iirc, and it still results in a typed dict schema. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could have #[derive(Debug, PartialEq)]
enum VarKwargsMode {
Uniform(CombinedValidator),
UnpackedTypedDict(TypedDictValidator),
} ... and replace the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will leave this as an optimization for later, going to approve for now 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #1457 |
||
return py_schema_err!( | ||
"`var_kwargs_schema` must be specified when `var_kwargs_mode` is `'unpacked-typed-dict'`" | ||
); | ||
} | ||
|
||
Ok(Self { | ||
parameters, | ||
positional_params_count, | ||
var_args_validator: match schema.get_item(intern!(py, "var_args_schema"))? { | ||
Some(v) => Some(Box::new(build_validator(&v, config, definitions)?)), | ||
None => None, | ||
}, | ||
Viicos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var_kwargs_validator: match schema.get_item(intern!(py, "var_kwargs_schema"))? { | ||
Some(v) => Some(Box::new(build_validator(&v, config, definitions)?)), | ||
None => None, | ||
}, | ||
var_kwargs_mode, | ||
var_kwargs_validator, | ||
loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true), | ||
extra: ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Forbid)?, | ||
} | ||
|
@@ -258,6 +296,8 @@ impl Validator for ArgumentsValidator { | |
// if there are kwargs check any that haven't been processed yet | ||
if let Some(kwargs) = args.kwargs() { | ||
if kwargs.len() > used_kwargs.len() { | ||
let remaining_kwargs = PyDict::new_bound(py); | ||
|
||
for result in kwargs.iter() { | ||
let (raw_key, value) = result?; | ||
let either_str = match raw_key | ||
|
@@ -278,28 +318,55 @@ impl Validator for ArgumentsValidator { | |
Err(err) => return Err(err), | ||
}; | ||
if !used_kwargs.contains(either_str.as_cow()?.as_ref()) { | ||
match self.var_kwargs_validator { | ||
Some(ref validator) => match validator.validate(py, value.borrow_input(), state) { | ||
Ok(value) => { | ||
output_kwargs.set_item(either_str.as_py_string(py, state.cache_str()), value)?; | ||
} | ||
Err(ValError::LineErrors(line_errors)) => { | ||
for err in line_errors { | ||
errors.push(err.with_outer_location(raw_key.clone())); | ||
match self.var_kwargs_mode { | ||
VarKwargsMode::Single => match self.var_kwargs_validator { | ||
Some(ref validator) => match validator.validate(py, value.borrow_input(), state) { | ||
Viicos marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
Ok(value) => { | ||
output_kwargs | ||
.set_item(either_str.as_py_string(py, state.cache_str()), value)?; | ||
} | ||
Err(ValError::LineErrors(line_errors)) => { | ||
for err in line_errors { | ||
errors.push(err.with_outer_location(raw_key.clone())); | ||
} | ||
} | ||
Err(err) => return Err(err), | ||
}, | ||
None => { | ||
if let ExtraBehavior::Forbid = self.extra { | ||
errors.push(ValLineError::new_with_loc( | ||
ErrorTypeDefaults::UnexpectedKeywordArgument, | ||
value, | ||
raw_key.clone(), | ||
)); | ||
} | ||
} | ||
Err(err) => return Err(err), | ||
}, | ||
None => { | ||
if let ExtraBehavior::Forbid = self.extra { | ||
errors.push(ValLineError::new_with_loc( | ||
ErrorTypeDefaults::UnexpectedKeywordArgument, | ||
value, | ||
raw_key.clone(), | ||
)); | ||
} | ||
VarKwargsMode::UnpackedTypedDict => { | ||
// Save to the remaining kwargs, we will validate as a single dict: | ||
remaining_kwargs.set_item(either_str.as_py_string(py, state.cache_str()), value)?; | ||
} | ||
} | ||
} | ||
} | ||
|
||
if self.var_kwargs_mode == VarKwargsMode::UnpackedTypedDict { | ||
// `var_kwargs_validator` is guaranteed to be `Some`: | ||
match self | ||
.var_kwargs_validator | ||
.as_ref() | ||
.unwrap() | ||
.validate(py, remaining_kwargs.as_any(), state) | ||
{ | ||
Ok(value) => { | ||
output_kwargs.update(value.downcast_bound::<PyDict>(py).unwrap().as_mapping())?; | ||
} | ||
Err(ValError::LineErrors(line_errors)) => { | ||
for error in line_errors { | ||
errors.push(error); | ||
Viicos marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
} | ||
} | ||
Err(err) => return Err(err), | ||
} | ||
} | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.