@@ -5,7 +5,7 @@ use pyo3::types::{PyDict, PyList, PyString, PyTuple, PyType};
5
5
6
6
use ahash:: AHashSet ;
7
7
8
- use crate :: build_tools:: { is_strict, py_err, schema_or_config_same, SchemaDict } ;
8
+ use crate :: build_tools:: { is_strict, py_err, schema_or_config_same, ExtraBehavior , SchemaDict } ;
9
9
use crate :: errors:: { ErrorType , ValError , ValLineError , ValResult } ;
10
10
use crate :: input:: { GenericArguments , Input } ;
11
11
use crate :: lookup_key:: LookupKey ;
@@ -24,6 +24,7 @@ struct Field {
24
24
init_only : bool ,
25
25
lookup_key : LookupKey ,
26
26
validator : CombinedValidator ,
27
+ frozen : bool ,
27
28
}
28
29
29
30
#[ derive( Debug , Clone ) ]
@@ -33,6 +34,7 @@ pub struct DataclassArgsValidator {
33
34
init_only_count : Option < usize > ,
34
35
dataclass_name : String ,
35
36
validator_name : String ,
37
+ extra_behavior : ExtraBehavior ,
36
38
}
37
39
38
40
impl BuildValidator for DataclassArgsValidator {
@@ -47,6 +49,8 @@ impl BuildValidator for DataclassArgsValidator {
47
49
48
50
let populate_by_name = schema_or_config_same ( schema, config, intern ! ( py, "populate_by_name" ) ) ?. unwrap_or ( false ) ;
49
51
52
+ let extra_behavior = ExtraBehavior :: from_schema_or_config ( py, schema, config, ExtraBehavior :: Ignore ) ?;
53
+
50
54
let fields_schema: & PyList = schema. get_as_req ( intern ! ( py, "fields" ) ) ?;
51
55
let mut fields: Vec < Field > = Vec :: with_capacity ( fields_schema. len ( ) ) ;
52
56
@@ -91,6 +95,7 @@ impl BuildValidator for DataclassArgsValidator {
91
95
lookup_key,
92
96
validator,
93
97
init_only : field. get_as ( intern ! ( py, "init_only" ) ) ?. unwrap_or ( false ) ,
98
+ frozen : field. get_as :: < bool > ( intern ! ( py, "frozen" ) ) ?. unwrap_or ( false ) ,
94
99
} ) ;
95
100
}
96
101
@@ -108,6 +113,7 @@ impl BuildValidator for DataclassArgsValidator {
108
113
init_only_count,
109
114
dataclass_name,
110
115
validator_name,
116
+ extra_behavior,
111
117
}
112
118
. into ( ) )
113
119
}
@@ -254,11 +260,20 @@ impl Validator for DataclassArgsValidator {
254
260
match raw_key. strict_str( ) {
255
261
Ok ( either_str) => {
256
262
if !used_keys. contains( either_str. as_cow( ) ?. as_ref( ) ) {
257
- errors. push( ValLineError :: new_with_loc(
258
- ErrorType :: UnexpectedKeywordArgument ,
259
- value,
260
- raw_key. as_loc_item( ) ,
261
- ) ) ;
263
+ // Unknown / extra field
264
+ match self . extra_behavior {
265
+ ExtraBehavior :: Forbid => {
266
+ errors. push( ValLineError :: new_with_loc(
267
+ ErrorType :: UnexpectedKeywordArgument ,
268
+ value,
269
+ raw_key. as_loc_item( ) ,
270
+ ) ) ;
271
+ }
272
+ ExtraBehavior :: Ignore => { }
273
+ ExtraBehavior :: Allow => {
274
+ output_dict. set_item( either_str. as_py_string( py) , value) ?
275
+ }
276
+ }
262
277
}
263
278
}
264
279
Err ( ValError :: LineErrors ( line_errors) ) => {
@@ -303,7 +318,19 @@ impl Validator for DataclassArgsValidator {
303
318
) -> ValResult < ' data , PyObject > {
304
319
let dict: & PyDict = obj. downcast ( ) ?;
305
320
321
+ let ok = |output : PyObject | {
322
+ dict. set_item ( field_name, output) ?;
323
+ Ok ( dict. to_object ( py) )
324
+ } ;
325
+
306
326
if let Some ( field) = self . fields . iter ( ) . find ( |f| f. name == field_name) {
327
+ if field. frozen {
328
+ return Err ( ValError :: new_with_loc (
329
+ ErrorType :: FrozenField ,
330
+ field_value,
331
+ field. name . to_string ( ) ,
332
+ ) ) ;
333
+ }
307
334
// by using dict but removing the field in question, we match V1 behaviour
308
335
let data_dict = dict. copy ( ) ?;
309
336
if let Err ( err) = data_dict. del_item ( field_name) {
@@ -321,10 +348,7 @@ impl Validator for DataclassArgsValidator {
321
348
. validator
322
349
. validate ( py, field_value, & next_extra, slots, recursion_guard)
323
350
{
324
- Ok ( output) => {
325
- dict. set_item ( field_name, output) ?;
326
- Ok ( dict. to_object ( py) )
327
- }
351
+ Ok ( output) => ok ( output) ,
328
352
Err ( ValError :: LineErrors ( line_errors) ) => {
329
353
let errors = line_errors
330
354
. into_iter ( )
@@ -335,13 +359,21 @@ impl Validator for DataclassArgsValidator {
335
359
Err ( err) => Err ( err) ,
336
360
}
337
361
} else {
338
- Err ( ValError :: new_with_loc (
339
- ErrorType :: NoSuchAttribute {
340
- attribute : field_name. to_string ( ) ,
341
- } ,
342
- field_value,
343
- field_name. to_string ( ) ,
344
- ) )
362
+ // Handle extra (unknown) field
363
+ // We partially use the extra_behavior for initialization / validation
364
+ // to determine how to handle assignment
365
+ match self . extra_behavior {
366
+ // For dataclasses we allow assigning unknown fields
367
+ // to match stdlib dataclass behavior
368
+ ExtraBehavior :: Allow => ok ( field_value. to_object ( py) ) ,
369
+ _ => Err ( ValError :: new_with_loc (
370
+ ErrorType :: NoSuchAttribute {
371
+ attribute : field_name. to_string ( ) ,
372
+ } ,
373
+ field_value,
374
+ field_name. to_string ( ) ,
375
+ ) ) ,
376
+ }
345
377
}
346
378
}
347
379
@@ -364,6 +396,7 @@ pub struct DataclassValidator {
364
396
post_init : Option < Py < PyString > > ,
365
397
revalidate : Revalidate ,
366
398
name : String ,
399
+ frozen : bool ,
367
400
}
368
401
369
402
impl BuildValidator for DataclassValidator {
@@ -399,6 +432,7 @@ impl BuildValidator for DataclassValidator {
399
432
// as with model, get the class's `__name__`, not using `class.name()` since it uses `__qualname__`
400
433
// which is not what we want here
401
434
name : class. getattr ( intern ! ( py, "__name__" ) ) ?. extract ( ) ?,
435
+ frozen : schema. get_as ( intern ! ( py, "frozen" ) ) ?. unwrap_or ( false ) ,
402
436
}
403
437
. into ( ) )
404
438
}
@@ -455,6 +489,9 @@ impl Validator for DataclassValidator {
455
489
slots : & ' data [ CombinedValidator ] ,
456
490
recursion_guard : & ' s mut RecursionGuard ,
457
491
) -> ValResult < ' data , PyObject > {
492
+ if self . frozen {
493
+ return Err ( ValError :: new ( ErrorType :: FrozenInstance , field_value) ) ;
494
+ }
458
495
let dict_py_str = intern ! ( py, "__dict__" ) ;
459
496
let dict: & PyDict = obj. getattr ( dict_py_str) ?. downcast ( ) ?;
460
497
0 commit comments