diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d33e7dfe..7f737b852 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 @@ -666,6 +668,7 @@ jobs: permissions: id-token: write + contents: write steps: - uses: actions/checkout@v4 diff --git a/Cargo.lock b/Cargo.lock index 22bbe71f9..85f09027d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,7 +431,7 @@ dependencies = [ [[package]] name = "pydantic-core" -version = "2.35.2" +version = "2.37.2" dependencies = [ "ahash", "base64", diff --git a/Cargo.toml b/Cargo.toml index cf08c14dd..2ad3e75e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/package.json b/package.json index ecfc0372e..33a0a8d14 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pyproject.toml b/pyproject.toml index fb00a36a5..202cf41b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = 's@muelcolvin.com' }, { name = 'Adrian Garcia Badaracco', email = '1755071+adriangb@users.noreply.github.com' }, @@ -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' @@ -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' diff --git a/python/pydantic_core/__init__.py b/python/pydantic_core/__init__.py index a146499d0..2dbd756f1 100644 --- a/python/pydantic_core/__init__.py +++ b/python/pydantic_core/__init__.py @@ -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, @@ -40,6 +42,7 @@ __all__ = [ '__version__', + 'UNSET', 'CoreConfig', 'CoreSchema', 'CoreSchemaType', @@ -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'] +""" diff --git a/python/pydantic_core/_pydantic_core.pyi b/python/pydantic_core/_pydantic_core.pyi index dceddbf99..07b995761 100644 --- a/python/pydantic_core/_pydantic_core.pyi +++ b/python/pydantic_core/_pydantic_core.pyi @@ -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 diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 26b73c087..d03dded87 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -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'] @@ -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( @@ -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.: @@ -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( @@ -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, ) @@ -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] @@ -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: @@ -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 """ @@ -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, ) @@ -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( @@ -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: """ @@ -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 """ @@ -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, ) @@ -4070,6 +4101,7 @@ def definition_reference_schema( DatetimeSchema, TimedeltaSchema, LiteralSchema, + MissingSentinelSchema, EnumSchema, IsInstanceSchema, IsSubclassSchema, @@ -4128,6 +4160,7 @@ def definition_reference_schema( 'datetime', 'timedelta', 'literal', + 'missing-sentinel', 'enum', 'is-instance', 'is-subclass', @@ -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', diff --git a/src/common/missing_sentinel.rs b/src/common/missing_sentinel.rs new file mode 100644 index 000000000..b23da9bd0 --- /dev/null +++ b/src/common/missing_sentinel.rs @@ -0,0 +1,16 @@ +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::sync::GILOnceCell; + +static MISSING_SENTINEL_OBJECT: GILOnceCell> = 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) +} diff --git a/src/common/mod.rs b/src/common/mod.rs index 47c0a0349..776ea7753 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,2 +1,3 @@ +pub(crate) mod missing_sentinel; pub(crate) mod prebuilt; pub(crate) mod union; diff --git a/src/errors/types.rs b/src/errors/types.rs index 0c75e1e24..d2420629f 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -316,6 +316,8 @@ error_types! { expected: {ctx_type: String, ctx_fn: field_from_context}, }, // --------------------- + // missing sentinel + MissingSentinelError {}, // date errors DateType {}, DateParsing { @@ -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}", diff --git a/src/input/input_python.rs b/src/input/input_python.rs index e82cbaed7..a51307b22 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -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, @@ -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; @@ -45,6 +47,20 @@ use super::{ Input, }; +static FRACTION_TYPE: GILOnceCell> = 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::().ok()) } @@ -269,6 +285,8 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { float_as_int(self, self.extract::()?) } 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::() { float_as_int(self, float) } else if let Some(enum_val) = maybe_as_enum(self) { diff --git a/src/input/shared.rs b/src/input/shared.rs index 1a90b4142..8ca9c0013 100644 --- a/src/input/shared.rs +++ b/src/input/shared.rs @@ -227,3 +227,23 @@ pub fn decimal_as_int<'py>( } Ok(EitherInt::Py(numerator)) } + +pub fn fraction_as_int<'py>(input: &Bound<'py, PyAny>) -> ValResult> { + #[cfg(Py_3_12)] + let is_integer = input.call_method0("is_integer")?.extract::()?; + #[cfg(not(Py_3_12))] + let is_integer = input.getattr("denominator")?.extract::().map_or(false, |d| d == 1); + + if is_integer { + #[cfg(Py_3_11)] + let as_int = input.call_method0("__int__"); + #[cfg(not(Py_3_11))] + let as_int = input.call_method0("__trunc__"); + match as_int { + Ok(i) => Ok(EitherInt::Py(i.as_any().to_owned())), + Err(_) => Err(ValError::new(ErrorTypeDefaults::IntType, input)), + } + } else { + Err(ValError::new(ErrorTypeDefaults::IntFromFloat, input)) + } +} diff --git a/src/serializers/computed_fields.rs b/src/serializers/computed_fields.rs index 7a574093c..9870a6682 100644 --- a/src/serializers/computed_fields.rs +++ b/src/serializers/computed_fields.rs @@ -4,6 +4,7 @@ use pyo3::{intern, PyTraverseError, PyVisit}; use serde::ser::SerializeMap; use crate::build_tools::py_schema_error_type; +use crate::common::missing_sentinel::get_missing_sentinel_object; use crate::definitions::DefinitionsBuilder; use crate::py_gc::PyGcTraverse; use crate::serializers::filter::SchemaFilter; @@ -148,6 +149,10 @@ impl ComputedFields { if extra.exclude_none && value.is_none() { continue; } + let missing_sentinel = get_missing_sentinel_object(model.py()); + if value.is(missing_sentinel) { + continue; + } let field_extra = Extra { field_name: Some(&computed_field.property_name), diff --git a/src/serializers/fields.rs b/src/serializers/fields.rs index a5c5bc6b3..907078e00 100644 --- a/src/serializers/fields.rs +++ b/src/serializers/fields.rs @@ -7,6 +7,7 @@ use ahash::AHashMap; use serde::ser::SerializeMap; use smallvec::SmallVec; +use crate::common::missing_sentinel::get_missing_sentinel_object; use crate::serializers::extra::SerCheck; use crate::PydanticSerializationUnexpectedValue; @@ -15,8 +16,7 @@ use super::errors::py_err_se_err; use super::extra::Extra; use super::filter::SchemaFilter; use super::infer::{infer_json_key, infer_serialize, infer_to_python, SerializeInfer}; -use super::shared::PydanticSerializer; -use super::shared::{CombinedSerializer, TypeSerializer}; +use super::shared::{CombinedSerializer, PydanticSerializer, TypeSerializer}; /// representation of a field for serialization #[derive(Debug)] @@ -28,6 +28,7 @@ pub(super) struct SerField { pub serializer: Option, pub required: bool, pub serialize_by_alias: Option, + pub serialization_exclude_if: Option>, } impl_py_gc_traverse!(SerField { serializer }); @@ -40,6 +41,7 @@ impl SerField { serializer: Option, required: bool, serialize_by_alias: Option, + serialization_exclude_if: Option>, ) -> Self { let alias_py = alias.as_ref().map(|alias| PyString::new(py, alias.as_str()).into()); Self { @@ -49,6 +51,7 @@ impl SerField { serializer, required, serialize_by_alias, + serialization_exclude_if, } } @@ -71,6 +74,18 @@ impl SerField { } } +fn serialization_exclude_if(exclude_if_callable: Option<&Py>, value: &Bound<'_, PyAny>) -> PyResult { + if let Some(exclude_if_callable) = exclude_if_callable { + let py = value.py(); + let result = exclude_if_callable.call1(py, (value,))?; + let exclude = result.extract::(py)?; + if exclude { + return Ok(true); + } + } + Ok(false) +} + fn exclude_default(value: &Bound<'_, PyAny>, extra: &Extra, serializer: &CombinedSerializer) -> PyResult { if extra.exclude_defaults { if let Some(default) = serializer.get_default(value.py())? { @@ -154,6 +169,7 @@ impl GeneralFieldsSerializer { ) -> PyResult> { let output_dict = PyDict::new(py); let mut used_req_fields: usize = 0; + let missing_sentinel = get_missing_sentinel_object(py); // NOTE! we maintain the order of the input dict assuming that's right for result in main_iter { @@ -163,6 +179,10 @@ impl GeneralFieldsSerializer { if extra.exclude_none && value.is_none() { continue; } + if value.is(missing_sentinel) { + continue; + } + let field_extra = Extra { field_name: Some(key_str), ..extra @@ -170,16 +190,16 @@ impl GeneralFieldsSerializer { if let Some((next_include, next_exclude)) = self.filter.key_filter(&key, include, exclude)? { if let Some(field) = op_field { if let Some(ref serializer) = field.serializer { - if !exclude_default(&value, &field_extra, serializer)? { - let value = serializer.to_python( - &value, - next_include.as_ref(), - next_exclude.as_ref(), - &field_extra, - )?; - let output_key = field.get_key_py(output_dict.py(), &field_extra); - output_dict.set_item(output_key, value)?; + if exclude_default(&value, &field_extra, serializer)? { + continue; } + if serialization_exclude_if(field.serialization_exclude_if.as_ref(), &value)? { + continue; + } + let value = + serializer.to_python(&value, next_include.as_ref(), next_exclude.as_ref(), &field_extra)?; + let output_key = field.get_key_py(output_dict.py(), &field_extra); + output_dict.set_item(output_key, value)?; } if field.required { @@ -238,9 +258,13 @@ impl GeneralFieldsSerializer { for result in main_iter { let (key, value) = result.map_err(py_err_se_err)?; + let missing_sentinel = get_missing_sentinel_object(value.py()); if extra.exclude_none && value.is_none() { continue; } + if value.is(missing_sentinel) { + continue; + } let key_str = key_str(&key).map_err(py_err_se_err)?; let field_extra = Extra { field_name: Some(key_str), @@ -251,17 +275,23 @@ impl GeneralFieldsSerializer { if let Some((next_include, next_exclude)) = filter { if let Some(field) = self.fields.get(key_str) { if let Some(ref serializer) = field.serializer { - if !exclude_default(&value, &field_extra, serializer).map_err(py_err_se_err)? { - let s = PydanticSerializer::new( - &value, - serializer, - next_include.as_ref(), - next_exclude.as_ref(), - &field_extra, - ); - let output_key = field.get_key_json(key_str, &field_extra); - map.serialize_entry(&output_key, &s)?; + if exclude_default(&value, &field_extra, serializer).map_err(py_err_se_err)? { + continue; } + if serialization_exclude_if(field.serialization_exclude_if.as_ref(), &value) + .map_err(py_err_se_err)? + { + continue; + } + let s = PydanticSerializer::new( + &value, + serializer, + next_include.as_ref(), + next_exclude.as_ref(), + &field_extra, + ); + let output_key = field.get_key_json(key_str, &field_extra); + map.serialize_entry(&output_key, &s)?; } } else if self.mode == FieldsMode::TypedDictAllow { let output_key = infer_json_key(&key, &field_extra).map_err(py_err_se_err)?; @@ -326,6 +356,7 @@ impl TypeSerializer for GeneralFieldsSerializer { extra: &Extra, ) -> PyResult { let py = value.py(); + let missing_sentinel = get_missing_sentinel_object(py); // If there is already a model registered (from a dataclass, BaseModel) // then do not touch it // If there is no model, we (a TypedDict) are the model @@ -347,6 +378,9 @@ impl TypeSerializer for GeneralFieldsSerializer { if extra.exclude_none && value.is_none() { continue; } + if value.is(missing_sentinel) { + continue; + } if let Some((next_include, next_exclude)) = self.filter.key_filter(&key, include, exclude)? { let value = match &self.extra_serializer { Some(serializer) => { @@ -380,7 +414,7 @@ impl TypeSerializer for GeneralFieldsSerializer { extra.warnings.on_fallback_ser::(self.get_name(), value, extra)?; return infer_serialize(value, serializer, include, exclude, extra); }; - + let missing_sentinel = get_missing_sentinel_object(value.py()); // If there is already a model registered (from a dataclass, BaseModel) // then do not touch it // If there is no model, we (a TypedDict) are the model @@ -407,6 +441,9 @@ impl TypeSerializer for GeneralFieldsSerializer { if extra.exclude_none && value.is_none() { continue; } + if value.is(missing_sentinel) { + continue; + } let filter = self.filter.key_filter(&key, include, exclude).map_err(py_err_se_err)?; if let Some((next_include, next_exclude)) = filter { let output_key = infer_json_key(&key, extra).map_err(py_err_se_err)?; diff --git a/src/serializers/shared.rs b/src/serializers/shared.rs index 81a673dde..10f6f52fe 100644 --- a/src/serializers/shared.rs +++ b/src/serializers/shared.rs @@ -142,6 +142,7 @@ combined_serializer! { Union: super::type_serializers::union::UnionSerializer; TaggedUnion: super::type_serializers::union::TaggedUnionSerializer; Literal: super::type_serializers::literal::LiteralSerializer; + MissingSentinel: super::type_serializers::missing_sentinel::MissingSentinelSerializer; Enum: super::type_serializers::enum_::EnumSerializer; Recursive: super::type_serializers::definitions::DefinitionRefSerializer; Tuple: super::type_serializers::tuple::TupleSerializer; @@ -343,6 +344,7 @@ impl PyGcTraverse for CombinedSerializer { CombinedSerializer::Union(inner) => inner.py_gc_traverse(visit), CombinedSerializer::TaggedUnion(inner) => inner.py_gc_traverse(visit), CombinedSerializer::Literal(inner) => inner.py_gc_traverse(visit), + CombinedSerializer::MissingSentinel(inner) => inner.py_gc_traverse(visit), CombinedSerializer::Enum(inner) => inner.py_gc_traverse(visit), CombinedSerializer::Recursive(inner) => inner.py_gc_traverse(visit), CombinedSerializer::Tuple(inner) => inner.py_gc_traverse(visit), diff --git a/src/serializers/type_serializers/dataclass.rs b/src/serializers/type_serializers/dataclass.rs index 996a80d72..1c2a1f6e1 100644 --- a/src/serializers/type_serializers/dataclass.rs +++ b/src/serializers/type_serializers/dataclass.rs @@ -43,24 +43,35 @@ impl BuildSerializer for DataclassArgsBuilder { let name: String = field_info.get_as_req(intern!(py, "name"))?; let key_py: Py = PyString::new(py, &name).into(); - if !field_info.get_as(intern!(py, "init_only"))?.unwrap_or(false) { if field_info.get_as(intern!(py, "serialization_exclude"))? == Some(true) { - fields.insert(name, SerField::new(py, key_py, None, None, true, serialize_by_alias)); + fields.insert( + name, + SerField::new(py, key_py, None, None, true, serialize_by_alias, None), + ); } else { let schema = field_info.get_as_req(intern!(py, "schema"))?; let serializer = CombinedSerializer::build(&schema, config, definitions) .map_err(|e| py_schema_error_type!("Field `{}`:\n {}", index, e))?; let alias = field_info.get_as(intern!(py, "serialization_alias"))?; + let serialization_exclude_if: Option> = + field_info.get_as(intern!(py, "serialization_exclude_if"))?; fields.insert( name, - SerField::new(py, key_py, alias, Some(serializer), true, serialize_by_alias), + SerField::new( + py, + key_py, + alias, + Some(serializer), + true, + serialize_by_alias, + serialization_exclude_if, + ), ); } } } - let computed_fields = ComputedFields::new(schema, config, definitions)?; Ok(GeneralFieldsSerializer::new(fields, fields_mode, None, computed_fields).into()) diff --git a/src/serializers/type_serializers/missing_sentinel.rs b/src/serializers/type_serializers/missing_sentinel.rs new file mode 100644 index 000000000..90f20f13e --- /dev/null +++ b/src/serializers/type_serializers/missing_sentinel.rs @@ -0,0 +1,76 @@ +// This serializer is defined so that building a schema serializer containing an +// 'missing-sentinel' core schema doesn't crash. In practice, the serializer isn't +// used for model-like classes, as the 'fields' serializer takes care of omitting +// the fields from the output (the serializer can still be used if the 'missing-sentinel' +// core schema is used standalone (e.g. with a Pydantic type adapter), but this isn't +// something we explicitly support. + +use std::borrow::Cow; + +use pyo3::prelude::*; +use pyo3::types::PyDict; + +use serde::ser::Error; + +use crate::common::missing_sentinel::get_missing_sentinel_object; +use crate::definitions::DefinitionsBuilder; +use crate::PydanticSerializationUnexpectedValue; + +use super::{BuildSerializer, CombinedSerializer, Extra, TypeSerializer}; + +#[derive(Debug)] +pub struct MissingSentinelSerializer {} + +impl BuildSerializer for MissingSentinelSerializer { + const EXPECTED_TYPE: &'static str = "missing-sentinel"; + + fn build( + _schema: &Bound<'_, PyDict>, + _config: Option<&Bound<'_, PyDict>>, + _definitions: &mut DefinitionsBuilder, + ) -> PyResult { + Ok(Self {}.into()) + } +} + +impl_py_gc_traverse!(MissingSentinelSerializer {}); + +impl TypeSerializer for MissingSentinelSerializer { + fn to_python( + &self, + value: &Bound<'_, PyAny>, + _include: Option<&Bound<'_, PyAny>>, + _exclude: Option<&Bound<'_, PyAny>>, + _extra: &Extra, + ) -> PyResult { + let missing_sentinel = get_missing_sentinel_object(value.py()); + + if value.is(missing_sentinel) { + Ok(missing_sentinel.to_owned().into()) + } else { + Err( + PydanticSerializationUnexpectedValue::new_from_msg(Some("Expected 'MISSING' sentinel".to_string())) + .to_py_err(), + ) + } + } + + fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { + self.invalid_as_json_key(key, extra, Self::EXPECTED_TYPE) + } + + fn serde_serialize( + &self, + _value: &Bound<'_, PyAny>, + _serializer: S, + _include: Option<&Bound<'_, PyAny>>, + _exclude: Option<&Bound<'_, PyAny>>, + _extra: &Extra, + ) -> Result { + Err(Error::custom("'MISSING' can't be serialized to JSON".to_string())) + } + + fn get_name(&self) -> &str { + Self::EXPECTED_TYPE + } +} diff --git a/src/serializers/type_serializers/mod.rs b/src/serializers/type_serializers/mod.rs index dabd006a3..5fe990382 100644 --- a/src/serializers/type_serializers/mod.rs +++ b/src/serializers/type_serializers/mod.rs @@ -15,6 +15,7 @@ pub mod json; pub mod json_or_python; pub mod list; pub mod literal; +pub mod missing_sentinel; pub mod model; pub mod nullable; pub mod other; diff --git a/src/serializers/type_serializers/model.rs b/src/serializers/type_serializers/model.rs index 21f7ce024..253d2a093 100644 --- a/src/serializers/type_serializers/model.rs +++ b/src/serializers/type_serializers/model.rs @@ -56,17 +56,29 @@ impl BuildSerializer for ModelFieldsBuilder { let key_py: Py = key_py.into(); if field_info.get_as(intern!(py, "serialization_exclude"))? == Some(true) { - fields.insert(key, SerField::new(py, key_py, None, None, true, serialize_by_alias)); + fields.insert( + key, + SerField::new(py, key_py, None, None, true, serialize_by_alias, None), + ); } else { let alias: Option = field_info.get_as(intern!(py, "serialization_alias"))?; - + let serialization_exclude_if: Option> = + field_info.get_as(intern!(py, "serialization_exclude_if"))?; let schema = field_info.get_as_req(intern!(py, "schema"))?; 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, serialize_by_alias), + SerField::new( + py, + key_py, + alias, + Some(serializer), + true, + serialize_by_alias, + serialization_exclude_if, + ), ); } } diff --git a/src/serializers/type_serializers/typed_dict.rs b/src/serializers/type_serializers/typed_dict.rs index d93a88550..406b95779 100644 --- a/src/serializers/type_serializers/typed_dict.rs +++ b/src/serializers/type_serializers/typed_dict.rs @@ -54,16 +54,28 @@ impl BuildSerializer for TypedDictBuilder { let required = field_info.get_as(intern!(py, "required"))?.unwrap_or(total); if field_info.get_as(intern!(py, "serialization_exclude"))? == Some(true) { - fields.insert(key, SerField::new(py, key_py, None, None, required, serialize_by_alias)); + fields.insert( + key, + SerField::new(py, key_py, None, None, required, serialize_by_alias, None), + ); } else { let alias: Option = field_info.get_as(intern!(py, "serialization_alias"))?; - + let serialization_exclude_if: Option> = + field_info.get_as(intern!(py, "serialization_exclude_if"))?; let schema = field_info.get_as_req(intern!(py, "schema"))?; 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), required, serialize_by_alias), + SerField::new( + py, + key_py, + alias, + Some(serializer), + required, + serialize_by_alias, + serialization_exclude_if, + ), ); } } diff --git a/src/validators/missing_sentinel.rs b/src/validators/missing_sentinel.rs new file mode 100644 index 000000000..e949772bf --- /dev/null +++ b/src/validators/missing_sentinel.rs @@ -0,0 +1,47 @@ +use core::fmt::Debug; + +use pyo3::prelude::*; +use pyo3::types::PyDict; + +use crate::common::missing_sentinel::get_missing_sentinel_object; +use crate::errors::{ErrorType, ValError, ValResult}; +use crate::input::Input; + +use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; + +#[derive(Debug, Clone)] +pub struct MissingSentinelValidator {} + +impl BuildValidator for MissingSentinelValidator { + const EXPECTED_TYPE: &'static str = "missing-sentinel"; + + fn build( + _schema: &Bound<'_, PyDict>, + _config: Option<&Bound<'_, PyDict>>, + _definitions: &mut DefinitionsBuilder, + ) -> PyResult { + Ok(CombinedValidator::MissingSentinel(Self {})) + } +} + +impl_py_gc_traverse!(MissingSentinelValidator {}); + +impl Validator for MissingSentinelValidator { + fn validate<'py>( + &self, + py: Python<'py>, + input: &(impl Input<'py> + ?Sized), + _state: &mut ValidationState<'_, 'py>, + ) -> ValResult { + let missing_sentinel = get_missing_sentinel_object(py); + + match input.as_python() { + Some(v) if v.is(missing_sentinel) => Ok(v.to_owned().into()), + _ => Err(ValError::new(ErrorType::MissingSentinelError { context: None }, input)), + } + } + + fn get_name(&self) -> &str { + Self::EXPECTED_TYPE + } +} diff --git a/src/validators/mod.rs b/src/validators/mod.rs index 2fd79c495..2f326cfea 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -47,6 +47,7 @@ mod json_or_python; mod lax_or_strict; mod list; mod literal; +mod missing_sentinel; mod model; mod model_fields; mod none; @@ -574,6 +575,8 @@ fn build_validator_inner( call::CallValidator, // literals literal::LiteralValidator, + // missing sentinel + missing_sentinel::MissingSentinelValidator, // enums enum_::BuildEnumValidator, // any @@ -741,6 +744,8 @@ pub enum CombinedValidator { FunctionCall(call::CallValidator), // literals Literal(literal::LiteralValidator), + // Missing sentinel + MissingSentinel(missing_sentinel::MissingSentinelValidator), // enums IntEnum(enum_::EnumValidator), StrEnum(enum_::EnumValidator), diff --git a/tests/benchmarks/test_serialization_micro.py b/tests/benchmarks/test_serialization_micro.py index 96170b5eb..687c48a7e 100644 --- a/tests/benchmarks/test_serialization_micro.py +++ b/tests/benchmarks/test_serialization_micro.py @@ -349,7 +349,7 @@ def r(): s.to_json(m_big) -@pytest.mark.benchmark(group='model-list-json') +@pytest.mark.benchmark(group='temporal') def test_datetime(benchmark): v = SchemaSerializer(core_schema.datetime_schema()) d = datetime(2022, 12, 2, 12, 13, 14) @@ -360,6 +360,33 @@ def r(): v.to_python(d, mode='json') +@pytest.mark.benchmark(group='temporal') +def test_datetime_seconds(benchmark): + v = SchemaSerializer( + core_schema.datetime_schema(), + config={ + 'ser_json_temporal': 'seconds', + }, + ) + d = datetime(2022, 12, 2, 12, 13, 14) + assert v.to_python(d, mode='json') == 1669983194.0 + + @benchmark + def r(): + v.to_python(d, mode='json') + + +@pytest.mark.benchmark(group='temporal') +def test_datetime_milliseconds(benchmark): + v = SchemaSerializer(core_schema.datetime_schema(), config={'ser_json_temporal': 'milliseconds'}) + d = datetime(2022, 12, 2, 12, 13, 14) + assert v.to_python(d, mode='json') == 1669983194000.0 + + @benchmark + def r(): + v.to_python(d, mode='json') + + @pytest.mark.benchmark(group='model-list-json') def test_uuid(benchmark): v = SchemaSerializer(core_schema.uuid_schema()) diff --git a/tests/emscripten_runner.js b/tests/emscripten_runner.js index c527f86f8..64c459a4c 100644 --- a/tests/emscripten_runner.js +++ b/tests/emscripten_runner.js @@ -105,7 +105,7 @@ await micropip.install([ 'pytest-mock', 'tzdata', 'file:${wheel_path}', - 'typing-extensions', + 'typing-extensions>=4.14.1', 'typing-inspection', ]) importlib.invalidate_caches() diff --git a/tests/serializers/test_dataclasses.py b/tests/serializers/test_dataclasses.py index e14ea268e..4d8ef9ff6 100644 --- a/tests/serializers/test_dataclasses.py +++ b/tests/serializers/test_dataclasses.py @@ -54,7 +54,9 @@ def test_serialization_exclude(): core_schema.dataclass_args_schema( 'Foo', [ - core_schema.dataclass_field(name='a', schema=core_schema.str_schema()), + core_schema.dataclass_field( + name='a', schema=core_schema.str_schema(), serialization_exclude_if=lambda x: x == 'bye' + ), core_schema.dataclass_field(name='b', schema=core_schema.bytes_schema(), serialization_exclude=True), ], ), @@ -63,12 +65,18 @@ def test_serialization_exclude(): s = SchemaSerializer(schema) assert s.to_python(Foo(a='hello', b=b'more')) == {'a': 'hello'} assert s.to_python(Foo(a='hello', b=b'more'), mode='json') == {'a': 'hello'} + # a = 'bye' excludes it + assert s.to_python(Foo(a='bye', b=b'more'), mode='json') == {} j = s.to_json(Foo(a='hello', b=b'more')) - if on_pypy: assert json.loads(j) == {'a': 'hello'} else: assert j == b'{"a":"hello"}' + j = s.to_json(Foo(a='bye', b=b'more')) + if on_pypy: + assert json.loads(j) == {} + else: + assert j == b'{}' def test_serialization_alias(): diff --git a/tests/serializers/test_functions.py b/tests/serializers/test_functions.py index 926749721..c31c4e6b6 100644 --- a/tests/serializers/test_functions.py +++ b/tests/serializers/test_functions.py @@ -517,7 +517,9 @@ def __init__(self, **kwargs): MyModel, core_schema.typed_dict_schema( { - 'a': core_schema.typed_dict_field(core_schema.any_schema()), + 'a': core_schema.typed_dict_field( + core_schema.any_schema(), serialization_exclude_if=lambda x: isinstance(x, int) and x >= 2 + ), 'b': core_schema.typed_dict_field(core_schema.any_schema()), 'c': core_schema.typed_dict_field(core_schema.any_schema(), serialization_exclude=True), } @@ -541,6 +543,14 @@ def __init__(self, **kwargs): assert s.to_json(m, exclude={'b'}) == b'{"a":1}' assert calls == 6 + m = MyModel(a=2, b=b'foobar', c='excluded') + assert s.to_python(m) == {'b': b'foobar'} + assert calls == 7 + assert s.to_python(m, mode='json') == {'b': 'foobar'} + assert calls == 8 + assert s.to_json(m) == b'{"b":"foobar"}' + assert calls == 9 + def test_function_plain_model(): calls = 0 @@ -559,7 +569,9 @@ def __init__(self, **kwargs): MyModel, core_schema.typed_dict_schema( { - 'a': core_schema.typed_dict_field(core_schema.any_schema()), + 'a': core_schema.typed_dict_field( + core_schema.any_schema(), serialization_exclude_if=lambda x: x == 100 + ), 'b': core_schema.typed_dict_field(core_schema.any_schema()), 'c': core_schema.typed_dict_field(core_schema.any_schema(), serialization_exclude=True), } diff --git a/tests/serializers/test_model.py b/tests/serializers/test_model.py index 65871e050..74712685f 100644 --- a/tests/serializers/test_model.py +++ b/tests/serializers/test_model.py @@ -203,6 +203,36 @@ def test_include_exclude_args(params): assert json.loads(s.to_json(value, include=include, exclude=exclude)) == expected +def test_exclude_if(): + s = SchemaSerializer( + core_schema.model_schema( + BasicModel, + core_schema.model_fields_schema( + { + 'a': core_schema.model_field(core_schema.int_schema(), serialization_exclude_if=lambda x: x > 1), + 'b': core_schema.model_field( + core_schema.str_schema(), serialization_exclude_if=lambda x: 'foo' in x + ), + 'c': core_schema.model_field( + core_schema.str_schema(), + serialization_exclude=True, + serialization_exclude_if=lambda x: 'foo' in x, + ), + } + ), + ) + ) + assert s.to_python(BasicModel(a=0, b='bar', c='bar')) == {'a': 0, 'b': 'bar'} + assert s.to_python(BasicModel(a=2, b='bar', c='bar')) == {'b': 'bar'} + assert s.to_python(BasicModel(a=0, b='foo', c='bar')) == {'a': 0} + assert s.to_python(BasicModel(a=2, b='foo', c='bar')) == {} + + assert s.to_json(BasicModel(a=0, b='bar', c='bar')) == b'{"a":0,"b":"bar"}' + assert s.to_json(BasicModel(a=2, b='bar', c='bar')) == b'{"b":"bar"}' + assert s.to_json(BasicModel(a=0, b='foo', c='bar')) == b'{"a":0}' + assert s.to_json(BasicModel(a=2, b='foo', c='bar')) == b'{}' + + def test_alias(): s = SchemaSerializer( core_schema.model_schema( @@ -714,6 +744,34 @@ def volume(self) -> int: assert s.to_json(Model(3, 4), by_alias=True) == b'{"width":3,"height":4,"Area":12,"volume":48}' +def test_computed_field_without_fields() -> None: + """https://github.com/pydantic/pydantic/issues/5551""" + + # Original test introduced in https://github.com/pydantic/pydantic-core/pull/550 + + class A: + @property + def b(self) -> str: + return 'b' + + schema = core_schema.model_schema( + cls=A, + config={}, + schema=core_schema.model_fields_schema( + fields={}, + computed_fields=[ + core_schema.computed_field('b', return_schema=core_schema.any_schema()), + ], + ), + ) + + a = A() + + serializer = SchemaSerializer(schema) + + assert serializer.to_json(a) == b'{"b":"b"}' + + def test_computed_field_exclude_none(): @dataclasses.dataclass class Model: diff --git a/tests/serializers/test_typed_dict.py b/tests/serializers/test_typed_dict.py index bd2016d18..6626a3981 100644 --- a/tests/serializers/test_typed_dict.py +++ b/tests/serializers/test_typed_dict.py @@ -92,8 +92,12 @@ def test_include_exclude_schema(): { '0': core_schema.typed_dict_field(core_schema.int_schema(), serialization_exclude=True), '1': core_schema.typed_dict_field(core_schema.int_schema()), - '2': core_schema.typed_dict_field(core_schema.int_schema(), serialization_exclude=True), - '3': core_schema.typed_dict_field(core_schema.int_schema(), serialization_exclude=False), + '2': core_schema.typed_dict_field( + core_schema.int_schema(), serialization_exclude=True, serialization_exclude_if=lambda x: x < 0 + ), + '3': core_schema.typed_dict_field( + core_schema.int_schema(), serialization_exclude=False, serialization_exclude_if=lambda x: x < 0 + ), } ) ) @@ -102,6 +106,11 @@ def test_include_exclude_schema(): assert s.to_python(value, mode='json') == {'1': 1, '3': 3} assert json.loads(s.to_json(value)) == {'1': 1, '3': 3} + value = {'0': 0, '1': 1, '2': 2, '3': -3} + assert s.to_python(value) == {'1': 1} + assert s.to_python(value, mode='json') == {'1': 1} + assert json.loads(s.to_json(value)) == {'1': 1} + def test_alias(): s = SchemaSerializer( diff --git a/tests/test.rs b/tests/test.rs index 6ca066c91..90f8062d3 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -53,65 +53,6 @@ mod tests { }); } - #[test] - fn test_serialize_computed_fields() { - Python::with_gil(|py| { - let code = c_str!( - r#" -class A: - @property - def b(self) -> str: - return "b" - -schema = { - "cls": A, - "config": {}, - "schema": { - "computed_fields": [ - {"property_name": "b", "return_schema": {"type": "any"}, "type": "computed-field"} - ], - "fields": {}, - "type": "model-fields", - }, - "type": "model", -} -a = A() - "# - ); - let locals = PyDict::new(py); - py.run(code, None, Some(&locals)).unwrap(); - let a = locals.get_item("a").unwrap().unwrap(); - let schema = locals - .get_item("schema") - .unwrap() - .unwrap() - .downcast_into::() - .unwrap(); - let serialized = SchemaSerializer::py_new(schema, None) - .unwrap() - .to_json( - py, - &a, - None, - Some(false), - None, - None, - Some(true), - false, - false, - false, - false, - WarningsArg::Bool(true), - None, - false, - None, - ) - .unwrap(); - let serialized: &[u8] = serialized.extract(py).unwrap(); - assert_eq!(serialized, b"{\"b\":\"b\"}"); - }); - } - #[test] fn test_literal_schema() { Python::with_gil(|py| { diff --git a/tests/test_errors.py b/tests/test_errors.py index ad18a41c8..3e52697dd 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -340,6 +340,7 @@ def f(input_value, info): ('assertion_error', 'Assertion failed, foobar', {'error': AssertionError('foobar')}), ('literal_error', 'Input should be foo', {'expected': 'foo'}), ('literal_error', 'Input should be foo or bar', {'expected': 'foo or bar'}), + ('missing_sentinel_error', "Input should be the 'MISSING' sentinel", None), ('date_type', 'Input should be a valid date', None), ('date_parsing', 'Input should be a valid date in the format YYYY-MM-DD, foobar', {'error': 'foobar'}), ('date_from_datetime_parsing', 'Input should be a valid date or datetime, foobar', {'error': 'foobar'}), diff --git a/tests/test_schema_functions.py b/tests/test_schema_functions.py index c8a24b307..92c81b42f 100644 --- a/tests/test_schema_functions.py +++ b/tests/test_schema_functions.py @@ -82,6 +82,7 @@ def args(*args, **kwargs): {'type': 'timedelta', 'microseconds_precision': 'error'}, ), (core_schema.literal_schema, args(['a', 'b']), {'type': 'literal', 'expected': ['a', 'b']}), + (core_schema.missing_sentinel_schema, args(), {'type': 'missing-sentinel'}), ( core_schema.enum_schema, args(MyEnum, list(MyEnum.__members__.values())), diff --git a/tests/validators/test_int.py b/tests/validators/test_int.py index 1de2e0f0a..22818c17f 100644 --- a/tests/validators/test_int.py +++ b/tests/validators/test_int.py @@ -1,6 +1,7 @@ import json import re from decimal import Decimal +from fractions import Fraction from typing import Any import pytest @@ -132,13 +133,22 @@ def test_int_py_and_json(py_and_json: PyAndJson, input_value, expected): (-i64_max + 1, -i64_max + 1), (i64_max * 2, i64_max * 2), (-i64_max * 2, -i64_max * 2), + (Fraction(10_935_244_710_974_505), 10_935_244_710_974_505), # https://github.com/pydantic/pydantic/issues/12063 + pytest.param( + Fraction(1, 2), + Err( + 'Input should be a valid integer, got a number with a fractional part ' + '[type=int_from_float, input_value=Fraction(1, 2), input_type=Fraction]' + ), + id='fraction-remainder', + ), pytest.param( 1.00000000001, Err( 'Input should be a valid integer, got a number with a fractional part ' '[type=int_from_float, input_value=1.00000000001, input_type=float]' ), - id='decimal-remainder', + id='float-remainder', ), pytest.param( Decimal('1.001'), diff --git a/uv.lock b/uv.lock index de11e849a..962ea6e1f 100644 --- a/uv.lock +++ b/uv.lock @@ -311,25 +311,25 @@ wheels = [ [[package]] name = "maturin" -version = "1.8.1" +version = "1.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/08/ccb0f917722a35ab0d758be9bb5edaf645c3a3d6170061f10d396ecd273f/maturin-1.8.1.tar.gz", hash = "sha256:49cd964aabf59f8b0a6969f9860d2cdf194ac331529caae14c884f5659568857", size = 197397, upload-time = "2024-12-30T14:03:48.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/b5/8d9843ba4d2a107ea83499d0fb6758d6d9376a3e2202dbcc5ffa972e2e4a/maturin-1.9.2.tar.gz", hash = "sha256:8b534d3a8acb922fc7a01ec89c12ba950dccdc11b57457c1d4c2661ae24bf96d", size = 211836, upload-time = "2025-07-27T19:18:26.089Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/00/f34077315f34db8ad2ccf6bfe11b864ca27baab3a1320634da8e3cf89a48/maturin-1.8.1-py3-none-linux_armv6l.whl", hash = "sha256:7e590a23d9076b8a994f2e67bc63dc9a2d1c9a41b1e7b45ac354ba8275254e89", size = 7568415, upload-time = "2024-12-30T14:03:07.939Z" }, - { url = "https://files.pythonhosted.org/packages/5c/07/9219976135ce0cb32d2fa6ea5c6d0ad709013d9a17967312e149b98153a6/maturin-1.8.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d8251a95682c83ea60988c804b620c181911cd824aa107b4a49ac5333c92968", size = 14527816, upload-time = "2024-12-30T14:03:13.851Z" }, - { url = "https://files.pythonhosted.org/packages/e6/04/fa009a00903acdd1785d58322193140bfe358595347c39f315112dabdf9e/maturin-1.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9fc1a4354cac5e32c190410208039812ea88c4a36bd2b6499268ec49ef5de00", size = 7580446, upload-time = "2024-12-30T14:03:17.64Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d4/414b2aab9bbfe88182b734d3aa1b4fef7d7701e50f6be48500378b8c8721/maturin-1.8.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:621e171c6b39f95f1d0df69a118416034fbd59c0f89dcaea8c2ea62019deecba", size = 7650535, upload-time = "2024-12-30T14:03:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/f0/64/879418a8a0196013ec1fb19eada0781c04a30e8d6d9227e80f91275a4f5b/maturin-1.8.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:98f638739a5132962347871b85c91f525c9246ef4d99796ae98a2031e3df029f", size = 8006702, upload-time = "2024-12-30T14:03:24.318Z" }, - { url = "https://files.pythonhosted.org/packages/39/c2/605829324f8371294f70303aca130682df75318958efed246873d3d604ab/maturin-1.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:f9f5c47521924b6e515cbc652a042fe5f17f8747445be9d931048e5d8ddb50a4", size = 7368164, upload-time = "2024-12-30T14:03:26.582Z" }, - { url = "https://files.pythonhosted.org/packages/be/6c/30e136d397bb146b94b628c0ef7f17708281611b97849e2cf37847025ac7/maturin-1.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:0f4407c7353c31bfbb8cdeb82bc2170e474cbfb97b5ba27568f440c9d6c1fdd4", size = 7450889, upload-time = "2024-12-30T14:03:28.893Z" }, - { url = "https://files.pythonhosted.org/packages/1b/50/e1f5023512696d4e56096f702e2f68d6d9a30afe0a4eec82b0e27b8eb4e4/maturin-1.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:ec49cd70cad3c389946c6e2bc0bd50772a7fcb463040dd800720345897eec9bf", size = 9585819, upload-time = "2024-12-30T14:03:31.125Z" }, - { url = "https://files.pythonhosted.org/packages/b7/80/b24b5248d89d2e5982553900237a337ea098ca9297b8369ca2aa95549e0f/maturin-1.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08767d794de8f8a11c5c8b1b47a4ff9fb6ae2d2d97679e27030f2f509c8c2a0", size = 10920801, upload-time = "2024-12-30T14:03:35.127Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f4/8ede7a662fabf93456b44390a5ad22630e25fb5ddaecf787251071b2e143/maturin-1.8.1-py3-none-win32.whl", hash = "sha256:d678407713f3e10df33c5b3d7a343ec0551eb7f14d8ad9ba6febeb96f4e4c75c", size = 6873556, upload-time = "2024-12-30T14:03:37.913Z" }, - { url = "https://files.pythonhosted.org/packages/9c/22/757f093ed0e319e9648155b8c9d716765442bea5bc98ebc58ad4ad5b0524/maturin-1.8.1-py3-none-win_amd64.whl", hash = "sha256:a526f90fe0e5cb59ffb81f4ff547ddc42e823bbdeae4a31012c0893ca6dcaf46", size = 7823153, upload-time = "2024-12-30T14:03:40.33Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f5/051413e04f6da25069db5e76759ecdb8cd2a8ab4a94045b5a3bf548c66fa/maturin-1.8.1-py3-none-win_arm64.whl", hash = "sha256:e95f077fd2ddd2f048182880eed458c308571a534be3eb2add4d3dac55bf57f4", size = 6552131, upload-time = "2024-12-30T14:03:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/71/db/f20d4ccdfac063bde8c502d6bc6fae10c41ac8e2b1eadbce75ee706d5050/maturin-1.9.2-py3-none-linux_armv6l.whl", hash = "sha256:8f5d448b58c67ba8ef62c066ca02cf154557f2df20791d63e392e08e7721a03b", size = 8271893, upload-time = "2025-07-27T19:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/abfc2320a5e03d4c9d69bfa1c222f376f44c07374ed7c4c777b75099d265/maturin-1.9.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8dc091e8d98a9f7b8740b151f79685f43f305aa5ec444dbb3e49cacc036eb991", size = 16083703, upload-time = "2025-07-27T19:18:05.857Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/2bde4220b74cb3ccf85cd1d8bd69493810540b079690624de0e4539bfa70/maturin-1.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee768507f25da3ae8e18d2fb2e430a44552b3192a40d3ab1eae3f4a67f5792b5", size = 8423450, upload-time = "2025-07-27T19:18:07.885Z" }, + { url = "https://files.pythonhosted.org/packages/39/2c/eccb9705470ac331feb5887293348be3d6a04f37cd611ee2cd2e78339a47/maturin-1.9.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:b7dc9a38089bbd1ac93d9046a9db14a41d011efdb7d6059bb557ebd97ab207e7", size = 8267146, upload-time = "2025-07-27T19:18:09.887Z" }, + { url = "https://files.pythonhosted.org/packages/dc/40/90f43c8334e7a498817188728f758f628a2c2250ab8dcd062ce184303668/maturin-1.9.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:234b8cb12f14173452d71293c7ec86998b2d1c9f247b66023aa8b306d288817c", size = 8792134, upload-time = "2025-07-27T19:18:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/94d50a59311c9a41af88d96be7d074190d1908709876b1c9b136fb65141b/maturin-1.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0fe1480ac341b9ca733fdbf16a499fd2771e797819de416b5a47846ed0f8a17d", size = 8071546, upload-time = "2025-07-27T19:18:13.91Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8b/3a45dc1dbe4561658923b86a50bb6413bc0e9e56fd5d0f5293c7919dc4c7/maturin-1.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:029572e692af5bac4f58586f294883732b14d41abb3ba83a587a9c68c6824a2a", size = 8132544, upload-time = "2025-07-27T19:18:15.72Z" }, + { url = "https://files.pythonhosted.org/packages/93/f2/56c78ebd3d305a0912b5043b53bbdb627a2d3e2bb027e5e19305fe0ae557/maturin-1.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:58d69b388e52e93f43d25429afccaf4d1b04bb94606d46ddb21f7a97003ec174", size = 10616860, upload-time = "2025-07-27T19:18:17.478Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ab/77c5ef78b069c4a3bda175eafa922537bccebe038735f165ed1a04220748/maturin-1.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:09750de66dce2ae04ad3ac7df1266dfe77abdcf84b7b6cbe43a41cd7e89cf958", size = 8939318, upload-time = "2025-07-27T19:18:19.097Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/1897ecf4b0115b8df71011d8be3f5aa3a6dde47366d724c064fdb40fa357/maturin-1.9.2-py3-none-win32.whl", hash = "sha256:b2b12e88f5e3145cc4d749a1fb8958a83842f647c459112e9a8d75285521798f", size = 7276436, upload-time = "2025-07-27T19:18:21.318Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/437d363c4ee97bd9b88d68ff432067aaf07b10ffa0fa0bad3329d2567919/maturin-1.9.2-py3-none-win_amd64.whl", hash = "sha256:1df15e896b3a05396a91ec2e60295bd70075f984736b8c84e8b14941bcba40c8", size = 8283346, upload-time = "2025-07-27T19:18:22.652Z" }, + { url = "https://files.pythonhosted.org/packages/60/04/946552db9cfa831cee2df1bf3c857b06a8ed04f63ded9a3692dd6f88bdee/maturin-1.9.2-py3-none-win_arm64.whl", hash = "sha256:95a08e849fda883e1a961218b7834af6f128eb9fc9ace4b90b65031da6028251", size = 6942238, upload-time = "2025-07-27T19:18:24.432Z" }, ] [[package]] @@ -636,7 +636,7 @@ wasm = [ ] [package.metadata] -requires-dist = [{ name = "typing-extensions", specifier = ">=4.13.0" }] +requires-dist = [{ name = "typing-extensions", specifier = ">=4.14.1" }] [package.metadata.requires-dev] all = [ @@ -708,15 +708,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.392.post0" +version = "1.1.403" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/df/3c6f6b08fba7ccf49b114dfc4bb33e25c299883fd763f93fad47ef8bc58d/pyright-1.1.392.post0.tar.gz", hash = "sha256:3b7f88de74a28dcfa90c7d90c782b6569a48c2be5f9d4add38472bdaac247ebd", size = 3789911, upload-time = "2025-01-15T15:01:20.913Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526, upload-time = "2025-07-09T07:15:52.882Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/b1/a18de17f40e4f61ca58856b9ef9b0febf74ff88978c3f7776f910071f567/pyright-1.1.392.post0-py3-none-any.whl", hash = "sha256:252f84458a46fa2f0fd4e2f91fc74f50b9ca52c757062e93f6c250c0d8329eb2", size = 5595487, upload-time = "2025-01-15T15:01:17.775Z" }, + { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" }, ] [[package]] @@ -956,11 +956,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.0" +version = "4.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] [[package]] diff --git a/wasm-preview/run_tests.py b/wasm-preview/run_tests.py index f4a7b3f33..00b8dfc61 100644 --- a/wasm-preview/run_tests.py +++ b/wasm-preview/run_tests.py @@ -45,6 +45,8 @@ async def main(tests_zip: str, tag_name: str): 'pytest-mock', 'tzdata', 'inline-snapshot<0.21', + 'typing-extensions>=4.14.1', + 'typing-inspection', pydantic_core_wheel, ] ) diff --git a/wasm-preview/worker.js b/wasm-preview/worker.js index 13414cdb5..f6c72846d 100644 --- a/wasm-preview/worker.js +++ b/wasm-preview/worker.js @@ -89,7 +89,7 @@ async function main() { get(`./run_tests.py?v=${Date.now()}`, 'text'), // e4cf2e2 commit matches the pydantic-core wheel being used, so tests should pass get(zip_url, 'blob'), - importScripts('https://cdn.jsdelivr.net/pyodide/v0.26.3/full/pyodide.js'), + importScripts('https://cdn.jsdelivr.net/pyodide/v0.27.7/full/pyodide.js'), ]); const pyodide = await loadPyodide(); @@ -98,7 +98,6 @@ async function main() { FS.mkdir('/test_dir'); FS.chdir('/test_dir'); await pyodide.loadPackage(['micropip', 'pytest', 'numpy', 'pygments']); - if (pydantic_core_version < '2.0.0') await pyodide.loadPackage(['typing-extensions']); await pyodide.runPythonAsync(python_code, {globals: pyodide.toPy({pydantic_core_version, tests_zip})}); post(); } catch (err) {