Skip to content

Add support for typing.Never #1579

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
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
45 changes: 45 additions & 0 deletions python/pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
@@ -3858,6 +3858,47 @@ 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, ValidationError

schema = core_schema.never_schema()
v = SchemaValidator(schema)
# Validation 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 +3954,7 @@ def definition_reference_schema(
DefinitionReferenceSchema,
UuidSchema,
ComplexSchema,
NeverSchema,
]
elif False:
CoreSchema: TypeAlias = Mapping[str, Any]
@@ -3970,6 +4012,7 @@ def definition_reference_schema(
'definition-ref',
'uuid',
'complex',
'never',
]

CoreSchemaFieldType = Literal['model-field', 'dataclass-field', 'typed-dict-field', 'computed-field']
@@ -4078,6 +4121,8 @@ def definition_reference_schema(
'decimal_whole_digits',
'complex_type',
'complex_str_parsing',
'never',
'never_serializing',
]


4 changes: 4 additions & 0 deletions src/errors/types.rs
Original file line number Diff line number Diff line change
@@ -430,6 +430,8 @@ error_types! {
// Complex errors
ComplexType {},
ComplexStrParsing {},
Never {},
NeverSerializing {},
}

macro_rules! render {
@@ -576,6 +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::NeverSerializing { .. } => "Type `typing.Never` cannot be serialized"
}
}

2 changes: 2 additions & 0 deletions src/serializers/shared.rs
Original file line number Diff line number Diff line change
@@ -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),
}
}
}
1 change: 1 addition & 0 deletions src/serializers/type_serializers/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 6 additions & 1 deletion src/serializers/type_serializers/model.rs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
}

56 changes: 56 additions & 0 deletions src/serializers/type_serializers/never.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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;

#[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<CombinedSerializer>,
) -> PyResult<CombinedSerializer> {
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<PyObject> {
py_err!(PyTypeError; ErrorTypeDefaults::NeverSerializing.message_template_python())
}

fn json_key<'a>(&self, _key: &'a Bound<'_, PyAny>, _extra: &Extra) -> PyResult<Cow<'a, str>> {
py_err!(PyTypeError; ErrorTypeDefaults::NeverSerializing.message_template_python())
}

fn serde_serialize<S: serde::ser::Serializer>(
&self,
_value: &Bound<'_, PyAny>,
_serializer: S,
_include: Option<&Bound<'_, PyAny>>,
_exclude: Option<&Bound<'_, PyAny>>,
_extra: &Extra,
) -> Result<S::Ok, S::Error> {
py_err!(PyTypeError; ErrorTypeDefaults::NeverSerializing.message_template_python()).map_err(py_err_se_err)
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}
}
3 changes: 3 additions & 0 deletions src/validators/mod.rs
Original file line number Diff line number Diff line change
@@ -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,
60 changes: 60 additions & 0 deletions src/validators/never.rs
Original file line number Diff line number Diff line change
@@ -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<CombinedValidator>,
) -> PyResult<CombinedValidator> {
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<PyObject> {
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<impl Into<LocItem>>,
_state: &mut ValidationState<'_, 'py>,
) -> ValResult<Option<PyObject>> {
Ok(Some(self.undefined.clone()))
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}
}
21 changes: 21 additions & 0 deletions tests/serializers/test_model.py
Original file line number Diff line number Diff line change
@@ -1152,3 +1152,24 @@ 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:
pass

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}
17 changes: 17 additions & 0 deletions tests/serializers/test_never.py
Original file line number Diff line number Diff line change
@@ -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 `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 `typing.Never` cannot be serialized' in str(exc_info.value)
2 changes: 2 additions & 0 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -410,6 +410,8 @@ 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', 'No input is allowed for `typing.Never`', None),
('never_serializing', 'Type `typing.Never` cannot be serialized', None),
]


1 change: 1 addition & 0 deletions tests/test_schema_functions.py
Original file line number Diff line number Diff line change
@@ -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'}),
]

38 changes: 38 additions & 0 deletions tests/validators/test_never.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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': 'No input is allowed for `typing.Never`', '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': 'No input is allowed for `typing.Never`', 'input': None}
]

class MyModel:
pass

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
Loading