From 0c07f9a8e5282a48aca5490e05e95979736f0812 Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Thu, 21 Nov 2024 21:01:16 +0000 Subject: [PATCH 1/4] raise warnings when deprecated fields are filled --- python/pydantic_core/core_schema.py | 4 ++++ src/validators/model_fields.rs | 7 +++++++ tests/validators/test_model_fields.py | 23 +++++++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index c023a5635..7aede0ef6 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -2944,6 +2944,7 @@ class ModelField(TypedDict, total=False): serialization_alias: str serialization_exclude: bool # default: False frozen: bool + deprecation_msg: str metadata: Dict[str, Any] @@ -2954,6 +2955,7 @@ def model_field( serialization_alias: str | None = None, serialization_exclude: bool | None = None, frozen: bool | None = None, + deprecation_msg: str | None = None, metadata: Dict[str, Any] | None = None, ) -> ModelField: """ @@ -2971,6 +2973,7 @@ def model_field( serialization_alias: The alias to use as a key when serializing serialization_exclude: Whether to exclude the field when serializing frozen: Whether the field is frozen + deprecation_msg: A deprecation message indicating that the field is deprecated. `None` means that the field is not deprecated. metadata: Any other information you want to include with the schema, not used by pydantic-core """ return _dict_not_none( @@ -2980,6 +2983,7 @@ def model_field( serialization_alias=serialization_alias, serialization_exclude=serialization_exclude, frozen=frozen, + deprecation_msg=deprecation_msg, metadata=metadata, ) diff --git a/src/validators/model_fields.rs b/src/validators/model_fields.rs index 7aba7c8e3..ecdd0dbda 100644 --- a/src/validators/model_fields.rs +++ b/src/validators/model_fields.rs @@ -23,6 +23,7 @@ struct Field { name_py: Py, validator: CombinedValidator, frozen: bool, + deprecation_msg: Option, } impl_py_gc_traverse!(Field { validator }); @@ -92,6 +93,7 @@ impl BuildValidator for ModelFieldsValidator { name_py: field_name_py.into(), validator, frozen: field_info.get_as::(intern!(py, "frozen"))?.unwrap_or(false), + deprecation_msg: field_info.get_as::(intern!(py, "deprecation_msg"))?, }); } @@ -123,6 +125,8 @@ impl Validator for ModelFieldsValidator { // this validator does not yet support partial validation, disable it to avoid incorrect results state.allow_partial = false.into(); + let deprecation_warning_type = py.import_bound("builtins")?.getattr("DeprecationWarning")?; + let strict = state.strict_or(self.strict); let from_attributes = state.extra().from_attributes.unwrap_or(self.from_attributes); @@ -184,6 +188,9 @@ impl Validator for ModelFieldsValidator { // extra logic either way used_keys.insert(lookup_path.first_key()); } + if let Some(msg) = &field.deprecation_msg { + PyErr::warn_bound(py, &deprecation_warning_type, msg, 2)?; + } match field.validator.validate(py, value.borrow_input(), state) { Ok(value) => { model_dict.set_item(&field.name_py, value)?; diff --git a/tests/validators/test_model_fields.py b/tests/validators/test_model_fields.py index 8a22d96c3..2226f7728 100644 --- a/tests/validators/test_model_fields.py +++ b/tests/validators/test_model_fields.py @@ -1781,3 +1781,26 @@ def test_extra_behavior_ignore(config: Union[core_schema.CoreConfig, None], sche } ] assert 'not_f' not in m + + +def test_deprecation_msg(): + v = SchemaValidator( + { + 'type': 'model-fields', + 'fields': { + 'a': {'type': 'model-field', 'schema': {'type': 'int'}}, + 'b': { + 'type': 'model-field', + 'schema': {'type': 'default', 'schema': {'type': 'int'}, 'default': 2}, + 'deprecation_msg': 'hi', + }, + }, + } + ) + + # not touching the deprecated field: no warning + v.validate_python({'a': 1}) + + # validating the deprecated field: raise warning + with pytest.warns(DeprecationWarning, match='hi'): + v.validate_python({'a': 1, 'b': 1}) From 4a8aa3eecb78115f99a0048958d84e828e49348d Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Thu, 21 Nov 2024 21:20:25 +0000 Subject: [PATCH 2/4] try moving import --- src/validators/model_fields.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/validators/model_fields.rs b/src/validators/model_fields.rs index ecdd0dbda..95380c87f 100644 --- a/src/validators/model_fields.rs +++ b/src/validators/model_fields.rs @@ -125,8 +125,6 @@ impl Validator for ModelFieldsValidator { // this validator does not yet support partial validation, disable it to avoid incorrect results state.allow_partial = false.into(); - let deprecation_warning_type = py.import_bound("builtins")?.getattr("DeprecationWarning")?; - let strict = state.strict_or(self.strict); let from_attributes = state.extra().from_attributes.unwrap_or(self.from_attributes); @@ -189,6 +187,7 @@ impl Validator for ModelFieldsValidator { used_keys.insert(lookup_path.first_key()); } if let Some(msg) = &field.deprecation_msg { + let deprecation_warning_type = py.import_bound("builtins")?.getattr("DeprecationWarning")?; PyErr::warn_bound(py, &deprecation_warning_type, msg, 2)?; } match field.validator.validate(py, value.borrow_input(), state) { From 8e85a60e2f1cc3574435f14175f5d1a87e8cdb6a Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Thu, 21 Nov 2024 21:38:28 +0000 Subject: [PATCH 3/4] revise test --- tests/validators/test_model_fields.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/validators/test_model_fields.py b/tests/validators/test_model_fields.py index 2226f7728..bc88239a2 100644 --- a/tests/validators/test_model_fields.py +++ b/tests/validators/test_model_fields.py @@ -1792,7 +1792,12 @@ def test_deprecation_msg(): 'b': { 'type': 'model-field', 'schema': {'type': 'default', 'schema': {'type': 'int'}, 'default': 2}, - 'deprecation_msg': 'hi', + 'deprecation_msg': 'foo', + }, + 'c': { + 'type': 'model-field', + 'schema': {'type': 'default', 'schema': {'type': 'int'}, 'default': 2}, + 'deprecation_msg': 'bar', }, }, } @@ -1802,5 +1807,9 @@ def test_deprecation_msg(): v.validate_python({'a': 1}) # validating the deprecated field: raise warning - with pytest.warns(DeprecationWarning, match='hi'): - v.validate_python({'a': 1, 'b': 1}) + # ensure that we get two warnings + with pytest.warns(DeprecationWarning) as w: + v.validate_python({'a': 1, 'b': 1, 'c': 1}) + assert len(w) == 2 + assert str(w[0].message) == 'foo' + assert str(w[1].message) == 'bar' From a19365c9cc7982b84fffbe9a74da401438fc868c Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Tue, 26 Nov 2024 22:55:50 +0000 Subject: [PATCH 4/4] use pyo3 error type --- src/validators/model_fields.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/validators/model_fields.rs b/src/validators/model_fields.rs index 95380c87f..7f57931e0 100644 --- a/src/validators/model_fields.rs +++ b/src/validators/model_fields.rs @@ -1,7 +1,8 @@ -use pyo3::exceptions::PyKeyError; +use pyo3::exceptions::{PyDeprecationWarning, PyKeyError}; use pyo3::intern; use pyo3::prelude::*; use pyo3::types::{PyDict, PySet, PyString, PyType}; +use pyo3::PyTypeInfo; use ahash::AHashSet; @@ -187,7 +188,7 @@ impl Validator for ModelFieldsValidator { used_keys.insert(lookup_path.first_key()); } if let Some(msg) = &field.deprecation_msg { - let deprecation_warning_type = py.import_bound("builtins")?.getattr("DeprecationWarning")?; + let deprecation_warning_type = PyDeprecationWarning::type_object_bound(py); PyErr::warn_bound(py, &deprecation_warning_type, msg, 2)?; } match field.validator.validate(py, value.borrow_input(), state) {