Skip to content

Commit e1f4bba

Browse files
committed
Merge branch 'main' into fix-complex-infer
2 parents ef007bb + 184e7be commit e1f4bba

File tree

19 files changed

+82
-277
lines changed

19 files changed

+82
-277
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ jobs:
341341
# NOTE!: as per https://github.com/pydantic/pydantic-core/pull/149 this version needs to match the version
342342
# in node_modules/pyodide/repodata.json, to get the version, run:
343343
# `cat node_modules/pyodide/repodata.json | python -m json.tool | rg platform`
344-
version: '3.1.46'
344+
version: '3.1.58'
345345
actions-cache-folder: emsdk-cache
346346

347347
- run: pip install 'maturin>=1,<2' 'ruff==0.5.0' typing_extensions

Cargo.lock

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pydantic-core"
3-
version = "2.25.0"
3+
version = "2.25.1"
44
edition = "2021"
55
license = "MIT"
66
homepage = "https://github.com/pydantic/pydantic-core"
@@ -30,12 +30,12 @@ rust-version = "1.75"
3030
# TODO it would be very nice to remove the "py-clone" feature as it can panic,
3131
# but needs a bit of work to make sure it's not used in the codebase
3232
pyo3 = { version = "0.22.5", features = ["generate-import-lib", "num-bigint", "py-clone"] }
33-
regex = "1.11.0"
33+
regex = "1.11.1"
3434
strum = { version = "0.26.3", features = ["derive"] }
3535
strum_macros = "0.26.4"
3636
serde_json = {version = "1.0.132", features = ["arbitrary_precision", "preserve_order"]}
3737
enum_dispatch = "0.3.13"
38-
serde = { version = "1.0.213", features = ["derive"] }
38+
serde = { version = "1.0.214", features = ["derive"] }
3939
speedate = "0.14.4"
4040
smallvec = "1.13.2"
4141
ahash = "0.8.10"

Makefile

+2-2
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ endif
8484

8585
.PHONY: build-wasm
8686
build-wasm:
87-
@echo 'This requires python 3.11, maturin and emsdk to be installed'
88-
maturin build --release --target wasm32-unknown-emscripten --out dist -i 3.11
87+
@echo 'This requires python 3.12, maturin and emsdk to be installed'
88+
maturin build --release --target wasm32-unknown-emscripten --out dist -i 3.12
8989
ls -lh dist
9090

9191
.PHONY: format

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"main": "tests/emscripten_runner.js",
99
"dependencies": {
1010
"prettier": "^2.7.1",
11-
"pyodide": "^0.25.0"
11+
"pyodide": "^0.26.3"
1212
},
1313
"scripts": {
1414
"test": "node tests/emscripten_runner.js",

python/pydantic_core/_pydantic_core.pyi

+4-4
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ def to_json(
357357
by_alias: bool = True,
358358
exclude_none: bool = False,
359359
round_trip: bool = False,
360-
timedelta_mode: Literal['iso8601', 'seconds_float', 'milliseconds_float'] = 'iso8601',
360+
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
361361
bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8',
362362
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
363363
serialize_unknown: bool = False,
@@ -378,7 +378,7 @@ def to_json(
378378
by_alias: Whether to use the alias names of fields.
379379
exclude_none: Whether to exclude fields that have a value of `None`.
380380
round_trip: Whether to enable serialization and validation round-trip support.
381-
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'`, `'seconds_float'` or `'milliseconds_float'`.
381+
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
382382
bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`.
383383
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
384384
serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails
@@ -432,7 +432,7 @@ def to_jsonable_python(
432432
by_alias: bool = True,
433433
exclude_none: bool = False,
434434
round_trip: bool = False,
435-
timedelta_mode: Literal['iso8601', 'seconds_float', 'milliseconds_float'] = 'iso8601',
435+
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
436436
bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8',
437437
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
438438
serialize_unknown: bool = False,
@@ -453,7 +453,7 @@ def to_jsonable_python(
453453
by_alias: Whether to use the alias names of fields.
454454
exclude_none: Whether to exclude fields that have a value of `None`.
455455
round_trip: Whether to enable serialization and validation round-trip support.
456-
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'`, `'seconds_float'`, or`'milliseconds_float'`.
456+
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
457457
bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`.
458458
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
459459
serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails

python/pydantic_core/core_schema.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class CoreConfig(TypedDict, total=False):
105105
# fields related to float fields only
106106
allow_inf_nan: bool # default: True
107107
# the config options are used to customise serialization to JSON
108-
ser_json_timedelta: Literal['iso8601', 'seconds_float', 'milliseconds_float'] # default: 'iso8601'
108+
ser_json_timedelta: Literal['iso8601', 'float'] # default: 'iso8601'
109109
ser_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8'
110110
ser_json_inf_nan: Literal['null', 'constants', 'strings'] # default: 'null'
111111
val_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8'

src/input/datetime.rs

-87
Original file line numberDiff line numberDiff line change
@@ -109,93 +109,6 @@ impl<'a> EitherTimedelta<'a> {
109109
Self::Raw(duration) => duration_as_pytimedelta(py, duration),
110110
}
111111
}
112-
113-
pub fn total_seconds(&self) -> PyResult<f64> {
114-
match self {
115-
Self::Raw(timedelta) => {
116-
let mut days: i64 = i64::from(timedelta.day);
117-
let mut seconds: i64 = i64::from(timedelta.second);
118-
let mut microseconds = i64::from(timedelta.microsecond);
119-
if !timedelta.positive {
120-
days = -days;
121-
seconds = -seconds;
122-
microseconds = -microseconds;
123-
}
124-
125-
let days_seconds = (86_400 * days) + seconds;
126-
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
127-
let total_microseconds = days_seconds_as_micros + microseconds;
128-
Ok(total_microseconds as f64 / 1_000_000.0)
129-
} else {
130-
// Fall back to floating-point operations if the multiplication overflows
131-
let total_seconds = days_seconds as f64 + microseconds as f64 / 1_000_000.0;
132-
Ok(total_seconds)
133-
}
134-
}
135-
Self::PyExact(py_timedelta) => {
136-
let days: i64 = py_timedelta.get_days().into(); // -999999999 to 999999999
137-
let seconds: i64 = py_timedelta.get_seconds().into(); // 0 through 86399
138-
let microseconds = py_timedelta.get_microseconds(); // 0 through 999999
139-
let days_seconds = (86_400 * days) + seconds;
140-
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
141-
let total_microseconds = days_seconds_as_micros + i64::from(microseconds);
142-
Ok(total_microseconds as f64 / 1_000_000.0)
143-
} else {
144-
// Fall back to floating-point operations if the multiplication overflows
145-
let total_seconds = days_seconds as f64 + f64::from(microseconds) / 1_000_000.0;
146-
Ok(total_seconds)
147-
}
148-
}
149-
Self::PySubclass(py_timedelta) => py_timedelta
150-
.call_method0(intern!(py_timedelta.py(), "total_seconds"))?
151-
.extract(),
152-
}
153-
}
154-
155-
pub fn total_milliseconds(&self) -> PyResult<f64> {
156-
match self {
157-
Self::Raw(timedelta) => {
158-
let mut days: i64 = i64::from(timedelta.day);
159-
let mut seconds: i64 = i64::from(timedelta.second);
160-
let mut microseconds = i64::from(timedelta.microsecond);
161-
if !timedelta.positive {
162-
days = -days;
163-
seconds = -seconds;
164-
microseconds = -microseconds;
165-
}
166-
167-
let days_seconds = (86_400 * days) + seconds;
168-
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
169-
let total_microseconds = days_seconds_as_micros + microseconds;
170-
Ok(total_microseconds as f64 / 1_000.0)
171-
} else {
172-
// Fall back to floating-point operations if the multiplication overflows
173-
let total_seconds = days_seconds as f64 + microseconds as f64 / 1_000.0;
174-
Ok(total_seconds)
175-
}
176-
}
177-
Self::PyExact(py_timedelta) => {
178-
let days: i64 = py_timedelta.get_days().into(); // -999999999 to 999999999
179-
let seconds: i64 = py_timedelta.get_seconds().into(); // 0 through 86399
180-
let microseconds = py_timedelta.get_microseconds(); // 0 through 999999
181-
let days_seconds = (86_400 * days) + seconds;
182-
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
183-
let total_microseconds = days_seconds_as_micros + i64::from(microseconds);
184-
Ok(total_microseconds as f64 / 1_000.0)
185-
} else {
186-
// Fall back to floating-point operations if the multiplication overflows
187-
let total_milliseconds = days_seconds as f64 * 1_000.0 + f64::from(microseconds) / 1_000.0;
188-
Ok(total_milliseconds)
189-
}
190-
}
191-
Self::PySubclass(py_timedelta) => {
192-
let total_seconds: f64 = py_timedelta
193-
.call_method0(intern!(py_timedelta.py(), "total_seconds"))?
194-
.extract()?;
195-
Ok(total_seconds / 1000.0)
196-
}
197-
}
198-
}
199112
}
200113

201114
impl<'a> TryFrom<&'_ Bound<'a, PyAny>> for EitherTimedelta<'a> {

src/serializers/config.rs

+20-22
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::str::{from_utf8, FromStr, Utf8Error};
44
use base64::Engine;
55
use pyo3::intern;
66
use pyo3::prelude::*;
7-
use pyo3::types::{PyDict, PyString};
7+
use pyo3::types::{PyDelta, PyDict, PyString};
88

99
use serde::ser::Error;
1010

@@ -88,8 +88,7 @@ serialization_mode! {
8888
TimedeltaMode,
8989
"ser_json_timedelta",
9090
Iso8601 => "iso8601",
91-
SecondsFloat => "seconds_float",
92-
MillisecondsFloat => "milliseconds_float"
91+
Float => "float",
9392
}
9493

9594
serialization_mode! {
@@ -109,42 +108,43 @@ serialization_mode! {
109108
}
110109

111110
impl TimedeltaMode {
111+
fn total_seconds<'py>(py_timedelta: &Bound<'py, PyDelta>) -> PyResult<Bound<'py, PyAny>> {
112+
py_timedelta.call_method0(intern!(py_timedelta.py(), "total_seconds"))
113+
}
114+
112115
pub fn either_delta_to_json(self, py: Python, either_delta: &EitherTimedelta) -> PyResult<PyObject> {
113116
match self {
114117
Self::Iso8601 => {
115118
let d = either_delta.to_duration()?;
116119
Ok(d.to_string().into_py(py))
117120
}
118-
Self::SecondsFloat => {
119-
let seconds: f64 = either_delta.total_seconds()?;
121+
Self::Float => {
122+
// convert to int via a py timedelta not duration since we know this this case the input would have
123+
// been a py timedelta
124+
let py_timedelta = either_delta.try_into_py(py)?;
125+
let seconds = Self::total_seconds(&py_timedelta)?;
120126
Ok(seconds.into_py(py))
121127
}
122-
Self::MillisecondsFloat => {
123-
let milliseconds: f64 = either_delta.total_milliseconds()?;
124-
Ok(milliseconds.into_py(py))
125-
}
126128
}
127129
}
128130

129-
pub fn json_key<'py>(self, either_delta: &EitherTimedelta) -> PyResult<Cow<'py, str>> {
131+
pub fn json_key<'py>(self, py: Python, either_delta: &EitherTimedelta) -> PyResult<Cow<'py, str>> {
130132
match self {
131133
Self::Iso8601 => {
132134
let d = either_delta.to_duration()?;
133135
Ok(d.to_string().into())
134136
}
135-
Self::SecondsFloat => {
136-
let seconds: f64 = either_delta.total_seconds()?;
137+
Self::Float => {
138+
let py_timedelta = either_delta.try_into_py(py)?;
139+
let seconds: f64 = Self::total_seconds(&py_timedelta)?.extract()?;
137140
Ok(seconds.to_string().into())
138141
}
139-
Self::MillisecondsFloat => {
140-
let milliseconds: f64 = either_delta.total_milliseconds()?;
141-
Ok(milliseconds.to_string().into())
142-
}
143142
}
144143
}
145144

146145
pub fn timedelta_serialize<S: serde::ser::Serializer>(
147146
self,
147+
py: Python,
148148
either_delta: &EitherTimedelta,
149149
serializer: S,
150150
) -> Result<S::Ok, S::Error> {
@@ -153,14 +153,12 @@ impl TimedeltaMode {
153153
let d = either_delta.to_duration().map_err(py_err_se_err)?;
154154
serializer.serialize_str(&d.to_string())
155155
}
156-
Self::SecondsFloat => {
157-
let seconds: f64 = either_delta.total_seconds().map_err(py_err_se_err)?;
156+
Self::Float => {
157+
let py_timedelta = either_delta.try_into_py(py).map_err(py_err_se_err)?;
158+
let seconds = Self::total_seconds(&py_timedelta).map_err(py_err_se_err)?;
159+
let seconds: f64 = seconds.extract().map_err(py_err_se_err)?;
158160
serializer.serialize_f64(seconds)
159161
}
160-
Self::MillisecondsFloat => {
161-
let milliseconds: f64 = either_delta.total_milliseconds().map_err(py_err_se_err)?;
162-
serializer.serialize_f64(milliseconds)
163-
}
164162
}
165163
}
166164
}

src/serializers/infer.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ pub(crate) fn infer_serialize_known<S: Serializer>(
471471
extra
472472
.config
473473
.timedelta_mode
474-
.timedelta_serialize(&either_delta, serializer)
474+
.timedelta_serialize(value.py(), &either_delta, serializer)
475475
}
476476
ObType::Url => {
477477
let py_url: PyUrl = value.extract().map_err(py_err_se_err)?;
@@ -649,7 +649,7 @@ pub(crate) fn infer_json_key_known<'a>(
649649
}
650650
ObType::Timedelta => {
651651
let either_delta = EitherTimedelta::try_from(key)?;
652-
extra.config.timedelta_mode.json_key(&either_delta)
652+
extra.config.timedelta_mode.json_key(key.py(), &either_delta)
653653
}
654654
ObType::Url => {
655655
let py_url: PyUrl = key.extract()?;

src/serializers/type_serializers/timedelta.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ impl TypeSerializer for TimeDeltaSerializer {
5454

5555
fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult<Cow<'a, str>> {
5656
match EitherTimedelta::try_from(key) {
57-
Ok(either_timedelta) => self.timedelta_mode.json_key(&either_timedelta),
57+
Ok(either_timedelta) => self.timedelta_mode.json_key(key.py(), &either_timedelta),
5858
Err(_) => {
5959
extra.warnings.on_fallback_py(self.get_name(), key, extra)?;
6060
infer_json_key(key, extra)
@@ -71,7 +71,9 @@ impl TypeSerializer for TimeDeltaSerializer {
7171
extra: &Extra,
7272
) -> Result<S::Ok, S::Error> {
7373
match EitherTimedelta::try_from(value) {
74-
Ok(either_timedelta) => self.timedelta_mode.timedelta_serialize(&either_timedelta, serializer),
74+
Ok(either_timedelta) => self
75+
.timedelta_mode
76+
.timedelta_serialize(value.py(), &either_timedelta, serializer),
7577
Err(_) => {
7678
extra.warnings.on_fallback_ser::<S>(self.get_name(), value, extra)?;
7779
infer_serialize(value, serializer, include, exclude, extra)

src/validators/string.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -200,15 +200,14 @@ impl StrConstrainedValidator {
200200
}
201201

202202
// whether any of the constraints/customisations are actually enabled
203-
// except strict which can be set on StrValidator
203+
// except strict and coerce_numbers_to_str which can be set on StrValidator
204204
fn has_constraints_set(&self) -> bool {
205205
self.pattern.is_some()
206206
|| self.max_length.is_some()
207207
|| self.min_length.is_some()
208208
|| self.strip_whitespace
209209
|| self.to_lower
210210
|| self.to_upper
211-
|| self.coerce_numbers_to_str
212211
}
213212
}
214213

0 commit comments

Comments
 (0)