1
+ use std:: str:: FromStr ;
2
+
1
3
use pyo3:: intern;
2
4
use pyo3:: prelude:: * ;
3
5
use pyo3:: types:: { PyDict , PyList , PyString , PyTuple } ;
@@ -15,6 +17,27 @@ use crate::tools::SchemaDict;
15
17
use super :: validation_state:: ValidationState ;
16
18
use super :: { build_validator, BuildValidator , CombinedValidator , DefinitionsBuilder , Validator } ;
17
19
20
+ #[ derive( Debug , PartialEq ) ]
21
+ enum VarKwargsMode {
22
+ Uniform ,
23
+ UnpackedTypedDict ,
24
+ }
25
+
26
+ impl FromStr for VarKwargsMode {
27
+ type Err = PyErr ;
28
+
29
+ fn from_str ( s : & str ) -> Result < Self , Self :: Err > {
30
+ match s {
31
+ "uniform" => Ok ( Self :: Uniform ) ,
32
+ "unpacked-typed-dict" => Ok ( Self :: UnpackedTypedDict ) ,
33
+ s => py_schema_err ! (
34
+ "Invalid var_kwargs mode: `{}`, expected `uniform` or `unpacked-typed-dict`" ,
35
+ s
36
+ ) ,
37
+ }
38
+ }
39
+ }
40
+
18
41
#[ derive( Debug ) ]
19
42
struct Parameter {
20
43
positional : bool ,
@@ -29,6 +52,7 @@ pub struct ArgumentsValidator {
29
52
parameters : Vec < Parameter > ,
30
53
positional_params_count : usize ,
31
54
var_args_validator : Option < Box < CombinedValidator > > ,
55
+ var_kwargs_mode : VarKwargsMode ,
32
56
var_kwargs_validator : Option < Box < CombinedValidator > > ,
33
57
loc_by_alias : bool ,
34
58
extra : ExtraBehavior ,
@@ -117,17 +141,31 @@ impl BuildValidator for ArgumentsValidator {
117
141
} ) ;
118
142
}
119
143
144
+ let py_var_kwargs_mode: Bound < PyString > = schema
145
+ . get_as ( intern ! ( py, "var_kwargs_mode" ) ) ?
146
+ . unwrap_or_else ( || PyString :: new_bound ( py, "uniform" ) ) ;
147
+
148
+ let var_kwargs_mode = VarKwargsMode :: from_str ( py_var_kwargs_mode. to_str ( ) ?) ?;
149
+ let var_kwargs_validator = match schema. get_item ( intern ! ( py, "var_kwargs_schema" ) ) ? {
150
+ Some ( v) => Some ( Box :: new ( build_validator ( & v, config, definitions) ?) ) ,
151
+ None => None ,
152
+ } ;
153
+
154
+ if var_kwargs_mode == VarKwargsMode :: UnpackedTypedDict && var_kwargs_validator. is_none ( ) {
155
+ return py_schema_err ! (
156
+ "`var_kwargs_schema` must be specified when `var_kwargs_mode` is `'unpacked-typed-dict'`"
157
+ ) ;
158
+ }
159
+
120
160
Ok ( Self {
121
161
parameters,
122
162
positional_params_count,
123
163
var_args_validator : match schema. get_item ( intern ! ( py, "var_args_schema" ) ) ? {
124
164
Some ( v) => Some ( Box :: new ( build_validator ( & v, config, definitions) ?) ) ,
125
165
None => None ,
126
166
} ,
127
- var_kwargs_validator : match schema. get_item ( intern ! ( py, "var_kwargs_schema" ) ) ? {
128
- Some ( v) => Some ( Box :: new ( build_validator ( & v, config, definitions) ?) ) ,
129
- None => None ,
130
- } ,
167
+ var_kwargs_mode,
168
+ var_kwargs_validator,
131
169
loc_by_alias : config. get_as ( intern ! ( py, "loc_by_alias" ) ) ?. unwrap_or ( true ) ,
132
170
extra : ExtraBehavior :: from_schema_or_config ( py, schema, config, ExtraBehavior :: Forbid ) ?,
133
171
}
@@ -255,6 +293,9 @@ impl Validator for ArgumentsValidator {
255
293
}
256
294
}
257
295
}
296
+
297
+ let remaining_kwargs = PyDict :: new_bound ( py) ;
298
+
258
299
// if there are kwargs check any that haven't been processed yet
259
300
if let Some ( kwargs) = args. kwargs ( ) {
260
301
if kwargs. len ( ) > used_kwargs. len ( ) {
@@ -278,33 +319,58 @@ impl Validator for ArgumentsValidator {
278
319
Err ( err) => return Err ( err) ,
279
320
} ;
280
321
if !used_kwargs. contains ( either_str. as_cow ( ) ?. as_ref ( ) ) {
281
- match self . var_kwargs_validator {
282
- Some ( ref validator) => match validator. validate ( py, value. borrow_input ( ) , state) {
283
- Ok ( value) => {
284
- output_kwargs. set_item ( either_str. as_py_string ( py, state. cache_str ( ) ) , value) ?;
285
- }
286
- Err ( ValError :: LineErrors ( line_errors) ) => {
287
- for err in line_errors {
288
- errors. push ( err. with_outer_location ( raw_key. clone ( ) ) ) ;
322
+ match self . var_kwargs_mode {
323
+ VarKwargsMode :: Uniform => match & self . var_kwargs_validator {
324
+ Some ( validator) => match validator. validate ( py, value. borrow_input ( ) , state) {
325
+ Ok ( value) => {
326
+ output_kwargs
327
+ . set_item ( either_str. as_py_string ( py, state. cache_str ( ) ) , value) ?;
328
+ }
329
+ Err ( ValError :: LineErrors ( line_errors) ) => {
330
+ for err in line_errors {
331
+ errors. push ( err. with_outer_location ( raw_key. clone ( ) ) ) ;
332
+ }
333
+ }
334
+ Err ( err) => return Err ( err) ,
335
+ } ,
336
+ None => {
337
+ if let ExtraBehavior :: Forbid = self . extra {
338
+ errors. push ( ValLineError :: new_with_loc (
339
+ ErrorTypeDefaults :: UnexpectedKeywordArgument ,
340
+ value,
341
+ raw_key. clone ( ) ,
342
+ ) ) ;
289
343
}
290
344
}
291
- Err ( err) => return Err ( err) ,
292
345
} ,
293
- None => {
294
- if let ExtraBehavior :: Forbid = self . extra {
295
- errors. push ( ValLineError :: new_with_loc (
296
- ErrorTypeDefaults :: UnexpectedKeywordArgument ,
297
- value,
298
- raw_key. clone ( ) ,
299
- ) ) ;
300
- }
346
+ VarKwargsMode :: UnpackedTypedDict => {
347
+ // Save to the remaining kwargs, we will validate as a single dict:
348
+ remaining_kwargs. set_item ( either_str. as_py_string ( py, state. cache_str ( ) ) , value) ?;
301
349
}
302
350
}
303
351
}
304
352
}
305
353
}
306
354
}
307
355
356
+ if self . var_kwargs_mode == VarKwargsMode :: UnpackedTypedDict {
357
+ // `var_kwargs_validator` is guaranteed to be `Some`:
358
+ match self
359
+ . var_kwargs_validator
360
+ . as_ref ( )
361
+ . unwrap ( )
362
+ . validate ( py, remaining_kwargs. as_any ( ) , state)
363
+ {
364
+ Ok ( value) => {
365
+ output_kwargs. update ( value. downcast_bound :: < PyDict > ( py) . unwrap ( ) . as_mapping ( ) ) ?;
366
+ }
367
+ Err ( ValError :: LineErrors ( line_errors) ) => {
368
+ errors. extend ( line_errors) ;
369
+ }
370
+ Err ( err) => return Err ( err) ,
371
+ }
372
+ }
373
+
308
374
if !errors. is_empty ( ) {
309
375
Err ( ValError :: LineErrors ( errors) )
310
376
} else {
0 commit comments