Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raise warnings when deprecated fields are filled at model instantiation #1551

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions python/pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]


Expand All @@ -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:
"""
Expand All @@ -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(
Expand All @@ -2980,6 +2983,7 @@ def model_field(
serialization_alias=serialization_alias,
serialization_exclude=serialization_exclude,
frozen=frozen,
deprecation_msg=deprecation_msg,
metadata=metadata,
)

Expand Down
9 changes: 8 additions & 1 deletion src/validators/model_fields.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -23,6 +24,7 @@ struct Field {
name_py: Py<PyString>,
validator: CombinedValidator,
frozen: bool,
deprecation_msg: Option<String>,
}

impl_py_gc_traverse!(Field { validator });
Expand Down Expand Up @@ -92,6 +94,7 @@ impl BuildValidator for ModelFieldsValidator {
name_py: field_name_py.into(),
validator,
frozen: field_info.get_as::<bool>(intern!(py, "frozen"))?.unwrap_or(false),
deprecation_msg: field_info.get_as::<String>(intern!(py, "deprecation_msg"))?,
});
}

Expand Down Expand Up @@ -184,6 +187,10 @@ impl Validator for ModelFieldsValidator {
// extra logic either way
used_keys.insert(lookup_path.first_key());
}
if let Some(msg) = &field.deprecation_msg {
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) {
Ok(value) => {
model_dict.set_item(&field.name_py, value)?;
Expand Down
32 changes: 32 additions & 0 deletions tests/validators/test_model_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -1781,3 +1781,35 @@ 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': 'foo',
},
'c': {
'type': 'model-field',
'schema': {'type': 'default', 'schema': {'type': 'int'}, 'default': 2},
'deprecation_msg': 'bar',
},
},
}
)

# not touching the deprecated field: no warning
v.validate_python({'a': 1})

# validating the deprecated field: raise warning
# 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'