Skip to content

Commit

Permalink
Add 'frozen' to Config & SchemaValidator (#462)
Browse files Browse the repository at this point in the history
* create specfic frozen errors for model and field respectively

* add 'frozen' as field for SchemaValidator and add it to the representer

* simplify error response in validate_assignment for schemavalidator

* Rename Error FrozenModel -> FrozenInstance

* move frozen to ModelSchema

* a bit more cleanup

---------

Co-authored-by: Samuel Colvin <[email protected]>
  • Loading branch information
realDragonium and samuelcolvin authored Mar 27, 2023
1 parent d74ff2e commit 40b72a3
Show file tree
Hide file tree
Showing 7 changed files with 46 additions and 5 deletions.
7 changes: 6 additions & 1 deletion pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2558,6 +2558,7 @@ class ModelSchema(TypedDict, total=False):
schema: Required[CoreSchema]
post_init: str
strict: bool
frozen: bool
config: CoreConfig
ref: str
metadata: Any
Expand All @@ -2570,6 +2571,7 @@ def model_schema(
*,
post_init: str | None = None,
strict: bool | None = None,
frozen: bool | None = None,
config: CoreConfig | None = None,
ref: str | None = None,
metadata: Any = None,
Expand Down Expand Up @@ -2606,6 +2608,7 @@ class MyModel:
schema: The schema to use for the model
post_init: The call after init to use for the model
strict: Whether the model is strict
frozen: Whether the model is frozen
config: The config to use for the model
ref: See [TODO] for details
metadata: See [TODO] for details
Expand All @@ -2617,6 +2620,7 @@ class MyModel:
schema=schema,
post_init=post_init,
strict=strict,
frozen=frozen,
config=config,
ref=ref,
metadata=metadata,
Expand Down Expand Up @@ -3349,7 +3353,8 @@ def definition_reference_schema(
'recursion_loop',
'dict_attributes_type',
'missing',
'frozen',
'frozen_field',
'frozen_instance',
'extra_forbidden',
'invalid_key',
'get_attribute_error',
Expand Down
4 changes: 3 additions & 1 deletion src/errors/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ pub enum ErrorType {
#[strum(message = "Field required")]
Missing,
#[strum(message = "Field is frozen")]
Frozen,
FrozenField,
#[strum(message = "Instance is frozen")]
FrozenInstance,
#[strum(message = "Extra inputs are not permitted")]
ExtraForbidden,
#[strum(message = "Keys should be strings")]
Expand Down
5 changes: 5 additions & 0 deletions src/validators/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub struct ModelValidator {
post_init: Option<Py<PyString>>,
name: String,
expect_fields_set: bool,
frozen: bool,
}

impl BuildValidator for ModelValidator {
Expand Down Expand Up @@ -59,6 +60,7 @@ impl BuildValidator for ModelValidator {
// which is not what we want here
name: class.getattr(intern!(py, "__name__"))?.extract()?,
expect_fields_set,
frozen: schema.get_as(intern!(py, "frozen"))?.unwrap_or(false),
}
.into())
}
Expand Down Expand Up @@ -175,6 +177,9 @@ impl ModelValidator {
slots: &'data [CombinedValidator],
recursion_guard: &'s mut RecursionGuard,
) -> ValResult<'data, PyObject> {
if self.frozen {
return Err(ValError::new(ErrorType::FrozenInstance, input));
}
// inner validator takes care of updating dict, here we just need to update fields_set
let next_extra = Extra {
self_instance: self_instance.get_attr(intern!(py, "__dict__")),
Expand Down
6 changes: 5 additions & 1 deletion src/validators/typed_dict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,11 @@ impl TypedDictValidator {

if let Some(field) = self.fields.iter().find(|f| f.name == field) {
if field.frozen {
Err(ValError::new_with_loc(ErrorType::Frozen, input, field.name.to_string()))
Err(ValError::new_with_loc(
ErrorType::FrozenField,
input,
field.name.to_string(),
))
} else {
prepare_result(field.validator.validate(py, input, &extra, slots, recursion_guard))
}
Expand Down
3 changes: 2 additions & 1 deletion tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ def f(input_value, info):
('recursion_loop', 'Recursion error - cyclic reference detected', None),
('dict_attributes_type', 'Input should be a valid dictionary or instance to extract fields from', None),
('missing', 'Field required', None),
('frozen', 'Field is frozen', None),
('frozen_field', 'Field is frozen', None),
('frozen_instance', 'Instance is frozen', None),
('extra_forbidden', 'Extra inputs are not permitted', None),
('invalid_key', 'Keys should be strings', None),
('get_attribute_error', 'Error extracting attribute: foo', {'error': 'foo'}),
Expand Down
24 changes: 24 additions & 0 deletions tests/validators/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,3 +839,27 @@ class MyModel:
# wrong arguments
with pytest.raises(TypeError, match='self_instance should not be None on typed-dict validate_assignment'):
v.validate_assignment('field_a', 'field_a', b'different')


def test_frozen():
class MyModel:
__slots__ = {'__dict__'}

v = SchemaValidator(
core_schema.model_schema(
MyModel,
core_schema.typed_dict_schema({'f': core_schema.typed_dict_field(core_schema.str_schema())}),
frozen=True,
)
)

m = v.validate_python({'f': 'x'})
assert m.f == 'x'

with pytest.raises(ValidationError) as exc_info:
v.validate_assignment(m, 'f', 'y')

# insert_assert(exc_info.value.errors())
assert exc_info.value.errors() == [
{'type': 'frozen_instance', 'loc': (), 'msg': 'Instance is frozen', 'input': 'y'}
]
2 changes: 1 addition & 1 deletion tests/validators/test_typed_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -1628,5 +1628,5 @@ def test_frozen_field():
with pytest.raises(ValidationError) as exc_info:
v.validate_assignment(r1, 'is_developer', False)
assert exc_info.value.errors() == [
{'type': 'frozen', 'loc': ('is_developer',), 'msg': 'Field is frozen', 'input': False}
{'type': 'frozen_field', 'loc': ('is_developer',), 'msg': 'Field is frozen', 'input': False}
]

0 comments on commit 40b72a3

Please sign in to comment.