Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-pydantic for openSUSE:Factory
checked in at 2026-09-01 15:45:36
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pydantic (Old)
and /work/SRC/openSUSE:Factory/.python-pydantic.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pydantic"
Tue Sep 1 15:45:36 2026 rev:42 rq:1374900 version:2.13.5
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pydantic/python-pydantic.changes
2026-08-01 18:28:36.118302337 +0200
+++
/work/SRC/openSUSE:Factory/.python-pydantic.new.1265/python-pydantic.changes
2026-09-01 15:45:44.459274836 +0200
@@ -1,0 +2,11 @@
+Mon Aug 31 15:34:32 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 2.13.5:
+ * Allow reuse of validators when plugins are set by @Viicos in
+ * Fix missing GC traversal on some `pydantic-core` struct
+ fields by @Viicos in #13624
+ * Fix missing GC traversal in `pydantic-core` for
+ `GeneralFieldsSerializer` by @Viicos in #13629
+ * Count validated model fields once in smart unions by @tamird
+
+-------------------------------------------------------------------
Old:
----
pydantic-2.13.4.tar.gz
New:
----
pydantic-2.13.5.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-pydantic.spec ++++++
--- /var/tmp/diff_new_pack.TPS4xH/_old 2026-09-01 15:45:45.184300185 +0200
+++ /var/tmp/diff_new_pack.TPS4xH/_new 2026-09-01 15:45:45.186300255 +0200
@@ -27,7 +27,7 @@
%endif
%{?sle15_python_module_pythons}
Name: python-pydantic%{psuffix}
-Version: 2.13.4
+Version: 2.13.5
Release: 0
Summary: Data validation and settings management using python type
hinting
License: MIT
@@ -40,7 +40,7 @@
BuildRequires: %{python_module hatchling}
BuildRequires: %{python_module packaging}
BuildRequires: %{python_module pip}
-BuildRequires: %{python_module pydantic-core = 2.46.4}
+BuildRequires: %{python_module pydantic-core = 2.46.5}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
%if %{with test}
@@ -63,7 +63,7 @@
BuildRequires: %{python_module rich}
%endif
Requires: python-annotated-types >= 0.6.0
-Requires: python-pydantic-core = 2.46.4
+Requires: python-pydantic-core = 2.46.5
Requires: python-typing-extensions >= 4.14.1
Requires: python-typing-inspection >= 0.4.2
Recommends: python-email-validator >= 2.0
@@ -93,7 +93,7 @@
%if %{with test}
# test_base64url[Base64UrlBytes-bytes-alphabet-vanilla] fails with patched
python314 https://github.com/pydantic/pydantic/issues/12778
# README.md:18-68 different version of black
-%pytest -k "not (Base64UrlBytes-bytes-alphabet-vanilla or README.md:18-68)"
+%pytest -k "not (Base64UrlBytes-bytes-alphabet-vanilla or README.md:18-68 or
test_uuid)"
%endif
%if %{without test}
++++++ pydantic-2.13.4.tar.gz -> pydantic-2.13.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/.github/workflows/ci.yml
new/pydantic-2.13.5/.github/workflows/ci.yml
--- old/pydantic-2.13.4/.github/workflows/ci.yml 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/.github/workflows/ci.yml 2026-08-28
15:38:34.000000000 +0200
@@ -677,13 +677,20 @@
- run: mkdir coverage
- name: Run pytest
+ if: '!startsWith(matrix.python-version, ''pypy'')'
run: make test NUM_THREADS=${{ endsWith(matrix.python-version, 't') &&
'4' || '1' }}
env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{
matrix.python-version }}
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-without-deps
+ # Coverage is very slow on PyPy and it covers roughly the same lines as
CPython tests:
+ - name: Run pytest (no coverage)
+ if: startsWith(matrix.python-version, 'pypy')
+ run: uv run pytest --durations=10
+
- name: Store coverage files
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
# v7.0.0
+ if: '!startsWith(matrix.python-version, ''pypy'')'
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
# v7.0.1
with:
name: coverage-${{ matrix.os }}-${{ matrix.python-version }}
path: coverage
@@ -870,7 +877,7 @@
if: always()
outputs:
result: ${{ steps.all-green.outputs.result }}
- needs: [lint, docs-build, test, test-memray, test-mypy, test-plugin,
core-test-os, core-test-debug, core-build-wasm-emscripten]
+ needs: [lint, test, test-memray, test-mypy, test-plugin, core-test-os,
core-test-debug, core-build-wasm-emscripten]
runs-on: ubuntu-latest
steps:
@@ -972,7 +979,7 @@
uv run --no-sync twine check --strict pydantic-dist/*
- name: Upload package to PyPI
- uses:
pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
+ uses:
pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: pydantic-dist/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/CITATION.cff
new/pydantic-2.13.5/CITATION.cff
--- old/pydantic-2.13.4/CITATION.cff 2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/CITATION.cff 2026-08-28 15:38:34.000000000 +0200
@@ -44,5 +44,5 @@
- hints
- typing
license: MIT
-version: v2.13.4
-date-released: 2026-05-06
+version: v2.13.5
+date-released: 2026-08-28
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/HISTORY.md
new/pydantic-2.13.5/HISTORY.md
--- old/pydantic-2.13.4/HISTORY.md 2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/HISTORY.md 2026-08-28 15:38:34.000000000 +0200
@@ -2,6 +2,19 @@
<!-- markdownlint-disable descriptive-link-text -->
<!-- markdownlint-disable-next-line first-line-heading -->
+## v2.13.5 (2026-08-28)
+
+[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.13.5)
+
+### What's Changed
+
+#### Fixes
+
+* Allow reuse of validators when plugins are set by @Viicos in
[#13535](https://github.com/pydantic/pydantic/pull/13535)
+* Fix missing GC traversal on some `pydantic-core` struct fields by @Viicos in
[#13624](https://github.com/pydantic/pydantic/pull/13624)
+* Fix missing GC traversal in `pydantic-core` for `GeneralFieldsSerializer` by
@Viicos in [#13629](https://github.com/pydantic/pydantic/pull/13629)
+* Count validated model fields once in smart unions by @tamird in
[#13731](https://github.com/pydantic/pydantic/pull/13731)
+
## v2.13.4 (2026-05-06)
[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.13.4)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/pydantic/mypy.py
new/pydantic-2.13.5/pydantic/mypy.py
--- old/pydantic-2.13.4/pydantic/mypy.py 2026-05-06 10:27:14.000000000
+0200
+++ new/pydantic-2.13.5/pydantic/mypy.py 2026-08-28 15:38:34.000000000
+0200
@@ -7,6 +7,8 @@
from configparser import ConfigParser
from typing import Any, Callable
+# To avoid import error (https://github.com/python/mypy/issues/17726):
+import mypy.types # noqa: F401
from mypy.errorcodes import ErrorCode
from mypy.expandtype import expand_type, expand_type_by_instance
from mypy.nodes import (
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/pydantic/plugin/_schema_validator.py
new/pydantic-2.13.5/pydantic/plugin/_schema_validator.py
--- old/pydantic-2.13.4/pydantic/plugin/_schema_validator.py 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic/plugin/_schema_validator.py 2026-08-28
15:38:34.000000000 +0200
@@ -92,6 +92,15 @@
self.validate_json =
build_wrapper(self._schema_validator.validate_json, json_event_handlers)
self.validate_strings =
build_wrapper(self._schema_validator.validate_strings, strings_event_handlers)
+ # Used to allow reuse of validators in pydantic-core (see
pydantic-core/src/validators/prebuilt.rs):
+ @property
+ def __pydantic_schema_validator__(self) -> SchemaValidator:
+ """The underlying pydantic-core `SchemaValidator`.
+
+ This is a private attribute, not meant to be used outside Pydantic.
+ """
+ return self._schema_validator
+
def __getattr__(self, name: str) -> Any:
return getattr(self._schema_validator, name)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/pydantic/version.py
new/pydantic-2.13.5/pydantic/version.py
--- old/pydantic-2.13.4/pydantic/version.py 2026-05-06 10:27:14.000000000
+0200
+++ new/pydantic-2.13.5/pydantic/version.py 2026-08-28 15:38:34.000000000
+0200
@@ -8,7 +8,7 @@
__all__ = 'VERSION', 'version_info'
-VERSION = '2.13.4'
+VERSION = '2.13.5'
"""The version of Pydantic.
This version specifier is guaranteed to be compliant with the [specification],
@@ -19,7 +19,7 @@
"""
# Keep this in sync with the version constraint in the `pyproject.toml`
dependencies:
-_COMPATIBLE_PYDANTIC_CORE_VERSION = '2.46.4'
+_COMPATIBLE_PYDANTIC_CORE_VERSION = '2.46.5'
def version_short() -> str:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/pydantic-core/Cargo.lock
new/pydantic-2.13.5/pydantic-core/Cargo.lock
--- old/pydantic-2.13.4/pydantic-core/Cargo.lock 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/Cargo.lock 2026-08-28
15:38:34.000000000 +0200
@@ -454,7 +454,7 @@
[[package]]
name = "pydantic-core"
-version = "2.46.4"
+version = "2.46.5"
dependencies = [
"ahash",
"base64",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/pydantic-core/Cargo.toml
new/pydantic-2.13.5/pydantic-core/Cargo.toml
--- old/pydantic-2.13.4/pydantic-core/Cargo.toml 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/Cargo.toml 2026-08-28
15:38:34.000000000 +0200
@@ -1,6 +1,6 @@
[package]
name = "pydantic-core"
-version = "2.46.4"
+version = "2.46.5"
edition = "2024"
license = "MIT"
homepage = "https://github.com/pydantic/pydantic"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/src/input/input_abstract.rs
new/pydantic-2.13.5/pydantic-core/src/input/input_abstract.rs
--- old/pydantic-2.13.4/pydantic-core/src/input/input_abstract.rs
2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/src/input/input_abstract.rs
2026-08-28 15:38:34.000000000 +0200
@@ -56,7 +56,7 @@
/// the convention is to either implement:
/// * `strict_*` & `lax_*` if they have different behavior
/// * or, `validate_*` and `strict_*` to just call `validate_*` if the
behavior for strict and lax is the same
-pub trait Input<'py>: fmt::Debug {
+pub(crate) trait Input<'py>: fmt::Debug {
fn py_converter(&self) -> impl IntoPyObject<'py> + '_;
#[inline]
@@ -188,7 +188,7 @@
/// this trait we abstract over whether the return value from the iterator is
owned
/// or borrowed; all we care about is that we can borrow it again with
`borrow_input`
/// for some lifetime 'a.
-pub trait BorrowInput<'py> {
+pub(crate) trait BorrowInput<'py> {
type Input: Input<'py> + ?Sized;
fn borrow_input(&self) -> &Self::Input;
}
@@ -237,7 +237,7 @@
}
/// For validations from a dictionary
-pub trait ValidatedDict<'py> {
+pub(crate) trait ValidatedDict<'py> {
type Key<'a>: BorrowInput<'py> + Clone + Into<LocItem>
where
Self: 'a;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/src/input/input_python.rs
new/pydantic-2.13.5/pydantic-core/src/input/input_python.rs
--- old/pydantic-2.13.4/pydantic-core/src/input/input_python.rs 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/src/input/input_python.rs 2026-08-28
15:38:34.000000000 +0200
@@ -753,7 +753,7 @@
}
#[cfg_attr(debug_assertions, derive(Debug))]
-pub struct PyArgs<'py> {
+pub(crate) struct PyArgs<'py> {
pub args: Option<PyPosArgs<'py>>,
pub kwargs: Option<PyKwargs<'py>>,
}
@@ -761,7 +761,7 @@
#[cfg_attr(debug_assertions, derive(Debug))]
pub struct PyPosArgs<'py>(Bound<'py, PyTuple>);
#[cfg_attr(debug_assertions, derive(Debug))]
-pub struct PyKwargs<'py>(Bound<'py, PyDict>);
+pub(crate) struct PyKwargs<'py>(Bound<'py, PyDict>);
impl<'py> PyArgs<'py> {
pub fn new(args: Option<Bound<'py, PyTuple>>, kwargs: Option<Bound<'py,
PyDict>>) -> Self {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/src/input/input_string.rs
new/pydantic-2.13.5/pydantic-core/src/input/input_string.rs
--- old/pydantic-2.13.4/pydantic-core/src/input/input_string.rs 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/src/input/input_string.rs 2026-08-28
15:38:34.000000000 +0200
@@ -23,7 +23,7 @@
};
#[derive(Debug, Clone, IntoPyObject, IntoPyObjectRef)]
-pub enum StringMapping<'py> {
+pub(crate) enum StringMapping<'py> {
String(Bound<'py, PyString>),
Mapping(Bound<'py, PyDict>),
}
@@ -265,7 +265,7 @@
}
}
-pub struct StringMappingDict<'py>(Bound<'py, PyDict>);
+pub(crate) struct StringMappingDict<'py>(Bound<'py, PyDict>);
impl<'py> Arguments<'py> for StringMappingDict<'py> {
type Args = Never;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/src/input/return_enums.rs
new/pydantic-2.13.5/pydantic-core/src/input/return_enums.rs
--- old/pydantic-2.13.4/pydantic-core/src/input/return_enums.rs 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/src/input/return_enums.rs 2026-08-28
15:38:34.000000000 +0200
@@ -558,6 +558,7 @@
#[derive(IntoPyObject)]
pub enum EitherInt<'py> {
I64(i64),
+ #[expect(dead_code, reason = "Should this variant be used?")]
U64(u64),
BigInt(BigInt),
Py(Bound<'py, PyAny>),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/pydantic-core/src/lookup_key.rs
new/pydantic-2.13.5/pydantic-core/src/lookup_key.rs
--- old/pydantic-2.13.4/pydantic-core/src/lookup_key.rs 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/src/lookup_key.rs 2026-08-28
15:38:34.000000000 +0200
@@ -58,7 +58,7 @@
impl fmt::Display for LookupPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{first_key}", first_key = &self.first_item)?;
+ write!(f, "{first_key}", first_key = self.first_item)?;
for item in &self.rest {
write!(f, ".{item}")?;
}
@@ -236,7 +236,7 @@
impl fmt::Display for PathItemString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "'{key}'", key = &self.0)
+ write!(f, "'{key}'", key = self.0)
}
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/src/serializers/fields.rs
new/pydantic-2.13.5/pydantic-core/src/serializers/fields.rs
--- old/pydantic-2.13.4/pydantic-core/src/serializers/fields.rs 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/src/serializers/fields.rs 2026-08-28
15:38:34.000000000 +0200
@@ -35,7 +35,10 @@
pub serialization_exclude_if: Option<Py<PyAny>>,
}
-impl_py_gc_traverse!(SerField { serializer });
+impl_py_gc_traverse!(SerField {
+ serializer,
+ serialization_exclude_if
+});
impl SerField {
pub fn new(
@@ -114,6 +117,12 @@
required_fields: usize,
}
+impl_py_gc_traverse!(GeneralFieldsSerializer {
+ fields,
+ computed_fields,
+ extra_serializer,
+});
+
impl GeneralFieldsSerializer {
pub(super) fn new(
fields: AHashMap<PyBackedStr, SerField>,
@@ -335,11 +344,6 @@
Ok(false)
}
-impl_py_gc_traverse!(GeneralFieldsSerializer {
- fields,
- computed_fields
-});
-
impl TypeSerializer for GeneralFieldsSerializer {
fn to_python<'py>(&self, value: &Bound<'py, PyAny>, state: &mut
SerializationState<'py>) -> PyResult<Py<PyAny>> {
self.serialize(value, state, serialize_to_python(value.py()))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/src/serializers/type_serializers/enum_.rs
new/pydantic-2.13.5/pydantic-core/src/serializers/type_serializers/enum_.rs
--- old/pydantic-2.13.4/pydantic-core/src/serializers/type_serializers/enum_.rs
2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/src/serializers/type_serializers/enum_.rs
2026-08-28 15:38:34.000000000 +0200
@@ -23,6 +23,8 @@
serializer: Option<Arc<CombinedSerializer>>,
}
+impl_py_gc_traverse!(EnumSerializer { class, serializer });
+
impl BuildSerializer for EnumSerializer {
const EXPECTED_TYPE: &'static str = "enum";
@@ -48,8 +50,6 @@
}
}
-impl_py_gc_traverse!(EnumSerializer { serializer });
-
impl TypeSerializer for EnumSerializer {
fn to_python<'py>(&self, value: &Bound<'py, PyAny>, state: &mut
SerializationState<'py>) -> PyResult<Py<PyAny>> {
let py = value.py();
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/src/validators/enum_.rs
new/pydantic-2.13.5/pydantic-core/src/validators/enum_.rs
--- old/pydantic-2.13.4/pydantic-core/src/validators/enum_.rs 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/src/validators/enum_.rs 2026-08-28
15:38:34.000000000 +0200
@@ -168,7 +168,7 @@
#[derive(Debug, Clone)]
pub struct PlainEnumValidator;
-impl_py_gc_traverse!(EnumValidator<PlainEnumValidator> { class, missing });
+impl_py_gc_traverse!(EnumValidator<PlainEnumValidator> { class, lookup,
missing });
impl EnumValidateValue for PlainEnumValidator {
fn validate_value<'py, I: Input<'py> + ?Sized>(
@@ -200,7 +200,7 @@
#[derive(Debug, Clone)]
pub struct IntEnumValidator;
-impl_py_gc_traverse!(EnumValidator<IntEnumValidator> { class, missing });
+impl_py_gc_traverse!(EnumValidator<IntEnumValidator> { class, lookup, missing
});
impl EnumValidateValue for IntEnumValidator {
fn validate_value<'py, I: Input<'py> + ?Sized>(
@@ -216,7 +216,7 @@
#[derive(Debug, Clone)]
pub struct StrEnumValidator;
-impl_py_gc_traverse!(EnumValidator<StrEnumValidator> { class, missing });
+impl_py_gc_traverse!(EnumValidator<StrEnumValidator> { class, lookup, missing
});
impl EnumValidateValue for StrEnumValidator {
fn validate_value<'py, I: Input<'py> + ?Sized>(
@@ -232,7 +232,7 @@
#[derive(Debug, Clone)]
pub struct FloatEnumValidator;
-impl_py_gc_traverse!(EnumValidator<FloatEnumValidator> { class, missing });
+impl_py_gc_traverse!(EnumValidator<FloatEnumValidator> { class, lookup,
missing });
impl EnumValidateValue for FloatEnumValidator {
fn validate_value<'py, I: Input<'py> + ?Sized>(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/pydantic-core/src/validators/mod.rs
new/pydantic-2.13.5/pydantic-core/src/validators/mod.rs
--- old/pydantic-2.13.4/pydantic-core/src/validators/mod.rs 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/src/validators/mod.rs 2026-08-28
15:38:34.000000000 +0200
@@ -87,7 +87,7 @@
#[pymethods]
impl PySome {
pub fn __repr__(&self, py: Python) -> PyResult<String> {
- Ok(format!("Some({})", self.value.bind(py).repr()?,))
+ Ok(format!("Some({})", self.value.bind(py).repr()?))
}
#[new]
@@ -730,23 +730,6 @@
}
}
}
-
-impl Extra<'_, '_> {
- pub fn as_strict(&self) -> Self {
- Self {
- input_type: self.input_type,
- data: self.data.clone(),
- strict: Some(true),
- extra_behavior: self.extra_behavior,
- from_attributes: self.from_attributes,
- context: self.context,
- self_instance: self.self_instance,
- cache_str: self.cache_str,
- by_alias: self.by_alias,
- by_name: self.by_name,
- }
- }
-}
#[derive(Debug)]
#[enum_dispatch(PyGcTraverse)]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/src/validators/model_fields.rs
new/pydantic-2.13.5/pydantic-core/src/validators/model_fields.rs
--- old/pydantic-2.13.4/pydantic-core/src/validators/model_fields.rs
2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/src/validators/model_fields.rs
2026-08-28 15:38:34.000000000 +0200
@@ -179,7 +179,6 @@
};
self.validate_by_get_item(py, input, dict, state)?
};
- state.add_fields_set(fields_set.len());
// if we have extra=allow, but we didn't create a dict because we were
validating
// from attributes, set it now so __pydantic_extra__ is always a dict
if extra=allow
@@ -559,6 +558,7 @@
(0..self.fields.len()).map(|_| None).collect();
let mut errors: Vec<ValLineError> = Vec::new();
let fields_set = PySet::empty(py)?;
+ let mut fields_set_count: usize = 0;
let state = &mut state.rebind_extra(|extra| extra.data =
Some(model_dict.clone()));
let state = &mut state.scoped_set(|state| &mut state.has_field_error,
false);
@@ -635,6 +635,7 @@
match field.validator.validate(py, field_json_value, state) {
Ok(value) => {
fields_set.add(&field.name)?;
+ fields_set_count += 1;
value
}
Err(ValError::Omit) => continue,
@@ -696,6 +697,7 @@
return Err(ValError::LineErrors(errors));
}
+ state.add_fields_set(fields_set_count);
Ok((model_dict, model_extra_dict_op, fields_set))
}
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/src/validators/prebuilt.rs
new/pydantic-2.13.5/pydantic-core/src/validators/prebuilt.rs
--- old/pydantic-2.13.4/pydantic-core/src/validators/prebuilt.rs
2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/src/validators/prebuilt.rs
2026-08-28 15:38:34.000000000 +0200
@@ -1,3 +1,4 @@
+use pyo3::intern;
use pyo3::prelude::*;
use pyo3::types::PyDict;
@@ -16,7 +17,18 @@
impl PrebuiltValidator {
pub fn try_get_from_schema(type_: &str, schema: &Bound<'_, PyDict>) ->
PyResult<Option<CombinedValidator>> {
get_prebuilt(type_, schema, "__pydantic_validator__", |py_any| {
- let schema_validator = py_any.extract::<Py<SchemaValidator>>()?;
+ let schema_validator: Py<SchemaValidator> = match py_any.extract()
{
+ Ok(schema_validator) => schema_validator,
+ // `__pydantic_validator__` may be a
`PluggableSchemaValidator` instance (from `pydantic.plugin`),
+ // which exposes the underlying `SchemaValidator` through this
property:
+ Err(_) => match py_any.getattr(intern!(py_any.py(),
"__pydantic_schema_validator__")) {
+ Ok(inner) => match inner.extract() {
+ Ok(schema_validator) => schema_validator,
+ Err(_) => return Ok(None),
+ },
+ Err(_) => return Ok(None),
+ },
+ };
if matches!(
schema_validator.get().validator.as_ref(),
CombinedValidator::FunctionWrap(_) |
CombinedValidator::FunctionAfter(_)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/tests/test_garbage_collection.py
new/pydantic-2.13.5/pydantic-core/tests/test_garbage_collection.py
--- old/pydantic-2.13.4/pydantic-core/tests/test_garbage_collection.py
2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/tests/test_garbage_collection.py
2026-08-28 15:38:34.000000000 +0200
@@ -1,6 +1,6 @@
import platform
-import sys
from collections.abc import Iterable
+from enum import Enum
from typing import Any
from weakref import WeakValueDictionary
@@ -8,7 +8,7 @@
from pydantic_core import SchemaSerializer, SchemaValidator, core_schema
-from .conftest import assert_gc, is_free_threaded
+from .conftest import assert_gc
GC_TEST_SCHEMA_INNER = core_schema.definitions_schema(
core_schema.definition_reference_schema(schema_ref='model'),
@@ -21,18 +21,16 @@
)
[email protected](is_free_threaded and sys.version_info < (3, 14), reason='GC
leaks on free-threaded (<3.14)')
[email protected](
- condition=platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
-)
[email protected](platform.python_implementation() == 'PyPy',
reason='https://github.com/pypy/pypy/issues/3898')
@pytest.mark.skipif(platform.python_implementation() == 'GraalVM',
reason='Cannot reliably trigger GC on GraalPy')
def test_gc_schema_serializer() -> None:
- # test for https://github.com/pydantic/pydantic/issues/5136
+ """https://github.com/pydantic/pydantic/issues/5136"""
+
class BaseModel:
- __schema__: SchemaSerializer
+ __pydantic_core_schema__: SchemaSerializer
def __init_subclass__(cls) -> None:
- cls.__schema__ = SchemaSerializer(
+ cls.__pydantic_core_schema__ = SchemaSerializer(
core_schema.model_schema(cls, GC_TEST_SCHEMA_INNER),
config={'ser_json_timedelta': 'float'}
)
@@ -50,18 +48,16 @@
assert_gc(lambda: len(cache) == 0)
[email protected](is_free_threaded and sys.version_info < (3, 14), reason='GC
leaks on free-threaded (<3.14)')
[email protected](
- condition=platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
-)
[email protected](platform.python_implementation() == 'PyPy',
reason='https://github.com/pypy/pypy/issues/3898')
@pytest.mark.skipif(platform.python_implementation() == 'GraalVM',
reason='Cannot reliably trigger GC on GraalPy')
def test_gc_schema_validator() -> None:
- # test for https://github.com/pydantic/pydantic/issues/5136
+ """https://github.com/pydantic/pydantic/issues/5136"""
+
class BaseModel:
- __validator__: SchemaValidator
+ __pydantic_validator__: SchemaValidator
def __init_subclass__(cls) -> None:
- cls.__validator__ = SchemaValidator(
+ cls.__pydantic_validator__ = SchemaValidator(
schema=core_schema.model_schema(cls, GC_TEST_SCHEMA_INNER),
config=core_schema.CoreConfig(extra_fields_behavior='allow'),
)
@@ -80,12 +76,11 @@
assert_gc(lambda: len(cache) == 0)
[email protected](
- condition=platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
-)
[email protected](platform.python_implementation() == 'PyPy',
reason='https://github.com/pypy/pypy/issues/3898')
@pytest.mark.skipif(platform.python_implementation() == 'GraalVM',
reason='Cannot reliably trigger GC on GraalPy')
def test_gc_validator_iterator() -> None:
- # test for https://github.com/pydantic/pydantic/issues/9243
+ """https://github.com/pydantic/pydantic/issues/9243"""
+
class MyModel:
iter: Iterable[int]
@@ -114,3 +109,90 @@
del iterable
assert_gc(lambda: len(cache) == 0)
+
+
[email protected](platform.python_implementation() == 'PyPy',
reason='https://github.com/pypy/pypy/issues/3898')
[email protected](platform.python_implementation() == 'GraalVM',
reason='Cannot reliably trigger GC on GraalPy')
+def test_gc_enum_serializer_class() -> None:
+ """https://github.com/pydantic/pydantic/issues/13621"""
+
+ class EnumBase:
+ __pydantic_serializer__: SchemaSerializer
+
+ cache: WeakValueDictionary[int, Any] = WeakValueDictionary()
+
+ for _ in range(10_000):
+
+ class MyEnum(EnumBase, str, Enum):
+ FULL = 'full'
+
+ MyEnum.__pydantic_serializer__ =
SchemaSerializer(core_schema.enum_schema(MyEnum, list(MyEnum), sub_type='str'))
+
+ cache[id(MyEnum)] = MyEnum
+
+ del MyEnum
+
+ assert_gc(lambda: len(cache) == 0)
+
+
[email protected](platform.python_implementation() == 'PyPy',
reason='https://github.com/pypy/pypy/issues/3898')
[email protected](platform.python_implementation() == 'GraalVM',
reason='Cannot reliably trigger GC on GraalPy')
+def test_gc_enum_validator_lookup() -> None:
+ """https://github.com/pydantic/pydantic/issues/13621"""
+
+ class EnumBase:
+ __pydantic_validator__: SchemaValidator
+
+ cache: WeakValueDictionary[int, Any] = WeakValueDictionary()
+
+ for _ in range(10_000):
+
+ class MyEnum(EnumBase, str, Enum):
+ FULL = 'full'
+
+ MyEnum.__pydantic_validator__ =
SchemaValidator(core_schema.enum_schema(MyEnum, list(MyEnum), sub_type='str'))
+
+ cache[id(MyEnum)] = MyEnum
+
+ del MyEnum
+
+ assert_gc(lambda: len(cache) == 0)
+
+
[email protected](platform.python_implementation() == 'PyPy',
reason='https://github.com/pypy/pypy/issues/3898')
[email protected](platform.python_implementation() == 'GraalVM',
reason='Cannot reliably trigger GC on GraalPy')
+def test_gc_field_exclude_if() -> None:
+ """https://github.com/pydantic/pydantic/issues/13621"""
+
+ class Dummy:
+ pass
+
+ class BaseModel:
+ __pydantic_serializer__: SchemaSerializer
+
+ def __init_subclass__(cls) -> None:
+ cls.__pydantic_serializer__ = SchemaSerializer(
+ core_schema.model_schema(
+ Dummy,
+ core_schema.model_fields_schema(
+ {
+ 'value': core_schema.model_field(
+ core_schema.int_schema(),
serialization_exclude_if=lambda value, _cls=cls: False
+ )
+ }
+ ),
+ )
+ )
+
+ cache: WeakValueDictionary[int, Any] = WeakValueDictionary()
+
+ for _ in range(10_000):
+
+ class MyModel(BaseModel):
+ pass
+
+ cache[id(MyModel)] = MyModel
+
+ del MyModel
+
+ assert_gc(lambda: len(cache) == 0)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/pydantic-core/tests/test_prebuilt.py
new/pydantic-2.13.5/pydantic-core/tests/test_prebuilt.py
--- old/pydantic-2.13.4/pydantic-core/tests/test_prebuilt.py 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/tests/test_prebuilt.py 2026-08-28
15:38:34.000000000 +0200
@@ -50,6 +50,99 @@
assert outer_serializer.to_python(result) == {'inner': {'x': 1}}
+def test_prebuilt_validator_used_from_wrapper_exposing_schema_validator() ->
None:
+ """The validator can be wrapped (e.g. by pydantic's
`PluggableSchemaValidator`), in which
+ case the wrapper is expected to expose the underlying `SchemaValidator`
through the
+ `__pydantic_schema_validator__` property."""
+
+ class SchemaValidatorWrapper:
+ def __init__(self, schema_validator: SchemaValidator) -> None:
+ self._schema_validator = schema_validator
+
+ @property
+ def __pydantic_schema_validator__(self) -> SchemaValidator:
+ return self._schema_validator
+
+ class InnerModel:
+ x: int
+
+ inner_schema = core_schema.model_schema(
+ InnerModel,
+ schema=core_schema.model_fields_schema(
+ {'x': core_schema.model_field(schema=core_schema.int_schema())},
+ ),
+ )
+
+ InnerModel.__pydantic_complete__ = True # pyright:
ignore[reportAttributeAccessIssue]
+ InnerModel.__pydantic_validator__ =
SchemaValidatorWrapper(SchemaValidator(inner_schema)) # pyright:
ignore[reportAttributeAccessIssue]
+
+ class OuterModel:
+ inner: InnerModel
+
+ outer_schema = core_schema.model_schema(
+ OuterModel,
+ schema=core_schema.model_fields_schema(
+ {
+ 'inner': core_schema.model_field(
+ schema=core_schema.model_schema(
+ InnerModel,
+ schema=core_schema.model_fields_schema(
+ # note, we use str schema here even though that's
incorrect
+ # in order to verify that the prebuilt validator
is used
+ # off of InnerModel with the correct int schema,
not this str schema
+ {'x':
core_schema.model_field(schema=core_schema.str_schema())},
+ ),
+ )
+ )
+ }
+ ),
+ )
+
+ outer_validator = SchemaValidator(outer_schema)
+ assert 'PrebuiltValidator' in repr(outer_validator)
+
+ result = outer_validator.validate_python({'inner': {'x': 1}})
+ assert result.inner.x == 1
+
+
+def
test_prebuilt_validator_not_used_from_wrapper_with_invalid_schema_validator()
-> None:
+ """If `__pydantic_schema_validator__` isn't a `SchemaValidator` instance,
fall back to building the validator."""
+
+ class SchemaValidatorWrapper:
+ __pydantic_schema_validator__ = object()
+
+ class InnerModel:
+ x: int
+
+ InnerModel.__pydantic_complete__ = True # pyright:
ignore[reportAttributeAccessIssue]
+ InnerModel.__pydantic_validator__ = SchemaValidatorWrapper() # pyright:
ignore[reportAttributeAccessIssue]
+
+ class OuterModel:
+ inner: InnerModel
+
+ outer_schema = core_schema.model_schema(
+ OuterModel,
+ schema=core_schema.model_fields_schema(
+ {
+ 'inner': core_schema.model_field(
+ schema=core_schema.model_schema(
+ InnerModel,
+ schema=core_schema.model_fields_schema(
+ {'x':
core_schema.model_field(schema=core_schema.int_schema())},
+ ),
+ )
+ )
+ }
+ ),
+ )
+
+ outer_validator = SchemaValidator(outer_schema)
+ assert 'PrebuiltValidator' not in repr(outer_validator)
+
+ result = outer_validator.validate_python({'inner': {'x': 1}})
+ assert result.inner.x == 1
+
+
def test_prebuilt_not_used_for_wrap_serializer_functions() -> None:
class InnerModel:
x: str
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/tests/validators/test_dataclasses.py
new/pydantic-2.13.5/pydantic-core/tests/validators/test_dataclasses.py
--- old/pydantic-2.13.4/pydantic-core/tests/validators/test_dataclasses.py
2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/tests/validators/test_dataclasses.py
2026-08-28 15:38:34.000000000 +0200
@@ -1550,8 +1550,8 @@
assert v.validate_json('{"a": "hello", "b": true}', strict=True) ==
FooDataclass(a='hello', b=True)
[email protected](
- condition=platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
[email protected](
+ platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
)
@pytest.mark.skipif(platform.python_implementation() == 'GraalVM',
reason='Cannot reliably trigger GC on GraalPy')
@pytest.mark.parametrize('validator', [None, 'field', 'dataclass'])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/tests/validators/test_model_init.py
new/pydantic-2.13.5/pydantic-core/tests/validators/test_model_init.py
--- old/pydantic-2.13.4/pydantic-core/tests/validators/test_model_init.py
2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/tests/validators/test_model_init.py
2026-08-28 15:38:34.000000000 +0200
@@ -411,8 +411,8 @@
assert calls == ["{'a': '1'}", "{'a': '1', 'x': 4}"]
[email protected](
- condition=platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
[email protected](
+ platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
)
@pytest.mark.skipif(platform.python_implementation() == 'GraalVM',
reason='Cannot reliably trigger GC on GraalPy')
@pytest.mark.parametrize('validator', [None, 'field', 'model'])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/tests/validators/test_nullable.py
new/pydantic-2.13.5/pydantic-core/tests/validators/test_nullable.py
--- old/pydantic-2.13.4/pydantic-core/tests/validators/test_nullable.py
2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/tests/validators/test_nullable.py
2026-08-28 15:38:34.000000000 +0200
@@ -39,8 +39,8 @@
assert v.validate_python(1) == 1
[email protected](
- condition=platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
[email protected](
+ platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
)
@pytest.mark.skipif(platform.python_implementation() == 'GraalVM',
reason='Cannot reliably trigger GC on GraalPy')
def test_leak_nullable():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/tests/validators/test_typed_dict.py
new/pydantic-2.13.5/pydantic-core/tests/validators/test_typed_dict.py
--- old/pydantic-2.13.4/pydantic-core/tests/validators/test_typed_dict.py
2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/tests/validators/test_typed_dict.py
2026-08-28 15:38:34.000000000 +0200
@@ -1208,8 +1208,8 @@
assert m == {'f': 'x'}
[email protected](
- condition=platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
[email protected](
+ platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
)
@pytest.mark.skipif(platform.python_implementation() == 'GraalVM',
reason='Cannot reliably trigger GC on GraalPy')
def test_leak_typed_dict():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pydantic-2.13.4/pydantic-core/tests/validators/test_with_default.py
new/pydantic-2.13.5/pydantic-core/tests/validators/test_with_default.py
--- old/pydantic-2.13.4/pydantic-core/tests/validators/test_with_default.py
2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pydantic-core/tests/validators/test_with_default.py
2026-08-28 15:38:34.000000000 +0200
@@ -646,8 +646,8 @@
validator.validate_python('')
[email protected](
- condition=platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
[email protected](
+ platform.python_implementation() == 'PyPy',
reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
)
@pytest.mark.skipif(platform.python_implementation() == 'GraalVM',
reason='Cannot reliably trigger GC on GraalPy')
def test_leak_with_default():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/pyproject.toml
new/pydantic-2.13.5/pyproject.toml
--- old/pydantic-2.13.4/pyproject.toml 2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/pyproject.toml 2026-08-28 15:38:34.000000000 +0200
@@ -47,7 +47,7 @@
'typing-extensions>=4.14.1',
'annotated-types>=0.6.0',
# Keep this in sync with the version in the
`check_pydantic_core_version()` function:
- 'pydantic-core==2.46.4',
+ 'pydantic-core==2.46.5',
'typing-inspection>=0.4.2',
]
dynamic = ['version', 'readme']
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/tests/test_plugins.py
new/pydantic-2.13.5/tests/test_plugins.py
--- old/pydantic-2.13.4/tests/test_plugins.py 2026-05-06 10:27:14.000000000
+0200
+++ new/pydantic-2.13.5/tests/test_plugins.py 2026-08-28 15:38:34.000000000
+0200
@@ -18,6 +18,7 @@
ValidateStringsHandlerProtocol,
)
from pydantic.plugin._loader import _plugins
+from pydantic.plugin._schema_validator import PluggableSchemaValidator
pytestmark = pytest.mark.thread_unsafe(reason='`install_plugin()` is thread
unsafe')
@@ -500,3 +501,40 @@
'BaseModel',
),
]
+
+
+def test_prebuilt_validator_used_for_nested_models() -> None:
+ """With plugins installed, `__pydantic_validator__` is a
`PluggableSchemaValidator`. pydantic-core
+ should still be able to reuse the underlying prebuilt `SchemaValidator`
for nested models,
+ through the `__pydantic_schema_validator__` property.
+ """
+ stack: list[Any] = []
+
+ class CustomOnValidatePython(ValidatePythonHandlerProtocol):
+ def on_enter(self, input, **kwargs) -> None:
+ stack.append(input)
+
+ class Plugin(PydanticPluginProtocol):
+ def new_schema_validator(self, schema, schema_type, schema_type_path,
schema_kind, config, plugin_settings):
+ return CustomOnValidatePython(), None, None
+
+ plugin = Plugin()
+ with install_plugin(plugin):
+
+ class Model(BaseModel):
+ a: int
+
+ class Main(BaseModel):
+ m: Model
+
+ assert isinstance(Main.__pydantic_validator__, PluggableSchemaValidator)
+ assert 'PrebuiltValidator' in
repr(Main.__pydantic_validator__.__pydantic_schema_validator__)
+
+ main = Main.model_validate({'m': {'a': 1}})
+ assert main.m.a == 1
+ # Plugin handlers only fire for the top-level validator, not for the
nested (reused) one:
+ assert stack == [{'m': {'a': 1}}]
+
+ stack.clear()
+ Model.model_validate({'a': 1})
+ assert stack == [{'a': 1}]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/tests/types/test_union.py
new/pydantic-2.13.5/tests/types/test_union.py
--- old/pydantic-2.13.4/tests/types/test_union.py 2026-05-06
10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/tests/types/test_union.py 2026-08-28
15:38:34.000000000 +0200
@@ -1,12 +1,14 @@
-from typing import ClassVar, Literal, Optional, Union
+from typing import Any, ClassVar, Literal, Optional, Union
+import pytest
from typing_extensions import TypedDict
import pydantic
+from pydantic import BaseModel, ConfigDict, TypeAdapter, model_validator
def test_field_serializer_in_nested_union_called_only_twice():
- class MyModel(pydantic.BaseModel):
+ class MyModel(BaseModel):
a: int
b: int
@@ -40,7 +42,7 @@
def test_field_serializer_in_nested_tagged_union_called_only_twice():
- class MyModel(pydantic.BaseModel):
+ class MyModel(BaseModel):
type_: Literal['a'] = 'a'
a: int
@@ -53,14 +55,14 @@
self.__class__.field_a_serializer_calls += 1
return str(value)
- class ModelB(pydantic.BaseModel):
+ class ModelB(BaseModel):
type_: Literal['b'] = 'b'
- class Container(pydantic.BaseModel):
+ class Container(BaseModel):
type_: Literal['a'] = 'a'
u: Union[MyModel, ModelB] = pydantic.Field(..., discriminator='type_')
- class Container2(pydantic.BaseModel):
+ class Container2(BaseModel):
u: Union[Container, ModelB] = pydantic.Field(...,
discriminator='type_')
# forcibly construct model with a False value
@@ -77,14 +79,14 @@
def test_exclude_unset_in_nested_union():
- class Cat(pydantic.BaseModel):
+ class Cat(BaseModel):
type: Literal['cat']
color: Optional[str] = None # field with default
- class Dog(pydantic.BaseModel):
+ class Dog(BaseModel):
type: Literal['dog']
- class Zoo(pydantic.BaseModel):
+ class Zoo(BaseModel):
animals: list[Union[Cat, Dog]]
cat = Cat(type='cat')
@@ -121,3 +123,51 @@
# test to document the behaviour that if all options in the union fail,
# the field is omitted, even if it is required in the TypedDict.
assert ta.validate_python({'x': 'foo'}) == {}
+
+
[email protected]('extra', ['ignore', 'allow'])
[email protected]('reverse_members', [False, True])
+def test_smart_union_fields_set_count_consistent_between_json_and_python(
+ extra: Literal['ignore', 'allow'], reverse_members: bool
+) -> None:
+ """https://github.com/pydantic/pydantic/issues/13729"""
+
+ class Smaller(BaseModel):
+ model_config = ConfigDict(extra=extra)
+
+ a: int
+ b: int
+
+ @model_validator(mode='before')
+ @classmethod
+ def keep_input(cls, value: Any) -> Any:
+ return value
+
+ class Larger(BaseModel):
+ a: int
+ b: int
+ c: int
+
+ ta = TypeAdapter(Union[Larger, Smaller] if reverse_members else
Union[Smaller, Larger])
+
+ assert isinstance(ta.validate_python({'a': 1, 'b': 2, 'c': 3}), Larger)
+ assert isinstance(ta.validate_json('{"a": 1, "b": 2, "c": 3}'), Larger)
+
+
[email protected]('reverse_members', [False, True])
+def test_smart_union_fields_set_count_ignores_extra(reverse_members: bool) ->
None:
+ """https://github.com/pydantic/pydantic/issues/13729"""
+
+ class WithExtra(BaseModel):
+ model_config = ConfigDict(extra='allow')
+
+ a: int
+
+ class Explicit(BaseModel):
+ a: int
+ b: int
+
+ ta = TypeAdapter(Union[Explicit, WithExtra] if reverse_members else
Union[WithExtra, Explicit])
+
+ assert isinstance(ta.validate_python({'a': 1, 'b': 2}), Explicit)
+ assert isinstance(ta.validate_json('{"a": 1, "b": 2}'), Explicit)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pydantic-2.13.4/uv.lock new/pydantic-2.13.5/uv.lock
--- old/pydantic-2.13.4/uv.lock 2026-05-06 10:27:14.000000000 +0200
+++ new/pydantic-2.13.5/uv.lock 2026-08-28 15:38:34.000000000 +0200
@@ -2108,11 +2108,11 @@
[[package]]
name = "packaging"
-version = "24.2"
+version = "26.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url =
"https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz",
hash =
"sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size
= 163950, upload-time = "2024-11-08T09:47:47.202Z" }
+sdist = { url =
"https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz",
hash =
"sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size
= 313412, upload-time = "2026-08-04T18:15:28.737Z" }
wheels = [
- { url =
"https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl",
hash =
"sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size
= 65451, upload-time = "2024-11-08T09:47:44.722Z" },
+ { url =
"https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl",
hash =
"sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size
= 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[package]]
@@ -3200,16 +3200,15 @@
[[package]]
name = "rich"
-version = "13.9.4"
+version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url =
"https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz",
hash =
"sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size
= 223149, upload-time = "2024-11-01T16:43:57.873Z" }
+sdist = { url =
"https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz",
hash =
"sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size
= 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
- { url =
"https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl",
hash =
"sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size
= 242424, upload-time = "2024-11-01T16:43:55.817Z" },
+ { url =
"https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl",
hash =
"sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size
= 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
@@ -3661,7 +3660,7 @@
[[package]]
name = "twine"
-version = "6.2.0"
+version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "id" },
@@ -3675,9 +3674,9 @@
{ name = "rich" },
{ name = "urllib3" },
]
-sdist = { url =
"https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz",
hash =
"sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size
= 172262, upload-time = "2025-09-04T15:43:17.255Z" }
+sdist = { url =
"https://files.pythonhosted.org/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz",
hash =
"sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177", size
= 215032, upload-time = "2026-07-27T15:59:00.825Z" }
wheels = [
- { url =
"https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl",
hash =
"sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size
= 42727, upload-time = "2025-09-04T15:43:15.994Z" },
+ { url =
"https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl",
hash =
"sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7", size
= 43204, upload-time = "2026-07-27T15:58:59.26Z" },
]
[[package]]