Skip to content

Commit bf0bce0

Browse files
authored
Update speedate and truncate microseconds by default (#762)
1 parent 5f660f0 commit bf0bce0

File tree

17 files changed

+392
-193
lines changed

17 files changed

+392
-193
lines changed

Cargo.lock

Lines changed: 116 additions & 97 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pydantic-core"
3-
version = "2.2.0"
3+
version = "2.3.0"
44
edition = "2021"
55
license = "MIT"
66
homepage = "https://github.com/pydantic/pydantic-core"
@@ -35,7 +35,7 @@ enum_dispatch = "0.3.8"
3535
serde = { version = "1.0.147", features = ["derive"] }
3636
# disabled for benchmarks since it makes microbenchmark performance more flakey
3737
mimalloc = { version = "0.1.30", optional = true, default-features = false, features = ["local_dynamic_tls"] }
38-
speedate = "0.9.1"
38+
speedate = "0.10.0"
3939
ahash = "0.8.0"
4040
url = "2.3.1"
4141
# idna is already required by url, added here to be explicit

python/pydantic_core/core_schema.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,7 @@ class TimeSchema(TypedDict, total=False):
848848
lt: time
849849
gt: time
850850
tz_constraint: Union[Literal['aware', 'naive'], int]
851+
microseconds_precision: Literal['truncate', 'error']
851852
ref: str
852853
metadata: Any
853854
serialization: SerSchema
@@ -861,6 +862,7 @@ def time_schema(
861862
lt: time | None = None,
862863
gt: time | None = None,
863864
tz_constraint: Literal['aware', 'naive'] | int | None = None,
865+
microseconds_precision: Literal['truncate', 'error'] = 'truncate',
864866
ref: str | None = None,
865867
metadata: Any = None,
866868
serialization: SerSchema | None = None,
@@ -884,6 +886,7 @@ def time_schema(
884886
lt: The value must be strictly less than this time
885887
gt: The value must be strictly greater than this time
886888
tz_constraint: The value must be timezone aware or naive, or an int to indicate required tz offset
889+
microseconds_precision: The behavior when seconds have more than 6 digits or microseconds is too large
887890
ref: optional unique identifier of the schema, used to reference the schema in other places
888891
metadata: Any other information you want to include with the schema, not used by pydantic-core
889892
serialization: Custom serialization schema
@@ -896,6 +899,7 @@ def time_schema(
896899
lt=lt,
897900
gt=gt,
898901
tz_constraint=tz_constraint,
902+
microseconds_precision=microseconds_precision,
899903
ref=ref,
900904
metadata=metadata,
901905
serialization=serialization,
@@ -914,6 +918,7 @@ class DatetimeSchema(TypedDict, total=False):
914918
# defaults to current local utc offset from `time.localtime().tm_gmtoff`
915919
# value is restricted to -86_400 < offset < 86_400 by bounds in generate_self_schema.py
916920
now_utc_offset: int
921+
microseconds_precision: Literal['truncate', 'error'] = ('truncate',)
917922
ref: str
918923
metadata: Any
919924
serialization: SerSchema
@@ -929,6 +934,7 @@ def datetime_schema(
929934
now_op: Literal['past', 'future'] | None = None,
930935
tz_constraint: Literal['aware', 'naive'] | int | None = None,
931936
now_utc_offset: int | None = None,
937+
microseconds_precision: Literal['truncate', 'error'] = 'truncate',
932938
ref: str | None = None,
933939
metadata: Any = None,
934940
serialization: SerSchema | None = None,
@@ -956,6 +962,7 @@ def datetime_schema(
956962
tz_constraint: The value must be timezone aware or naive, or an int to indicate required tz offset
957963
TODO: use of a tzinfo where offset changes based on the datetime is not yet supported
958964
now_utc_offset: The value must be in the past or future relative to the current datetime with this utc offset
965+
microseconds_precision: The behavior when seconds have more than 6 digits or microseconds is too large
959966
ref: optional unique identifier of the schema, used to reference the schema in other places
960967
metadata: Any other information you want to include with the schema, not used by pydantic-core
961968
serialization: Custom serialization schema
@@ -970,6 +977,7 @@ def datetime_schema(
970977
now_op=now_op,
971978
tz_constraint=tz_constraint,
972979
now_utc_offset=now_utc_offset,
980+
microseconds_precision=microseconds_precision,
973981
ref=ref,
974982
metadata=metadata,
975983
serialization=serialization,
@@ -983,6 +991,7 @@ class TimedeltaSchema(TypedDict, total=False):
983991
ge: timedelta
984992
lt: timedelta
985993
gt: timedelta
994+
microseconds_precision: Literal['truncate', 'error']
986995
ref: str
987996
metadata: Any
988997
serialization: SerSchema
@@ -995,6 +1004,7 @@ def timedelta_schema(
9951004
ge: timedelta | None = None,
9961005
lt: timedelta | None = None,
9971006
gt: timedelta | None = None,
1007+
microseconds_precision: Literal['truncate', 'error'] = 'truncate',
9981008
ref: str | None = None,
9991009
metadata: Any = None,
10001010
serialization: SerSchema | None = None,
@@ -1017,6 +1027,7 @@ def timedelta_schema(
10171027
ge: The value must be greater than or equal to this timedelta
10181028
lt: The value must be strictly less than this timedelta
10191029
gt: The value must be strictly greater than this timedelta
1030+
microseconds_precision: The behavior when seconds have more than 6 digits or microseconds is too large
10201031
ref: optional unique identifier of the schema, used to reference the schema in other places
10211032
metadata: Any other information you want to include with the schema, not used by pydantic-core
10221033
serialization: Custom serialization schema
@@ -1028,6 +1039,7 @@ def timedelta_schema(
10281039
ge=ge,
10291040
lt=lt,
10301041
gt=gt,
1042+
microseconds_precision=microseconds_precision,
10311043
ref=ref,
10321044
metadata=metadata,
10331045
serialization=serialization,

src/input/datetime.rs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use pyo3::intern;
22
use pyo3::prelude::*;
33
use pyo3::types::{PyDate, PyDateTime, PyDelta, PyDeltaAccess, PyDict, PyTime, PyTzInfo};
4-
use speedate::{Date, DateTime, Duration, ParseError, Time};
4+
use speedate::MicrosecondsPrecisionOverflowBehavior;
5+
use speedate::{Date, DateTime, Duration, ParseError, Time, TimeConfig};
56
use std::borrow::Cow;
67
use strum::EnumMessage;
78

@@ -264,8 +265,17 @@ pub fn bytes_as_date<'a>(input: &'a impl Input<'a>, bytes: &[u8]) -> ValResult<'
264265
}
265266
}
266267

267-
pub fn bytes_as_time<'a>(input: &'a impl Input<'a>, bytes: &[u8]) -> ValResult<'a, EitherTime<'a>> {
268-
match Time::parse_bytes(bytes) {
268+
pub fn bytes_as_time<'a>(
269+
input: &'a impl Input<'a>,
270+
bytes: &[u8],
271+
microseconds_overflow_behavior: MicrosecondsPrecisionOverflowBehavior,
272+
) -> ValResult<'a, EitherTime<'a>> {
273+
match Time::parse_bytes_with_config(
274+
bytes,
275+
TimeConfig {
276+
microseconds_precision_overflow_behavior: microseconds_overflow_behavior,
277+
},
278+
) {
269279
Ok(date) => Ok(date.into()),
270280
Err(err) => Err(ValError::new(
271281
ErrorType::TimeParsing {
@@ -276,8 +286,17 @@ pub fn bytes_as_time<'a>(input: &'a impl Input<'a>, bytes: &[u8]) -> ValResult<'
276286
}
277287
}
278288

279-
pub fn bytes_as_datetime<'a, 'b>(input: &'a impl Input<'a>, bytes: &'b [u8]) -> ValResult<'a, EitherDateTime<'a>> {
280-
match DateTime::parse_bytes(bytes) {
289+
pub fn bytes_as_datetime<'a, 'b>(
290+
input: &'a impl Input<'a>,
291+
bytes: &'b [u8],
292+
microseconds_overflow_behavior: MicrosecondsPrecisionOverflowBehavior,
293+
) -> ValResult<'a, EitherDateTime<'a>> {
294+
match DateTime::parse_bytes_with_config(
295+
bytes,
296+
TimeConfig {
297+
microseconds_precision_overflow_behavior: microseconds_overflow_behavior,
298+
},
299+
) {
281300
Ok(dt) => Ok(dt.into()),
282301
Err(err) => Err(ValError::new(
283302
ErrorType::DatetimeParsing {
@@ -389,8 +408,17 @@ fn map_timedelta_err<'a>(input: &'a impl Input<'a>, err: ParseError) -> ValError
389408
)
390409
}
391410

392-
pub fn bytes_as_timedelta<'a, 'b>(input: &'a impl Input<'a>, bytes: &'b [u8]) -> ValResult<'a, EitherTimedelta<'a>> {
393-
match Duration::parse_bytes(bytes) {
411+
pub fn bytes_as_timedelta<'a, 'b>(
412+
input: &'a impl Input<'a>,
413+
bytes: &'b [u8],
414+
microseconds_overflow_behavior: MicrosecondsPrecisionOverflowBehavior,
415+
) -> ValResult<'a, EitherTimedelta<'a>> {
416+
match Duration::parse_bytes_with_config(
417+
bytes,
418+
TimeConfig {
419+
microseconds_precision_overflow_behavior: microseconds_overflow_behavior,
420+
},
421+
) {
394422
Ok(dt) => Ok(dt.into()),
395423
Err(err) => Err(map_timedelta_err(input, err)),
396424
}

src/input/input_abstract.rs

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -238,42 +238,72 @@ pub trait Input<'a>: fmt::Debug + ToPyObject {
238238
self.strict_date()
239239
}
240240

241-
fn validate_time(&self, strict: bool) -> ValResult<EitherTime> {
241+
fn validate_time(
242+
&self,
243+
strict: bool,
244+
microseconds_overflow_behavior: speedate::MicrosecondsPrecisionOverflowBehavior,
245+
) -> ValResult<EitherTime> {
242246
if strict {
243-
self.strict_time()
247+
self.strict_time(microseconds_overflow_behavior)
244248
} else {
245-
self.lax_time()
249+
self.lax_time(microseconds_overflow_behavior)
246250
}
247251
}
248-
fn strict_time(&self) -> ValResult<EitherTime>;
252+
fn strict_time(
253+
&self,
254+
microseconds_overflow_behavior: speedate::MicrosecondsPrecisionOverflowBehavior,
255+
) -> ValResult<EitherTime>;
249256
#[cfg_attr(has_no_coverage, no_coverage)]
250-
fn lax_time(&self) -> ValResult<EitherTime> {
251-
self.strict_time()
252-
}
253-
254-
fn validate_datetime(&self, strict: bool) -> ValResult<EitherDateTime> {
257+
fn lax_time(
258+
&self,
259+
microseconds_overflow_behavior: speedate::MicrosecondsPrecisionOverflowBehavior,
260+
) -> ValResult<EitherTime> {
261+
self.strict_time(microseconds_overflow_behavior)
262+
}
263+
264+
fn validate_datetime(
265+
&self,
266+
strict: bool,
267+
microseconds_overflow_behavior: speedate::MicrosecondsPrecisionOverflowBehavior,
268+
) -> ValResult<EitherDateTime> {
255269
if strict {
256-
self.strict_datetime()
270+
self.strict_datetime(microseconds_overflow_behavior)
257271
} else {
258-
self.lax_datetime()
272+
self.lax_datetime(microseconds_overflow_behavior)
259273
}
260274
}
261-
fn strict_datetime(&self) -> ValResult<EitherDateTime>;
275+
fn strict_datetime(
276+
&self,
277+
microseconds_overflow_behavior: speedate::MicrosecondsPrecisionOverflowBehavior,
278+
) -> ValResult<EitherDateTime>;
262279
#[cfg_attr(has_no_coverage, no_coverage)]
263-
fn lax_datetime(&self) -> ValResult<EitherDateTime> {
264-
self.strict_datetime()
265-
}
266-
267-
fn validate_timedelta(&self, strict: bool) -> ValResult<EitherTimedelta> {
280+
fn lax_datetime(
281+
&self,
282+
microseconds_overflow_behavior: speedate::MicrosecondsPrecisionOverflowBehavior,
283+
) -> ValResult<EitherDateTime> {
284+
self.strict_datetime(microseconds_overflow_behavior)
285+
}
286+
287+
fn validate_timedelta(
288+
&self,
289+
strict: bool,
290+
microseconds_overflow_behavior: speedate::MicrosecondsPrecisionOverflowBehavior,
291+
) -> ValResult<EitherTimedelta> {
268292
if strict {
269-
self.strict_timedelta()
293+
self.strict_timedelta(microseconds_overflow_behavior)
270294
} else {
271-
self.lax_timedelta()
295+
self.lax_timedelta(microseconds_overflow_behavior)
272296
}
273297
}
274-
fn strict_timedelta(&self) -> ValResult<EitherTimedelta>;
298+
fn strict_timedelta(
299+
&self,
300+
microseconds_overflow_behavior: speedate::MicrosecondsPrecisionOverflowBehavior,
301+
) -> ValResult<EitherTimedelta>;
275302
#[cfg_attr(has_no_coverage, no_coverage)]
276-
fn lax_timedelta(&self) -> ValResult<EitherTimedelta> {
277-
self.strict_timedelta()
303+
fn lax_timedelta(
304+
&self,
305+
microseconds_overflow_behavior: speedate::MicrosecondsPrecisionOverflowBehavior,
306+
) -> ValResult<EitherTimedelta> {
307+
self.strict_timedelta(microseconds_overflow_behavior)
278308
}
279309
}

0 commit comments

Comments
 (0)