Skip to content

Commit 9d7475c

Browse files
committed
implement never
1 parent 4c02cbd commit 9d7475c

File tree

11 files changed

+253
-2
lines changed

11 files changed

+253
-2
lines changed

python/pydantic_core/core_schema.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3858,6 +3858,46 @@ def definition_reference_schema(
38583858
)
38593859

38603860

3861+
class NeverSchema(TypedDict, total=False):
3862+
type: Required[Literal['never']]
3863+
ref: str
3864+
metadata: Dict[str, Any]
3865+
3866+
3867+
def never_schema(
3868+
*,
3869+
ref: str | None = None,
3870+
metadata: Dict[str, Any] | None = None,
3871+
) -> NeverSchema:
3872+
"""
3873+
Returns a schema that represents a `typing.Never` field, e.g.:
3874+
3875+
```py
3876+
from pydantic_core import SchemaValidator, core_schema
3877+
3878+
schema = core_schema.never_schema()
3879+
v = SchemaValidator(schema) # should always fail
3880+
try:
3881+
assert v.validate_python(1)
3882+
except ValidationError:
3883+
pass
3884+
try:
3885+
assert v.validate_python('s')
3886+
except ValidationError:
3887+
pass
3888+
```
3889+
3890+
Args:
3891+
ref: optional unique identifier of the schema, used to reference the schema in other places
3892+
metadata: Any other information you want to include with the schema, not used by pydantic-core
3893+
"""
3894+
return _dict_not_none(
3895+
type='never',
3896+
ref=ref,
3897+
metadata=metadata,
3898+
)
3899+
3900+
38613901
MYPY = False
38623902
# See https://github.com/python/mypy/issues/14034 for details, in summary mypy is extremely slow to process this
38633903
# union which kills performance not just for pydantic, but even for code using pydantic
@@ -3913,6 +3953,7 @@ def definition_reference_schema(
39133953
DefinitionReferenceSchema,
39143954
UuidSchema,
39153955
ComplexSchema,
3956+
NeverSchema,
39163957
]
39173958
elif False:
39183959
CoreSchema: TypeAlias = Mapping[str, Any]
@@ -3970,6 +4011,7 @@ def definition_reference_schema(
39704011
'definition-ref',
39714012
'uuid',
39724013
'complex',
4014+
'never',
39734015
]
39744016

39754017
CoreSchemaFieldType = Literal['model-field', 'dataclass-field', 'typed-dict-field', 'computed-field']

src/errors/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ error_types! {
430430
// Complex errors
431431
ComplexType {},
432432
ComplexStrParsing {},
433+
Never {},
433434
}
434435

435436
macro_rules! render {
@@ -576,6 +577,7 @@ impl ErrorType {
576577
Self::DecimalWholeDigits {..} => "Decimal input should have no more than {whole_digits} digit{expected_plural} before the decimal point",
577578
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",
578579
Self::ComplexStrParsing {..} => "Input should be a valid complex string following the rules at https://docs.python.org/3/library/functions.html#complex",
580+
Self::Never { .. } => "Unexpected input for a field that should never be filled"
579581
}
580582
}
581583

src/serializers/shared.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ combined_serializer! {
143143
Recursive: super::type_serializers::definitions::DefinitionRefSerializer;
144144
Tuple: super::type_serializers::tuple::TupleSerializer;
145145
Complex: super::type_serializers::complex::ComplexSerializer;
146+
Never: super::type_serializers::never::NeverSerializer;
146147
}
147148
}
148149

@@ -254,6 +255,7 @@ impl PyGcTraverse for CombinedSerializer {
254255
CombinedSerializer::Tuple(inner) => inner.py_gc_traverse(visit),
255256
CombinedSerializer::Uuid(inner) => inner.py_gc_traverse(visit),
256257
CombinedSerializer::Complex(inner) => inner.py_gc_traverse(visit),
258+
CombinedSerializer::Never(inner) => inner.py_gc_traverse(visit),
257259
}
258260
}
259261
}

src/serializers/type_serializers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod json_or_python;
1616
pub mod list;
1717
pub mod literal;
1818
pub mod model;
19+
pub mod never;
1920
pub mod nullable;
2021
pub mod other;
2122
pub mod set_frozenset;

src/serializers/type_serializers/model.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,12 @@ impl BuildSerializer for ModelFieldsBuilder {
6262
let serializer = CombinedSerializer::build(&schema, config, definitions)
6363
.map_err(|e| py_schema_error_type!("Field `{}`:\n {}", key, e))?;
6464

65-
fields.insert(key, SerField::new(py, key_py, alias, Some(serializer), true));
65+
match serializer {
66+
CombinedSerializer::Never(_) => {}
67+
s => {
68+
fields.insert(key, SerField::new(py, key_py, alias, Some(s), true));
69+
}
70+
}
6671
}
6772
}
6873

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use super::{py_err_se_err, BuildSerializer, CombinedSerializer, Extra, TypeSerializer};
2+
use crate::definitions::DefinitionsBuilder;
3+
use crate::tools::py_err;
4+
use pyo3::exceptions::PyTypeError;
5+
use pyo3::prelude::*;
6+
use pyo3::types::PyDict;
7+
use std::borrow::Cow;
8+
9+
const ERROR_MESSAGE: &str = "type `never` cannot be serialized";
10+
11+
#[derive(Debug)]
12+
pub struct NeverSerializer;
13+
14+
impl BuildSerializer for NeverSerializer {
15+
const EXPECTED_TYPE: &'static str = "never";
16+
17+
fn build(
18+
_schema: &Bound<'_, PyDict>,
19+
_config: Option<&Bound<'_, PyDict>>,
20+
_definitions: &mut DefinitionsBuilder<CombinedSerializer>,
21+
) -> PyResult<CombinedSerializer> {
22+
Ok(Self {}.into())
23+
}
24+
}
25+
26+
impl_py_gc_traverse!(NeverSerializer {});
27+
28+
impl TypeSerializer for NeverSerializer {
29+
fn to_python(
30+
&self,
31+
_value: &Bound<'_, PyAny>,
32+
_include: Option<&Bound<'_, PyAny>>,
33+
_exclude: Option<&Bound<'_, PyAny>>,
34+
_extra: &Extra,
35+
) -> PyResult<PyObject> {
36+
py_err!(PyTypeError; ERROR_MESSAGE)
37+
}
38+
39+
fn json_key<'a>(&self, _key: &'a Bound<'_, PyAny>, _extra: &Extra) -> PyResult<Cow<'a, str>> {
40+
py_err!(PyTypeError; ERROR_MESSAGE)
41+
}
42+
43+
fn serde_serialize<S: serde::ser::Serializer>(
44+
&self,
45+
_value: &Bound<'_, PyAny>,
46+
_serializer: S,
47+
_include: Option<&Bound<'_, PyAny>>,
48+
_exclude: Option<&Bound<'_, PyAny>>,
49+
_extra: &Extra,
50+
) -> Result<S::Ok, S::Error> {
51+
py_err!(PyTypeError; ERROR_MESSAGE).map_err(py_err_se_err)
52+
}
53+
54+
fn get_name(&self) -> &str {
55+
Self::EXPECTED_TYPE
56+
}
57+
}

src/validators/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ mod list;
4949
mod literal;
5050
mod model;
5151
mod model_fields;
52+
mod never;
5253
mod none;
5354
mod nullable;
5455
mod set;
@@ -611,6 +612,7 @@ pub fn build_validator(
611612
definitions::DefinitionRefValidator,
612613
definitions::DefinitionsValidatorBuilder,
613614
complex::ComplexValidator,
615+
never::NeverValidator,
614616
)
615617
}
616618

@@ -765,6 +767,7 @@ pub enum CombinedValidator {
765767
// input dependent
766768
JsonOrPython(json_or_python::JsonOrPython),
767769
Complex(complex::ComplexValidator),
770+
Never(never::NeverValidator),
768771
}
769772

770773
/// This trait must be implemented by all validators, it allows various validators to be accessed consistently,

src/validators/never.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use pyo3::prelude::*;
2+
use pyo3::types::PyDict;
3+
4+
use crate::errors::{ErrorTypeDefaults, ValError, ValResult};
5+
use crate::input::Input;
6+
use crate::PydanticUndefinedType;
7+
8+
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, LocItem, ValidationState, Validator};
9+
10+
#[derive(Debug)]
11+
pub struct NeverValidator {
12+
undefined: PyObject,
13+
}
14+
15+
impl BuildValidator for NeverValidator {
16+
const EXPECTED_TYPE: &'static str = "never";
17+
18+
fn build(
19+
schema: &Bound<'_, PyDict>,
20+
_config: Option<&Bound<'_, PyDict>>,
21+
_definitions: &mut DefinitionsBuilder<CombinedValidator>,
22+
) -> PyResult<CombinedValidator> {
23+
let py = schema.py();
24+
Ok(Self {
25+
undefined: PydanticUndefinedType::new(py).to_object(py),
26+
}
27+
.into())
28+
}
29+
}
30+
31+
impl_py_gc_traverse!(NeverValidator {});
32+
33+
impl Validator for NeverValidator {
34+
fn validate<'py>(
35+
&self,
36+
py: Python<'py>,
37+
input: &(impl Input<'py> + ?Sized),
38+
_state: &mut ValidationState<'_, 'py>,
39+
) -> ValResult<PyObject> {
40+
let obj = input.to_object(py);
41+
if obj.is(&self.undefined) {
42+
Ok(obj)
43+
} else {
44+
Err(ValError::new(ErrorTypeDefaults::Never, input))
45+
}
46+
}
47+
48+
fn default_value<'py>(
49+
&self,
50+
_py: Python<'py>,
51+
_outer_loc: Option<impl Into<LocItem>>,
52+
_state: &mut ValidationState<'_, 'py>,
53+
) -> ValResult<Option<PyObject>> {
54+
Ok(Some(self.undefined.clone()))
55+
}
56+
57+
fn get_name(&self) -> &str {
58+
Self::EXPECTED_TYPE
59+
}
60+
}

tests/serializers/test_model.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import platform
44
import warnings
55
from random import randint
6-
from typing import Any, ClassVar, Dict
6+
from typing import Any, ClassVar, Dict, Never
77

88
try:
99
from functools import cached_property
@@ -1152,3 +1152,25 @@ class BModel(BasicModel): ...
11521152
with pytest.warns(UserWarning, match='Expected 2 fields but got 1 for type `.*AModel` with value `.*`.+'):
11531153
value = BasicModel(root=AModel(type='a'))
11541154
s.to_python(value)
1155+
1156+
1157+
def test_never():
1158+
class MyModel:
1159+
a: int
1160+
b: Never
1161+
1162+
schema = core_schema.model_schema(
1163+
MyModel,
1164+
core_schema.model_fields_schema(
1165+
{
1166+
'a': core_schema.model_field(core_schema.int_schema()),
1167+
'b': core_schema.model_field(core_schema.never_schema()),
1168+
}
1169+
),
1170+
)
1171+
v = SchemaValidator(schema)
1172+
m = v.validate_python({'a': 1})
1173+
s = SchemaSerializer(schema)
1174+
# `b` should not break the serialiser or be serialised
1175+
assert s.to_python(m) == {'a': 1}
1176+
assert json.loads(s.to_json(m)) == {'a': 1}

tests/serializers/test_never.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import pytest
2+
3+
from pydantic_core import PydanticSerializationError, SchemaSerializer, core_schema
4+
5+
6+
def test_to_python_never():
7+
v = SchemaSerializer(core_schema.never_schema())
8+
with pytest.raises(TypeError) as exc_info:
9+
v.to_python(1)
10+
assert str(exc_info.value) == 'type `never` cannot be serialized'
11+
12+
13+
def test_to_json_never():
14+
v = SchemaSerializer(core_schema.never_schema())
15+
with pytest.raises(PydanticSerializationError) as exc_info:
16+
v.to_json('null')
17+
assert 'type `never` cannot be serialized' in str(exc_info.value)

0 commit comments

Comments
 (0)