@@ -9,7 +9,9 @@ use pyo3::IntoPyObjectExt;
99
1010use crate :: build_tools:: py_schema_err;
1111use crate :: build_tools:: { schema_or_config_same, ExtraBehavior } ;
12+ use crate :: errors:: LocItem ;
1213use crate :: errors:: { ErrorTypeDefaults , ValError , ValLineError , ValResult } ;
14+ use crate :: input:: ConsumeIterator ;
1315use crate :: input:: {
1416 Arguments , BorrowInput , Input , KeywordArgs , PositionalArgs , ValidatedDict , ValidatedTuple , ValidationMatch ,
1517} ;
@@ -86,13 +88,22 @@ impl BuildValidator for ArgumentsV3Validator {
8688 let mut parameters: Vec < Parameter > = Vec :: with_capacity ( arguments_schema. len ( ) ) ;
8789
8890 let mut had_default_arg = false ;
91+ let mut had_positional_or_keyword = false ;
92+ let mut had_var_args = false ;
8993 let mut had_keyword_only = false ;
94+ let mut had_var_kwargs = false ;
95+
96+ let mut names: AHashSet < String > = AHashSet :: with_capacity ( arguments_schema. len ( ) ) ;
9097
9198 for arg in arguments_schema. iter ( ) {
9299 let arg = arg. downcast :: < PyDict > ( ) ?;
93100
94101 let py_name: Bound < PyString > = arg. get_as_req ( intern ! ( py, "name" ) ) ?;
95102 let name = py_name. to_string ( ) ;
103+ if !names. insert ( name. clone ( ) ) {
104+ return py_schema_err ! ( "Duplicate parameter '{}'" , name) ;
105+ }
106+
96107 let py_mode = arg. get_as :: < Bound < ' _ , PyString > > ( intern ! ( py, "mode" ) ) ?;
97108 let py_mode = py_mode
98109 . as_ref ( )
@@ -102,8 +113,51 @@ impl BuildValidator for ArgumentsV3Validator {
102113
103114 let mode = ParameterMode :: from_str ( py_mode) ?;
104115
105- if mode == ParameterMode :: KeywordOnly {
106- had_keyword_only = true ;
116+ match mode {
117+ ParameterMode :: PositionalOnly => {
118+ if had_positional_or_keyword || had_var_args || had_keyword_only || had_var_kwargs {
119+ return py_schema_err ! (
120+ "Positional only parameter '{}' cannot follow other parameter kinds" ,
121+ name
122+ ) ;
123+ }
124+ }
125+ ParameterMode :: PositionalOrKeyword => {
126+ if had_var_args || had_keyword_only || had_var_kwargs {
127+ return py_schema_err ! (
128+ "Positional or keyword parameter '{}' cannot follow variadic or keyword only parameters" ,
129+ name
130+ ) ;
131+ }
132+ had_positional_or_keyword = true ;
133+ }
134+ ParameterMode :: VarArgs => {
135+ if had_var_args {
136+ return py_schema_err ! ( "Duplicate variadic positional parameter '{}'" , name) ;
137+ }
138+ if had_keyword_only || had_var_kwargs {
139+ return py_schema_err ! (
140+ "Variadic positional parameter '{}' cannot follow variadic or keyword only parameters" ,
141+ name
142+ ) ;
143+ }
144+ had_var_args = true ;
145+ }
146+ ParameterMode :: KeywordOnly => {
147+ if had_var_kwargs {
148+ return py_schema_err ! (
149+ "Keyword only parameter '{}' cannot follow variadic keyword only parameter" ,
150+ name
151+ ) ;
152+ }
153+ had_keyword_only = true ;
154+ }
155+ ParameterMode :: VarKwargsUniform | ParameterMode :: VarKwargsUnpackedTypedDict => {
156+ if had_var_kwargs {
157+ return py_schema_err ! ( "Duplicate variadic keyword parameter '{}'" , name) ;
158+ }
159+ had_var_kwargs = true ;
160+ }
107161 }
108162
109163 let schema = arg. get_as_req ( intern ! ( py, "schema" ) ) ?;
@@ -188,12 +242,26 @@ impl ArgumentsV3Validator {
188242 let validate_by_alias = state. validate_by_alias_or ( self . validate_by_alias ) ;
189243 let validate_by_name = state. validate_by_name_or ( self . validate_by_name ) ;
190244
245+ // Keep track of used keys for extra behavior:
246+ let mut used_keys: Option < AHashSet < & str > > = if self . extra == ExtraBehavior :: Ignore || mapping. is_py_get_attr ( ) {
247+ None
248+ } else {
249+ Some ( AHashSet :: with_capacity ( self . parameters . len ( ) ) )
250+ } ;
251+
191252 for parameter in & self . parameters {
192253 let lookup_key = parameter
193254 . lookup_key_collection
194255 . select ( validate_by_alias, validate_by_name) ?;
256+
195257 // A value is present in the mapping:
196258 if let Some ( ( lookup_path, dict_value) ) = mapping. get_item ( lookup_key) ? {
259+ if let Some ( ref mut used_keys) = used_keys {
260+ // key is "used" whether or not validation passes, since we want to skip this key in
261+ // extra logic either way
262+ used_keys. insert ( lookup_path. first_key ( ) ) ;
263+ }
264+
197265 match parameter. mode {
198266 ParameterMode :: PositionalOnly | ParameterMode :: PositionalOrKeyword => {
199267 match parameter. validator . validate ( py, dict_value. borrow_input ( ) , state) {
@@ -368,6 +436,66 @@ impl ArgumentsV3Validator {
368436 }
369437 }
370438
439+ if let Some ( used_keys) = used_keys {
440+ struct ValidateExtra < ' a > {
441+ used_keys : AHashSet < & ' a str > ,
442+ errors : & ' a mut Vec < ValLineError > ,
443+ extra_behavior : ExtraBehavior ,
444+ }
445+
446+ impl < ' py , Key , Value > ConsumeIterator < ValResult < ( Key , Value ) > > for ValidateExtra < ' _ >
447+ where
448+ Key : BorrowInput < ' py > + Clone + Into < LocItem > ,
449+ Value : BorrowInput < ' py > ,
450+ {
451+ type Output = ValResult < ( ) > ;
452+ fn consume_iterator ( self , iterator : impl Iterator < Item = ValResult < ( Key , Value ) > > ) -> Self :: Output {
453+ for item_result in iterator {
454+ let ( raw_key, value) = item_result?;
455+ let either_str = match raw_key
456+ . borrow_input ( )
457+ . validate_str ( true , false )
458+ . map ( ValidationMatch :: into_inner)
459+ {
460+ Ok ( k) => k,
461+ Err ( ValError :: LineErrors ( line_errors) ) => {
462+ for err in line_errors {
463+ self . errors . push (
464+ err. with_outer_location ( raw_key. clone ( ) )
465+ . with_type ( ErrorTypeDefaults :: InvalidKey ) ,
466+ ) ;
467+ }
468+ continue ;
469+ }
470+ Err ( err) => return Err ( err) ,
471+ } ;
472+ let cow = either_str. as_cow ( ) ?;
473+ if self . used_keys . contains ( cow. as_ref ( ) ) {
474+ continue ;
475+ }
476+
477+ let value = value. borrow_input ( ) ;
478+
479+ if self . extra_behavior == ExtraBehavior :: Forbid {
480+ self . errors . push ( ValLineError :: new_with_loc (
481+ ErrorTypeDefaults :: ExtraForbidden ,
482+ value,
483+ raw_key. clone ( ) ,
484+ ) ) ;
485+ }
486+ }
487+
488+ Ok ( ( ) )
489+ }
490+ }
491+
492+ mapping. iterate ( ValidateExtra {
493+ used_keys,
494+ errors : & mut errors,
495+ extra_behavior : self . extra ,
496+ } ) ??;
497+ }
498+
371499 if !errors. is_empty ( ) {
372500 Err ( ValError :: LineErrors ( errors) )
373501 } else {
@@ -397,7 +525,7 @@ impl ArgumentsV3Validator {
397525 let validate_by_alias = state. validate_by_alias_or ( self . validate_by_alias ) ;
398526 let validate_by_name = state. validate_by_name_or ( self . validate_by_name ) ;
399527
400- // go through non variadic arguments , getting the value from args or kwargs and validating it
528+ // go through non variadic parameters , getting the value from args or kwargs and validating it
401529 for ( index, parameter) in self . parameters . iter ( ) . filter ( |p| !p. is_variadic ( ) ) . enumerate ( ) {
402530 let lookup_key = parameter
403531 . lookup_key_collection
0 commit comments