Skip to content
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
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,8 @@ jobs:
- uses: mymindstorm/setup-emsdk@v14
with:
# NOTE!: as per https://github.com/pydantic/pydantic-core/pull/149 this version needs to match the version
# in node_modules/pyodide/repodata.json, to get the version, run:
# `cat node_modules/pyodide/repodata.json | python -m json.tool | rg platform`
# in node_modules/pyodide/pyodide-lock.json, to get the version, run:
# `cat node_modules/pyodide/pyodide-lock.json | jq .info.platform`
version: '3.1.58'
actions-cache-folder: emsdk-cache

Expand Down Expand Up @@ -620,6 +620,8 @@ jobs:
apk update
apk add python3 py3-pip git curl
fi
env: |
UV_NO_PROGRESS: '1'
run: |
set -x
curl -LsSf https://astral.sh/uv/install.sh | sh
Expand Down Expand Up @@ -666,6 +668,7 @@ jobs:

permissions:
id-token: write
contents: write

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pydantic-core"
version = "2.35.2"
version = "2.37.2"
edition = "2021"
license = "MIT"
homepage = "https://github.com/pydantic/pydantic-core"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"main": "tests/emscripten_runner.js",
"dependencies": {
"prettier": "^2.7.1",
"pyodide": "^0.26.3"
"pyodide": "^0.27.7"
},
"scripts": {
"test": "node tests/emscripten_runner.js",
Expand Down
10 changes: 6 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
[build-system]
requires = ['maturin>=1,<2']
requires = ['maturin>=1.9,<2']
build-backend = 'maturin'

[project]
name = 'pydantic_core'
description = "Core functionality for Pydantic validation and serialization"
requires-python = '>=3.9'
license = 'MIT'
license-files = ['LICENSE']
authors = [
{ name = 'Samuel Colvin', email = '[email protected]' },
{ name = 'Adrian Garcia Badaracco', email = '[email protected]' },
Expand All @@ -29,16 +31,15 @@ classifiers = [
'Framework :: Pydantic',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'License :: OSI Approved :: MIT License',
'Operating System :: POSIX :: Linux',
'Operating System :: Microsoft :: Windows',
'Operating System :: MacOS',
'Typing :: Typed',
]
dependencies = [
'typing-extensions>=4.13.0',
'typing-extensions>=4.14.1',
]
dynamic = ['license', 'readme', 'version']
dynamic = ['readme', 'version']

[project.urls]
Homepage = 'https://github.com/pydantic/pydantic-core'
Expand Down Expand Up @@ -160,3 +161,4 @@ fix = ["create", "fix"]
# this ensures that `uv run` doesn't actually build the package; a `make`
# command is needed to build
package = false
required-version = '>=0.7.2'
29 changes: 29 additions & 0 deletions python/pydantic_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import sys as _sys
from typing import Any as _Any

from typing_extensions import Sentinel

from ._pydantic_core import (
ArgsKwargs,
MultiHostUrl,
Expand Down Expand Up @@ -40,6 +42,7 @@

__all__ = [
'__version__',
'UNSET',
'CoreConfig',
'CoreSchema',
'CoreSchemaType',
Expand Down Expand Up @@ -140,3 +143,29 @@ class MultiHostHost(_TypedDict):
"""The host part of this host, or `None`."""
port: int | None
"""The port part of this host, or `None`."""


MISSING = Sentinel('MISSING')
"""A singleton indicating a field value was not provided during validation.

This singleton can be used a default value, as an alternative to `None` when it has
an explicit meaning. During serialization, any field with `MISSING` as a value is excluded
from the output.

Example:
```python
from pydantic import BaseModel

from pydantic_core import MISSING


class Configuration(BaseModel):
timeout: int | None | MISSING = MISSING


# configuration defaults, stored somewhere else:
defaults = {'timeout': 200}

conf = Configuration.model_validate({...})
timeout = conf.timeout if timeout.timeout is not MISSING else defaults['timeout']
"""
3 changes: 0 additions & 3 deletions python/pydantic_core/_pydantic_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -646,9 +646,6 @@ class ValidationError(ValueError):
"""
Python constructor for a Validation Error.

The API for constructing validation errors will probably change in the future,
hence the static method rather than `__init__`.

Arguments:
title: The title of the error, as used in the heading of `str(validation_error)`
line_errors: A list of [`InitErrorDetails`][pydantic_core.InitErrorDetails] which contain information
Expand Down
34 changes: 34 additions & 0 deletions python/pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,25 @@ class Color(Enum):
)


class MissingSentinelSchema(TypedDict, total=False):
type: Required[Literal['missing-sentinel']]
metadata: dict[str, Any]
serialization: SerSchema


def missing_sentinel_schema(
metadata: dict[str, Any] | None = None,
serialization: SerSchema | None = None,
) -> MissingSentinelSchema:
"""Returns a schema for the `MISSING` sentinel."""

return _dict_not_none(
type='missing-sentinel',
metadata=metadata,
serialization=serialization,
)


# must match input/parse_json.rs::JsonType::try_from
JsonType = Literal['null', 'bool', 'int', 'float', 'str', 'list', 'dict']

Expand Down Expand Up @@ -2897,6 +2916,7 @@ class TypedDictField(TypedDict, total=False):
serialization_alias: str
serialization_exclude: bool # default: False
metadata: dict[str, Any]
serialization_exclude_if: Callable[[Any], bool] # default None


def typed_dict_field(
Expand All @@ -2907,6 +2927,7 @@ def typed_dict_field(
serialization_alias: str | None = None,
serialization_exclude: bool | None = None,
metadata: dict[str, Any] | None = None,
serialization_exclude_if: Callable[[Any], bool] | None = None,
) -> TypedDictField:
"""
Returns a schema that matches a typed dict field, e.g.:
Expand All @@ -2923,6 +2944,7 @@ def typed_dict_field(
validation_alias: The alias(es) to use to find the field in the validation data
serialization_alias: The alias to use as a key when serializing
serialization_exclude: Whether to exclude the field when serializing
serialization_exclude_if: A callable that determines whether to exclude the field when serializing based on its value.
metadata: Any other information you want to include with the schema, not used by pydantic-core
"""
return _dict_not_none(
Expand All @@ -2932,6 +2954,7 @@ def typed_dict_field(
validation_alias=validation_alias,
serialization_alias=serialization_alias,
serialization_exclude=serialization_exclude,
serialization_exclude_if=serialization_exclude_if,
metadata=metadata,
)

Expand Down Expand Up @@ -3023,6 +3046,7 @@ class ModelField(TypedDict, total=False):
validation_alias: Union[str, list[Union[str, int]], list[list[Union[str, int]]]]
serialization_alias: str
serialization_exclude: bool # default: False
serialization_exclude_if: Callable[[Any], bool] # default: None
frozen: bool
metadata: dict[str, Any]

Expand All @@ -3033,6 +3057,7 @@ def model_field(
validation_alias: str | list[str | int] | list[list[str | int]] | None = None,
serialization_alias: str | None = None,
serialization_exclude: bool | None = None,
serialization_exclude_if: Callable[[Any], bool] | None = None,
frozen: bool | None = None,
metadata: dict[str, Any] | None = None,
) -> ModelField:
Expand All @@ -3050,6 +3075,7 @@ def model_field(
validation_alias: The alias(es) to use to find the field in the validation data
serialization_alias: The alias to use as a key when serializing
serialization_exclude: Whether to exclude the field when serializing
serialization_exclude_if: A Callable that determines whether to exclude a field during serialization based on its value.
frozen: Whether the field is frozen
metadata: Any other information you want to include with the schema, not used by pydantic-core
"""
Expand All @@ -3059,6 +3085,7 @@ def model_field(
validation_alias=validation_alias,
serialization_alias=serialization_alias,
serialization_exclude=serialization_exclude,
serialization_exclude_if=serialization_exclude_if,
frozen=frozen,
metadata=metadata,
)
Expand Down Expand Up @@ -3251,6 +3278,7 @@ class DataclassField(TypedDict, total=False):
serialization_alias: str
serialization_exclude: bool # default: False
metadata: dict[str, Any]
serialization_exclude_if: Callable[[Any], bool] # default: None


def dataclass_field(
Expand All @@ -3264,6 +3292,7 @@ def dataclass_field(
serialization_alias: str | None = None,
serialization_exclude: bool | None = None,
metadata: dict[str, Any] | None = None,
serialization_exclude_if: Callable[[Any], bool] | None = None,
frozen: bool | None = None,
) -> DataclassField:
"""
Expand All @@ -3289,6 +3318,7 @@ def dataclass_field(
validation_alias: The alias(es) to use to find the field in the validation data
serialization_alias: The alias to use as a key when serializing
serialization_exclude: Whether to exclude the field when serializing
serialization_exclude_if: A callable that determines whether to exclude the field when serializing based on its value.
metadata: Any other information you want to include with the schema, not used by pydantic-core
frozen: Whether the field is frozen
"""
Expand All @@ -3302,6 +3332,7 @@ def dataclass_field(
validation_alias=validation_alias,
serialization_alias=serialization_alias,
serialization_exclude=serialization_exclude,
serialization_exclude_if=serialization_exclude_if,
metadata=metadata,
frozen=frozen,
)
Expand Down Expand Up @@ -4070,6 +4101,7 @@ def definition_reference_schema(
DatetimeSchema,
TimedeltaSchema,
LiteralSchema,
MissingSentinelSchema,
EnumSchema,
IsInstanceSchema,
IsSubclassSchema,
Expand Down Expand Up @@ -4128,6 +4160,7 @@ def definition_reference_schema(
'datetime',
'timedelta',
'literal',
'missing-sentinel',
'enum',
'is-instance',
'is-subclass',
Expand Down Expand Up @@ -4227,6 +4260,7 @@ def definition_reference_schema(
'value_error',
'assertion_error',
'literal_error',
'missing_sentinel_error',
'date_type',
'date_parsing',
'date_from_datetime_parsing',
Expand Down
16 changes: 16 additions & 0 deletions src/common/missing_sentinel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use pyo3::intern;
use pyo3::prelude::*;
use pyo3::sync::GILOnceCell;

static MISSING_SENTINEL_OBJECT: GILOnceCell<Py<PyAny>> = GILOnceCell::new();

pub fn get_missing_sentinel_object(py: Python) -> &Bound<'_, PyAny> {
MISSING_SENTINEL_OBJECT
.get_or_init(py, || {
py.import(intern!(py, "pydantic_core"))
.and_then(|core_module| core_module.getattr(intern!(py, "MISSING")))
.unwrap()
.into()
})
.bind(py)
}
1 change: 1 addition & 0 deletions src/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub(crate) mod missing_sentinel;
pub(crate) mod prebuilt;
pub(crate) mod union;
3 changes: 3 additions & 0 deletions src/errors/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,8 @@ error_types! {
expected: {ctx_type: String, ctx_fn: field_from_context},
},
// ---------------------
// missing sentinel
MissingSentinelError {},
// date errors
DateType {},
DateParsing {
Expand Down Expand Up @@ -531,6 +533,7 @@ impl ErrorType {
Self::AssertionError {..} => "Assertion failed, {error}",
Self::CustomError {..} => "", // custom errors are handled separately
Self::LiteralError {..} => "Input should be {expected}",
Self::MissingSentinelError { .. } => "Input should be the 'MISSING' sentinel",
Self::DateType {..} => "Input should be a valid date",
Self::DateParsing {..} => "Input should be a valid date in the format YYYY-MM-DD, {error}",
Self::DateFromDatetimeParsing {..} => "Input should be a valid date or datetime, {error}",
Expand Down
20 changes: 19 additions & 1 deletion src/input/input_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::str::from_utf8;
use pyo3::intern;
use pyo3::prelude::*;

use pyo3::sync::GILOnceCell;
use pyo3::types::PyType;
use pyo3::types::{
PyBool, PyByteArray, PyBytes, PyComplex, PyDate, PyDateTime, PyDict, PyFloat, PyFrozenSet, PyInt, PyIterator,
Expand Down Expand Up @@ -30,7 +31,8 @@ use super::input_abstract::ValMatch;
use super::return_enums::EitherComplex;
use super::return_enums::{iterate_attributes, iterate_mapping_items, ValidationMatch};
use super::shared::{
decimal_as_int, float_as_int, get_enum_meta_object, int_as_bool, str_as_bool, str_as_float, str_as_int,
decimal_as_int, float_as_int, fraction_as_int, get_enum_meta_object, int_as_bool, str_as_bool, str_as_float,
str_as_int,
};
use super::Arguments;
use super::ConsumeIterator;
Expand All @@ -45,6 +47,20 @@ use super::{
Input,
};

static FRACTION_TYPE: GILOnceCell<Py<PyType>> = GILOnceCell::new();

pub fn get_fraction_type(py: Python) -> &Bound<'_, PyType> {
FRACTION_TYPE
.get_or_init(py, || {
py.import("fractions")
.and_then(|fractions_module| fractions_module.getattr("Fraction"))
.unwrap()
.extract()
.unwrap()
})
.bind(py)
}

pub(crate) fn downcast_python_input<'py, T: PyTypeCheck>(input: &(impl Input<'py> + ?Sized)) -> Option<&Bound<'py, T>> {
input.as_python().and_then(|any| any.downcast::<T>().ok())
}
Expand Down Expand Up @@ -269,6 +285,8 @@ impl<'py> Input<'py> for Bound<'py, PyAny> {
float_as_int(self, self.extract::<f64>()?)
} else if let Ok(decimal) = self.validate_decimal(true, self.py()) {
decimal_as_int(self, &decimal.into_inner())
} else if self.is_instance(get_fraction_type(self.py()))? {
fraction_as_int(self)
} else if let Ok(float) = self.extract::<f64>() {
float_as_int(self, float)
} else if let Some(enum_val) = maybe_as_enum(self) {
Expand Down
Loading