diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43b639cfa..0a6c5da0c 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 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..edb2d99c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ classifiers = [ 'Typing :: Typed', ] dependencies = [ - 'typing-extensions>=4.13.0', + 'typing-extensions>=4.14.1', ] dynamic = ['license', 'readme', 'version'] @@ -160,3 +160,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/core_schema.py b/python/pydantic_core/core_schema.py index 7e0cd7e4c..0ede97911 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -1346,6 +1346,16 @@ class Color(Enum): ) +class MissingSentinelSchema(TypedDict, total=False): + type: Required[Literal['missing-sentinel']] + + +def missing_sentinel_schema() -> MissingSentinelSchema: + """Returns a schema for the `MISSING` sentinel.""" + + return {'type': 'missing-sentinel'} + + # must match input/parse_json.rs::JsonType::try_from JsonType = Literal['null', 'bool', 'int', 'float', 'str', 'list', 'dict'] @@ -4082,6 +4092,7 @@ def definition_reference_schema( DatetimeSchema, TimedeltaSchema, LiteralSchema, + MissingSentinelSchema, EnumSchema, IsInstanceSchema, IsSubclassSchema, @@ -4140,6 +4151,7 @@ def definition_reference_schema( 'datetime', 'timedelta', 'literal', + 'missing-sentinel', 'enum', 'is-instance', 'is-subclass', @@ -4239,6 +4251,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/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 44e6f3c7b..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)] @@ -169,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 { @@ -178,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 @@ -253,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), @@ -347,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 @@ -368,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) => { @@ -401,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 @@ -428,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/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/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/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_model.py b/tests/serializers/test_model.py index 2a7b759a4..74712685f 100644 --- a/tests/serializers/test_model.py +++ b/tests/serializers/test_model.py @@ -744,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/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/uv.lock b/uv.lock index de11e849a..b1da22c74 100644 --- a/uv.lock +++ b/uv.lock @@ -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) {