From 9d7475ceb96d93acad694d1b92533fd2c5d1d07b Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Mon, 9 Dec 2024 22:41:10 +0000 Subject: [PATCH 01/11] implement never --- python/pydantic_core/core_schema.py | 42 ++++++++++++++++ src/errors/types.rs | 2 + src/serializers/shared.rs | 2 + src/serializers/type_serializers/mod.rs | 1 + src/serializers/type_serializers/model.rs | 7 ++- src/serializers/type_serializers/never.rs | 57 +++++++++++++++++++++ src/validators/mod.rs | 3 ++ src/validators/never.rs | 60 +++++++++++++++++++++++ tests/serializers/test_model.py | 24 ++++++++- tests/serializers/test_never.py | 17 +++++++ tests/validators/test_never.py | 40 +++++++++++++++ 11 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 src/serializers/type_serializers/never.rs create mode 100644 src/validators/never.rs create mode 100644 tests/serializers/test_never.py create mode 100644 tests/validators/test_never.py diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 0fcfe9cab..7409a59bf 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -3858,6 +3858,46 @@ def definition_reference_schema( ) +class NeverSchema(TypedDict, total=False): + type: Required[Literal['never']] + ref: str + metadata: Dict[str, Any] + + +def never_schema( + *, + ref: str | None = None, + metadata: Dict[str, Any] | None = None, +) -> NeverSchema: + """ + Returns a schema that represents a `typing.Never` field, e.g.: + + ```py + from pydantic_core import SchemaValidator, core_schema + + schema = core_schema.never_schema() + v = SchemaValidator(schema) # should always fail + try: + assert v.validate_python(1) + except ValidationError: + pass + try: + assert v.validate_python('s') + except ValidationError: + pass + ``` + + Args: + ref: optional unique identifier of the schema, used to reference the schema in other places + metadata: Any other information you want to include with the schema, not used by pydantic-core + """ + return _dict_not_none( + type='never', + ref=ref, + metadata=metadata, + ) + + MYPY = False # See https://github.com/python/mypy/issues/14034 for details, in summary mypy is extremely slow to process this # union which kills performance not just for pydantic, but even for code using pydantic @@ -3913,6 +3953,7 @@ def definition_reference_schema( DefinitionReferenceSchema, UuidSchema, ComplexSchema, + NeverSchema, ] elif False: CoreSchema: TypeAlias = Mapping[str, Any] @@ -3970,6 +4011,7 @@ def definition_reference_schema( 'definition-ref', 'uuid', 'complex', + 'never', ] CoreSchemaFieldType = Literal['model-field', 'dataclass-field', 'typed-dict-field', 'computed-field'] diff --git a/src/errors/types.rs b/src/errors/types.rs index 07186003d..b70cc9f18 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -430,6 +430,7 @@ error_types! { // Complex errors ComplexType {}, ComplexStrParsing {}, + Never {}, } macro_rules! render { @@ -576,6 +577,7 @@ impl ErrorType { Self::DecimalWholeDigits {..} => "Decimal input should have no more than {whole_digits} digit{expected_plural} before the decimal point", Self::ComplexType {..} => "Input should be a valid python complex object, a number, or a valid complex string following the rules at https://docs.python.org/3/library/functions.html#complex", Self::ComplexStrParsing {..} => "Input should be a valid complex string following the rules at https://docs.python.org/3/library/functions.html#complex", + Self::Never { .. } => "Unexpected input for a field that should never be filled" } } diff --git a/src/serializers/shared.rs b/src/serializers/shared.rs index d3a87735f..fb5900a44 100644 --- a/src/serializers/shared.rs +++ b/src/serializers/shared.rs @@ -143,6 +143,7 @@ combined_serializer! { Recursive: super::type_serializers::definitions::DefinitionRefSerializer; Tuple: super::type_serializers::tuple::TupleSerializer; Complex: super::type_serializers::complex::ComplexSerializer; + Never: super::type_serializers::never::NeverSerializer; } } @@ -254,6 +255,7 @@ impl PyGcTraverse for CombinedSerializer { CombinedSerializer::Tuple(inner) => inner.py_gc_traverse(visit), CombinedSerializer::Uuid(inner) => inner.py_gc_traverse(visit), CombinedSerializer::Complex(inner) => inner.py_gc_traverse(visit), + CombinedSerializer::Never(inner) => inner.py_gc_traverse(visit), } } } diff --git a/src/serializers/type_serializers/mod.rs b/src/serializers/type_serializers/mod.rs index dabd006a3..811b8abb6 100644 --- a/src/serializers/type_serializers/mod.rs +++ b/src/serializers/type_serializers/mod.rs @@ -16,6 +16,7 @@ pub mod json_or_python; pub mod list; pub mod literal; pub mod model; +pub mod never; pub mod nullable; pub mod other; pub mod set_frozenset; diff --git a/src/serializers/type_serializers/model.rs b/src/serializers/type_serializers/model.rs index 36ddaf69f..26dbfd253 100644 --- a/src/serializers/type_serializers/model.rs +++ b/src/serializers/type_serializers/model.rs @@ -62,7 +62,12 @@ impl BuildSerializer for ModelFieldsBuilder { let serializer = CombinedSerializer::build(&schema, config, definitions) .map_err(|e| py_schema_error_type!("Field `{}`:\n {}", key, e))?; - fields.insert(key, SerField::new(py, key_py, alias, Some(serializer), true)); + match serializer { + CombinedSerializer::Never(_) => {} + s => { + fields.insert(key, SerField::new(py, key_py, alias, Some(s), true)); + } + } } } diff --git a/src/serializers/type_serializers/never.rs b/src/serializers/type_serializers/never.rs new file mode 100644 index 000000000..94ce6c387 --- /dev/null +++ b/src/serializers/type_serializers/never.rs @@ -0,0 +1,57 @@ +use super::{py_err_se_err, BuildSerializer, CombinedSerializer, Extra, TypeSerializer}; +use crate::definitions::DefinitionsBuilder; +use crate::tools::py_err; +use pyo3::exceptions::PyTypeError; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use std::borrow::Cow; + +const ERROR_MESSAGE: &str = "type `never` cannot be serialized"; + +#[derive(Debug)] +pub struct NeverSerializer; + +impl BuildSerializer for NeverSerializer { + const EXPECTED_TYPE: &'static str = "never"; + + fn build( + _schema: &Bound<'_, PyDict>, + _config: Option<&Bound<'_, PyDict>>, + _definitions: &mut DefinitionsBuilder, + ) -> PyResult { + Ok(Self {}.into()) + } +} + +impl_py_gc_traverse!(NeverSerializer {}); + +impl TypeSerializer for NeverSerializer { + fn to_python( + &self, + _value: &Bound<'_, PyAny>, + _include: Option<&Bound<'_, PyAny>>, + _exclude: Option<&Bound<'_, PyAny>>, + _extra: &Extra, + ) -> PyResult { + py_err!(PyTypeError; ERROR_MESSAGE) + } + + fn json_key<'a>(&self, _key: &'a Bound<'_, PyAny>, _extra: &Extra) -> PyResult> { + py_err!(PyTypeError; ERROR_MESSAGE) + } + + fn serde_serialize( + &self, + _value: &Bound<'_, PyAny>, + _serializer: S, + _include: Option<&Bound<'_, PyAny>>, + _exclude: Option<&Bound<'_, PyAny>>, + _extra: &Extra, + ) -> Result { + py_err!(PyTypeError; ERROR_MESSAGE).map_err(py_err_se_err) + } + + fn get_name(&self) -> &str { + Self::EXPECTED_TYPE + } +} diff --git a/src/validators/mod.rs b/src/validators/mod.rs index cafdfcd12..d6d6c522f 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -49,6 +49,7 @@ mod list; mod literal; mod model; mod model_fields; +mod never; mod none; mod nullable; mod set; @@ -611,6 +612,7 @@ pub fn build_validator( definitions::DefinitionRefValidator, definitions::DefinitionsValidatorBuilder, complex::ComplexValidator, + never::NeverValidator, ) } @@ -765,6 +767,7 @@ pub enum CombinedValidator { // input dependent JsonOrPython(json_or_python::JsonOrPython), Complex(complex::ComplexValidator), + Never(never::NeverValidator), } /// This trait must be implemented by all validators, it allows various validators to be accessed consistently, diff --git a/src/validators/never.rs b/src/validators/never.rs new file mode 100644 index 000000000..1179bd469 --- /dev/null +++ b/src/validators/never.rs @@ -0,0 +1,60 @@ +use pyo3::prelude::*; +use pyo3::types::PyDict; + +use crate::errors::{ErrorTypeDefaults, ValError, ValResult}; +use crate::input::Input; +use crate::PydanticUndefinedType; + +use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, LocItem, ValidationState, Validator}; + +#[derive(Debug)] +pub struct NeverValidator { + undefined: PyObject, +} + +impl BuildValidator for NeverValidator { + const EXPECTED_TYPE: &'static str = "never"; + + fn build( + schema: &Bound<'_, PyDict>, + _config: Option<&Bound<'_, PyDict>>, + _definitions: &mut DefinitionsBuilder, + ) -> PyResult { + let py = schema.py(); + Ok(Self { + undefined: PydanticUndefinedType::new(py).to_object(py), + } + .into()) + } +} + +impl_py_gc_traverse!(NeverValidator {}); + +impl Validator for NeverValidator { + fn validate<'py>( + &self, + py: Python<'py>, + input: &(impl Input<'py> + ?Sized), + _state: &mut ValidationState<'_, 'py>, + ) -> ValResult { + let obj = input.to_object(py); + if obj.is(&self.undefined) { + Ok(obj) + } else { + Err(ValError::new(ErrorTypeDefaults::Never, input)) + } + } + + fn default_value<'py>( + &self, + _py: Python<'py>, + _outer_loc: Option>, + _state: &mut ValidationState<'_, 'py>, + ) -> ValResult> { + Ok(Some(self.undefined.clone())) + } + + fn get_name(&self) -> &str { + Self::EXPECTED_TYPE + } +} diff --git a/tests/serializers/test_model.py b/tests/serializers/test_model.py index 9fa44032a..d15e054c7 100644 --- a/tests/serializers/test_model.py +++ b/tests/serializers/test_model.py @@ -3,7 +3,7 @@ import platform import warnings from random import randint -from typing import Any, ClassVar, Dict +from typing import Any, ClassVar, Dict, Never try: from functools import cached_property @@ -1152,3 +1152,25 @@ class BModel(BasicModel): ... with pytest.warns(UserWarning, match='Expected 2 fields but got 1 for type `.*AModel` with value `.*`.+'): value = BasicModel(root=AModel(type='a')) s.to_python(value) + + +def test_never(): + class MyModel: + a: int + b: Never + + schema = core_schema.model_schema( + MyModel, + core_schema.model_fields_schema( + { + 'a': core_schema.model_field(core_schema.int_schema()), + 'b': core_schema.model_field(core_schema.never_schema()), + } + ), + ) + v = SchemaValidator(schema) + m = v.validate_python({'a': 1}) + s = SchemaSerializer(schema) + # `b` should not break the serialiser or be serialised + assert s.to_python(m) == {'a': 1} + assert json.loads(s.to_json(m)) == {'a': 1} diff --git a/tests/serializers/test_never.py b/tests/serializers/test_never.py new file mode 100644 index 000000000..4a57b89fd --- /dev/null +++ b/tests/serializers/test_never.py @@ -0,0 +1,17 @@ +import pytest + +from pydantic_core import PydanticSerializationError, SchemaSerializer, core_schema + + +def test_to_python_never(): + v = SchemaSerializer(core_schema.never_schema()) + with pytest.raises(TypeError) as exc_info: + v.to_python(1) + assert str(exc_info.value) == 'type `never` cannot be serialized' + + +def test_to_json_never(): + v = SchemaSerializer(core_schema.never_schema()) + with pytest.raises(PydanticSerializationError) as exc_info: + v.to_json('null') + assert 'type `never` cannot be serialized' in str(exc_info.value) diff --git a/tests/validators/test_never.py b/tests/validators/test_never.py new file mode 100644 index 000000000..34f7ca09c --- /dev/null +++ b/tests/validators/test_never.py @@ -0,0 +1,40 @@ +from typing import Never + +import pytest + +from pydantic_core import PydanticUndefined, SchemaValidator, ValidationError, core_schema + + +def test_python_never(): + v = SchemaValidator(core_schema.never_schema()) + with pytest.raises(ValidationError) as exc_info: + v.validate_python(1) + assert exc_info.value.errors(include_url=False) == [ + {'type': 'never', 'loc': (), 'msg': 'Unexpected input for a field that should never be filled', 'input': 1} + ] + + assert v.validate_python(PydanticUndefined) is PydanticUndefined + + +def test_json_never(): + v = SchemaValidator(core_schema.never_schema()) + with pytest.raises(ValidationError) as exc_info: + v.validate_json('null') + assert exc_info.value.errors(include_url=False) == [ + {'type': 'never', 'loc': (), 'msg': 'Unexpected input for a field that should never be filled', 'input': None} + ] + + class MyModel: + a: Never + + schema = core_schema.model_schema( + MyModel, + core_schema.model_fields_schema( + { + 'a': core_schema.model_field(core_schema.never_schema()), + } + ), + ) + v = SchemaValidator(schema) + m = v.validate_json('{}') + assert m.a is PydanticUndefined From eef89021e2f78b7ea72ae39eb5a5641badc59e0c Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Tue, 10 Dec 2024 00:08:12 +0000 Subject: [PATCH 02/11] fix tests --- tests/serializers/test_model.py | 6 +++++- tests/test_schema_functions.py | 1 + tests/validators/test_never.py | 5 ++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/serializers/test_model.py b/tests/serializers/test_model.py index d15e054c7..5dc39f2df 100644 --- a/tests/serializers/test_model.py +++ b/tests/serializers/test_model.py @@ -1,9 +1,10 @@ import dataclasses import json import platform +import sys import warnings from random import randint -from typing import Any, ClassVar, Dict, Never +from typing import Any, ClassVar, Dict try: from functools import cached_property @@ -1154,7 +1155,10 @@ class BModel(BasicModel): ... s.to_python(value) +@pytest.mark.skipif(sys.version_info < (3, 11), reason='typing.Never was introduced in 3.11') def test_never(): + from typing import Never + class MyModel: a: int b: Never diff --git a/tests/test_schema_functions.py b/tests/test_schema_functions.py index a15adfca5..d2bc9ef13 100644 --- a/tests/test_schema_functions.py +++ b/tests/test_schema_functions.py @@ -291,6 +291,7 @@ def args(*args, **kwargs): (core_schema.decimal_schema, args(), {'type': 'decimal'}), (core_schema.decimal_schema, args(multiple_of=5, gt=1.2), {'type': 'decimal', 'multiple_of': 5, 'gt': 1.2}), (core_schema.complex_schema, args(), {'type': 'complex'}), + (core_schema.never_schema, args(), {'type': 'never'}), (core_schema.invalid_schema, args(), {'type': 'invalid'}), ] diff --git a/tests/validators/test_never.py b/tests/validators/test_never.py index 34f7ca09c..68b3529f0 100644 --- a/tests/validators/test_never.py +++ b/tests/validators/test_never.py @@ -1,4 +1,4 @@ -from typing import Never +import sys import pytest @@ -16,7 +16,10 @@ def test_python_never(): assert v.validate_python(PydanticUndefined) is PydanticUndefined +@pytest.mark.skipif(sys.version_info < (3, 11), reason='typing.Never was introduced in 3.11') def test_json_never(): + from typing import Never + v = SchemaValidator(core_schema.never_schema()) with pytest.raises(ValidationError) as exc_info: v.validate_json('null') From 9f71bfbba62b8de5abf83e6de2e068bbcd280694 Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Tue, 10 Dec 2024 00:11:17 +0000 Subject: [PATCH 03/11] fix tests --- python/pydantic_core/core_schema.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 7409a59bf..b54caf05d 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -4120,6 +4120,7 @@ def never_schema( 'decimal_whole_digits', 'complex_type', 'complex_str_parsing', + 'never', ] From 119479842cbc5097c40ba2fb328eddd6e15bea26 Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Tue, 10 Dec 2024 08:25:12 +0000 Subject: [PATCH 04/11] fix tests --- python/pydantic_core/core_schema.py | 3 ++- tests/test_errors.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index b54caf05d..161dd437d 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -3876,7 +3876,8 @@ def never_schema( from pydantic_core import SchemaValidator, core_schema schema = core_schema.never_schema() - v = SchemaValidator(schema) # should always fail + v = SchemaValidator(schema) + # Validation should always fail try: assert v.validate_python(1) except ValidationError: diff --git a/tests/test_errors.py b/tests/test_errors.py index 4760baad7..6b5e72832 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -410,6 +410,7 @@ def f(input_value, info): 'Input should be a valid complex string following the rules at https://docs.python.org/3/library/functions.html#complex', None, ), + ('never', 'Unexpected input for a field that should never be filled', None), ] From 4ac8b387be49604094e544d75e147c9bf066104e Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Tue, 10 Dec 2024 08:31:21 +0000 Subject: [PATCH 05/11] fix tests --- python/pydantic_core/core_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 161dd437d..2420f149f 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -3873,7 +3873,7 @@ def never_schema( Returns a schema that represents a `typing.Never` field, e.g.: ```py - from pydantic_core import SchemaValidator, core_schema + from pydantic_core import SchemaValidator, core_schema, ValidationError schema = core_schema.never_schema() v = SchemaValidator(schema) From a3caf8008016fbe99c57cc5c3e67f54f94880618 Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Wed, 11 Dec 2024 07:34:58 +0000 Subject: [PATCH 06/11] update error message --- src/errors/types.rs | 2 +- tests/test_errors.py | 2 +- tests/validators/test_never.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/errors/types.rs b/src/errors/types.rs index b70cc9f18..f63510464 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -577,7 +577,7 @@ impl ErrorType { Self::DecimalWholeDigits {..} => "Decimal input should have no more than {whole_digits} digit{expected_plural} before the decimal point", Self::ComplexType {..} => "Input should be a valid python complex object, a number, or a valid complex string following the rules at https://docs.python.org/3/library/functions.html#complex", Self::ComplexStrParsing {..} => "Input should be a valid complex string following the rules at https://docs.python.org/3/library/functions.html#complex", - Self::Never { .. } => "Unexpected input for a field that should never be filled" + Self::Never { .. } => "No input is allowed for `typing.Never`" } } diff --git a/tests/test_errors.py b/tests/test_errors.py index 6b5e72832..1a6e73247 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -410,7 +410,7 @@ def f(input_value, info): 'Input should be a valid complex string following the rules at https://docs.python.org/3/library/functions.html#complex', None, ), - ('never', 'Unexpected input for a field that should never be filled', None), + ('never', 'No input is allowed for `typing.Never`', None), ] diff --git a/tests/validators/test_never.py b/tests/validators/test_never.py index 68b3529f0..46d42372b 100644 --- a/tests/validators/test_never.py +++ b/tests/validators/test_never.py @@ -10,7 +10,7 @@ def test_python_never(): with pytest.raises(ValidationError) as exc_info: v.validate_python(1) assert exc_info.value.errors(include_url=False) == [ - {'type': 'never', 'loc': (), 'msg': 'Unexpected input for a field that should never be filled', 'input': 1} + {'type': 'never', 'loc': (), 'msg': 'No input is allowed for `typing.Never`', 'input': 1} ] assert v.validate_python(PydanticUndefined) is PydanticUndefined @@ -24,7 +24,7 @@ def test_json_never(): with pytest.raises(ValidationError) as exc_info: v.validate_json('null') assert exc_info.value.errors(include_url=False) == [ - {'type': 'never', 'loc': (), 'msg': 'Unexpected input for a field that should never be filled', 'input': None} + {'type': 'never', 'loc': (), 'msg': 'No input is allowed for `typing.Never`', 'input': None} ] class MyModel: From 185ef6dfd6372d1637729da661f4095aed495a6a Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Wed, 11 Dec 2024 08:06:28 +0000 Subject: [PATCH 07/11] update test --- tests/serializers/test_model.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/serializers/test_model.py b/tests/serializers/test_model.py index 5dc39f2df..ef9a5bab0 100644 --- a/tests/serializers/test_model.py +++ b/tests/serializers/test_model.py @@ -1,7 +1,6 @@ import dataclasses import json import platform -import sys import warnings from random import randint from typing import Any, ClassVar, Dict @@ -1155,13 +1154,9 @@ class BModel(BasicModel): ... s.to_python(value) -@pytest.mark.skipif(sys.version_info < (3, 11), reason='typing.Never was introduced in 3.11') def test_never(): - from typing import Never - class MyModel: - a: int - b: Never + pass schema = core_schema.model_schema( MyModel, From a896b4c233ac57fc5c572b80ba0dc3772b82f5d0 Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Wed, 11 Dec 2024 08:11:21 +0000 Subject: [PATCH 08/11] update test --- tests/validators/test_never.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/validators/test_never.py b/tests/validators/test_never.py index 46d42372b..8b0f81ea9 100644 --- a/tests/validators/test_never.py +++ b/tests/validators/test_never.py @@ -1,5 +1,3 @@ -import sys - import pytest from pydantic_core import PydanticUndefined, SchemaValidator, ValidationError, core_schema @@ -16,10 +14,7 @@ def test_python_never(): assert v.validate_python(PydanticUndefined) is PydanticUndefined -@pytest.mark.skipif(sys.version_info < (3, 11), reason='typing.Never was introduced in 3.11') def test_json_never(): - from typing import Never - v = SchemaValidator(core_schema.never_schema()) with pytest.raises(ValidationError) as exc_info: v.validate_json('null') @@ -28,7 +23,7 @@ def test_json_never(): ] class MyModel: - a: Never + pass schema = core_schema.model_schema( MyModel, From 3a555594d15d1bbb955ad8d307905a012edd3433 Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Wed, 11 Dec 2024 08:20:58 +0000 Subject: [PATCH 09/11] move error message --- python/pydantic_core/core_schema.py | 1 + src/errors/types.rs | 4 +++- src/serializers/type_serializers/never.rs | 9 ++++----- tests/test_errors.py | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 2420f149f..494f69a05 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -4122,6 +4122,7 @@ def never_schema( 'complex_type', 'complex_str_parsing', 'never', + 'never_serializing', ] diff --git a/src/errors/types.rs b/src/errors/types.rs index f63510464..7ea20802d 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -431,6 +431,7 @@ error_types! { ComplexType {}, ComplexStrParsing {}, Never {}, + NeverSerializing {}, } macro_rules! render { @@ -577,7 +578,8 @@ impl ErrorType { Self::DecimalWholeDigits {..} => "Decimal input should have no more than {whole_digits} digit{expected_plural} before the decimal point", Self::ComplexType {..} => "Input should be a valid python complex object, a number, or a valid complex string following the rules at https://docs.python.org/3/library/functions.html#complex", Self::ComplexStrParsing {..} => "Input should be a valid complex string following the rules at https://docs.python.org/3/library/functions.html#complex", - Self::Never { .. } => "No input is allowed for `typing.Never`" + Self::Never { .. } => "No input is allowed for `typing.Never`", + Self::NeverSerializing { .. } => "Type `typing.Never` cannot be serialized" } } diff --git a/src/serializers/type_serializers/never.rs b/src/serializers/type_serializers/never.rs index 94ce6c387..f0de59dff 100644 --- a/src/serializers/type_serializers/never.rs +++ b/src/serializers/type_serializers/never.rs @@ -1,13 +1,12 @@ use super::{py_err_se_err, BuildSerializer, CombinedSerializer, Extra, TypeSerializer}; use crate::definitions::DefinitionsBuilder; +use crate::errors::ErrorTypeDefaults; use crate::tools::py_err; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::types::PyDict; use std::borrow::Cow; -const ERROR_MESSAGE: &str = "type `never` cannot be serialized"; - #[derive(Debug)] pub struct NeverSerializer; @@ -33,11 +32,11 @@ impl TypeSerializer for NeverSerializer { _exclude: Option<&Bound<'_, PyAny>>, _extra: &Extra, ) -> PyResult { - py_err!(PyTypeError; ERROR_MESSAGE) + py_err!(PyTypeError; ErrorTypeDefaults::Never.message_template_python()) } fn json_key<'a>(&self, _key: &'a Bound<'_, PyAny>, _extra: &Extra) -> PyResult> { - py_err!(PyTypeError; ERROR_MESSAGE) + py_err!(PyTypeError; ErrorTypeDefaults::Never.message_template_python()) } fn serde_serialize( @@ -48,7 +47,7 @@ impl TypeSerializer for NeverSerializer { _exclude: Option<&Bound<'_, PyAny>>, _extra: &Extra, ) -> Result { - py_err!(PyTypeError; ERROR_MESSAGE).map_err(py_err_se_err) + py_err!(PyTypeError; ErrorTypeDefaults::Never.message_template_python()).map_err(py_err_se_err) } fn get_name(&self) -> &str { diff --git a/tests/test_errors.py b/tests/test_errors.py index 1a6e73247..94e1555ba 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -411,6 +411,7 @@ def f(input_value, info): None, ), ('never', 'No input is allowed for `typing.Never`', None), + ('never_serializing', 'Type `typing.Never` cannot be serialized', None), ] From 47c30364c946e44090e07872c449c97435740b9d Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Wed, 11 Dec 2024 22:39:22 +0000 Subject: [PATCH 10/11] fix tests --- src/serializers/type_serializers/never.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/serializers/type_serializers/never.rs b/src/serializers/type_serializers/never.rs index f0de59dff..060fac9ff 100644 --- a/src/serializers/type_serializers/never.rs +++ b/src/serializers/type_serializers/never.rs @@ -32,11 +32,11 @@ impl TypeSerializer for NeverSerializer { _exclude: Option<&Bound<'_, PyAny>>, _extra: &Extra, ) -> PyResult { - py_err!(PyTypeError; ErrorTypeDefaults::Never.message_template_python()) + py_err!(PyTypeError; ErrorTypeDefaults::NeverSerializing.message_template_python()) } fn json_key<'a>(&self, _key: &'a Bound<'_, PyAny>, _extra: &Extra) -> PyResult> { - py_err!(PyTypeError; ErrorTypeDefaults::Never.message_template_python()) + py_err!(PyTypeError; ErrorTypeDefaults::NeverSerializing.message_template_python()) } fn serde_serialize( @@ -47,7 +47,7 @@ impl TypeSerializer for NeverSerializer { _exclude: Option<&Bound<'_, PyAny>>, _extra: &Extra, ) -> Result { - py_err!(PyTypeError; ErrorTypeDefaults::Never.message_template_python()).map_err(py_err_se_err) + py_err!(PyTypeError; ErrorTypeDefaults::NeverSerializing.message_template_python()).map_err(py_err_se_err) } fn get_name(&self) -> &str { From 26df7ba142c0a7e27d20665b176131463f9c63fd Mon Sep 17 00:00:00 2001 From: Huan-Cheng Chang Date: Wed, 11 Dec 2024 22:42:24 +0000 Subject: [PATCH 11/11] fix tests --- tests/serializers/test_never.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/serializers/test_never.py b/tests/serializers/test_never.py index 4a57b89fd..ca4cf63d8 100644 --- a/tests/serializers/test_never.py +++ b/tests/serializers/test_never.py @@ -7,11 +7,11 @@ def test_to_python_never(): v = SchemaSerializer(core_schema.never_schema()) with pytest.raises(TypeError) as exc_info: v.to_python(1) - assert str(exc_info.value) == 'type `never` cannot be serialized' + assert str(exc_info.value) == 'Type `typing.Never` cannot be serialized' def test_to_json_never(): v = SchemaSerializer(core_schema.never_schema()) with pytest.raises(PydanticSerializationError) as exc_info: v.to_json('null') - assert 'type `never` cannot be serialized' in str(exc_info.value) + assert 'Type `typing.Never` cannot be serialized' in str(exc_info.value)