Skip to content

Commit d39c591

Browse files
committed
Merge main
2 parents 37fa39f + f5b804b commit d39c591

File tree

12 files changed

+155
-39
lines changed

12 files changed

+155
-39
lines changed

Cargo.lock

Lines changed: 3 additions & 3 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.3.0"
3+
version = "2.4.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.10.0"
38+
speedate = "0.11.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/_pydantic_core.pyi

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import datetime
34
import decimal
45
import sys
56
from typing import Any, Callable, Generic, Optional, Type, TypeVar
@@ -43,6 +44,7 @@ __all__ = [
4344
'to_json',
4445
'to_jsonable_python',
4546
'list_all_errors',
47+
'TzInfo',
4648
]
4749
__version__: str
4850
build_profile: str
@@ -71,7 +73,7 @@ class SchemaValidator:
7173
*,
7274
strict: bool | None = None,
7375
from_attributes: bool | None = None,
74-
context: Any = None,
76+
context: 'dict[str, Any] | None' = None,
7577
self_instance: Any | None = None,
7678
) -> Any: ...
7779
def isinstance_python(
@@ -80,15 +82,15 @@ class SchemaValidator:
8082
*,
8183
strict: bool | None = None,
8284
from_attributes: bool | None = None,
83-
context: Any = None,
85+
context: 'dict[str, Any] | None' = None,
8486
self_instance: Any | None = None,
8587
) -> bool: ...
8688
def validate_json(
8789
self,
8890
input: str | bytes | bytearray,
8991
*,
9092
strict: bool | None = None,
91-
context: Any = None,
93+
context: 'dict[str, Any] | None' = None,
9294
self_instance: Any | None = None,
9395
) -> Any: ...
9496
def validate_assignment(
@@ -99,7 +101,7 @@ class SchemaValidator:
99101
*,
100102
strict: bool | None = None,
101103
from_attributes: bool | None = None,
102-
context: Any = None,
104+
context: 'dict[str, Any] | None' = None,
103105
) -> dict[str, Any] | tuple[dict[str, Any], dict[str, Any] | None, set[str]]:
104106
"""
105107
ModelValidator and ModelFieldsValidator will return a tuple of (fields data, extra data, fields set)
@@ -326,3 +328,10 @@ def list_all_errors() -> list[ErrorTypeInfo]:
326328
"""
327329
Get information about all built-in errors.
328330
"""
331+
332+
@final
333+
class TzInfo(datetime.tzinfo):
334+
def tzname(self, _dt: datetime.datetime | None) -> str | None: ...
335+
def utcoffset(self, _dt: datetime.datetime | None) -> datetime.timedelta: ...
336+
def dst(self, _dt: datetime.datetime | None) -> datetime.timedelta: ...
337+
def __deepcopy__(self, _memo: dict[Any, Any]) -> 'TzInfo': ...

src/input/datetime.rs

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

9-
use crate::errors::{ErrorType, ValError, ValResult};
9+
use strum::EnumMessage;
1010

1111
use super::Input;
12+
use crate::errors::{ErrorType, ValError, ValResult};
1213

1314
#[cfg_attr(debug_assertions, derive(Debug))]
1415
pub enum EitherDate<'a> {
@@ -272,8 +273,9 @@ pub fn bytes_as_time<'a>(
272273
) -> ValResult<'a, EitherTime<'a>> {
273274
match Time::parse_bytes_with_config(
274275
bytes,
275-
TimeConfig {
276+
&TimeConfig {
276277
microseconds_precision_overflow_behavior: microseconds_overflow_behavior,
278+
unix_timestamp_offset: Some(0),
277279
},
278280
) {
279281
Ok(date) => Ok(date.into()),
@@ -293,8 +295,9 @@ pub fn bytes_as_datetime<'a, 'b>(
293295
) -> ValResult<'a, EitherDateTime<'a>> {
294296
match DateTime::parse_bytes_with_config(
295297
bytes,
296-
TimeConfig {
298+
&TimeConfig {
297299
microseconds_precision_overflow_behavior: microseconds_overflow_behavior,
300+
unix_timestamp_offset: Some(0),
298301
},
299302
) {
300303
Ok(dt) => Ok(dt.into()),
@@ -312,7 +315,14 @@ pub fn int_as_datetime<'a>(
312315
timestamp: i64,
313316
timestamp_microseconds: u32,
314317
) -> ValResult<EitherDateTime> {
315-
match DateTime::from_timestamp(timestamp, timestamp_microseconds) {
318+
match DateTime::from_timestamp_with_config(
319+
timestamp,
320+
timestamp_microseconds,
321+
&TimeConfig {
322+
unix_timestamp_offset: Some(0),
323+
..Default::default()
324+
},
325+
) {
316326
Ok(dt) => Ok(dt.into()),
317327
Err(err) => Err(ValError::new(
318328
ErrorType::DatetimeParsing {
@@ -381,7 +391,14 @@ pub fn int_as_time<'a>(
381391
// ok
382392
t => t as u32,
383393
};
384-
match Time::from_timestamp(time_timestamp, timestamp_microseconds) {
394+
match Time::from_timestamp_with_config(
395+
time_timestamp,
396+
timestamp_microseconds,
397+
&TimeConfig {
398+
unix_timestamp_offset: Some(0),
399+
..Default::default()
400+
},
401+
) {
385402
Ok(dt) => Ok(dt.into()),
386403
Err(err) => Err(ValError::new(
387404
ErrorType::TimeParsing {
@@ -415,8 +432,9 @@ pub fn bytes_as_timedelta<'a, 'b>(
415432
) -> ValResult<'a, EitherTimedelta<'a>> {
416433
match Duration::parse_bytes_with_config(
417434
bytes,
418-
TimeConfig {
435+
&TimeConfig {
419436
microseconds_precision_overflow_behavior: microseconds_overflow_behavior,
437+
unix_timestamp_offset: Some(0),
420438
},
421439
) {
422440
Ok(dt) => Ok(dt.into()),
@@ -447,7 +465,7 @@ pub fn float_as_duration<'a>(input: &'a impl Input<'a>, total_seconds: f64) -> V
447465
#[pyclass(module = "pydantic_core._pydantic_core", extends = PyTzInfo)]
448466
#[derive(Clone)]
449467
#[cfg_attr(debug_assertions, derive(Debug))]
450-
struct TzInfo {
468+
pub struct TzInfo {
451469
seconds: i32,
452470
}
453471

@@ -486,4 +504,10 @@ impl TzInfo {
486504
fn __deepcopy__(&self, py: Python, _memo: &PyDict) -> PyResult<Py<Self>> {
487505
Py::new(py, self.clone())
488506
}
507+
508+
pub fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
509+
let args = (self.seconds,);
510+
let cls = Py::new(py, self.clone())?.getattr(py, "__class__")?;
511+
Ok((cls, args).into_py(py))
512+
}
489513
}

src/input/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod parse_json;
1010
mod return_enums;
1111
mod shared;
1212

13+
pub use datetime::TzInfo;
1314
pub(crate) use datetime::{
1415
duration_as_pytimedelta, pydate_as_date, pydatetime_as_datetime, pytime_as_time, pytimedelta_as_duration,
1516
EitherDate, EitherDateTime, EitherTime, EitherTimedelta,

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mod url;
2424
mod validators;
2525

2626
// required for benchmarks
27+
pub use self::input::TzInfo;
2728
pub use self::url::{PyMultiHostUrl, PyUrl};
2829
pub use argument_markers::{ArgsKwargs, PydanticUndefinedType};
2930
pub use build_tools::SchemaError;
@@ -93,6 +94,7 @@ fn _pydantic_core(py: Python, m: &PyModule) -> PyResult<()> {
9394
m.add_class::<PyMultiHostUrl>()?;
9495
m.add_class::<ArgsKwargs>()?;
9596
m.add_class::<SchemaSerializer>()?;
97+
m.add_class::<TzInfo>()?;
9698
m.add_function(wrap_pyfunction!(to_json, m)?)?;
9799
m.add_function(wrap_pyfunction!(to_jsonable_python, m)?)?;
98100
m.add_function(wrap_pyfunction!(list_all_errors, m)?)?;

src/serializers/computed_fields.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ impl ComputedField {
135135
let value = self
136136
.serializer
137137
.to_python(next_value, next_include, next_exclude, extra)?;
138+
if extra.exclude_none && value.is_none(py) {
139+
return Ok(());
140+
}
138141
let key = match extra.by_alias {
139142
true => self.alias_py.as_ref(py),
140143
false => property_name_py,

src/validators/date.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ fn date_from_datetime<'data>(
153153
minute: 0,
154154
second: 0,
155155
microsecond: 0,
156-
tz_offset: None,
156+
tz_offset: dt.time.tz_offset,
157157
};
158158
if dt.time == zero_time {
159159
Ok(EitherDate::Raw(dt.date))

tests/serializers/test_model.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,46 @@ def volume(self) -> int:
605605
assert s.to_json(Model(3, 4)) == b'{"width":3,"height":4,"Area":12,"volume":48}'
606606

607607

608+
def test_computed_field_to_python_exclude_none():
609+
@dataclasses.dataclass
610+
class Model:
611+
width: int
612+
height: int
613+
614+
@property
615+
def area(self) -> int:
616+
return self.width * self.height
617+
618+
@property
619+
def volume(self) -> None:
620+
return None
621+
622+
s = SchemaSerializer(
623+
core_schema.model_schema(
624+
Model,
625+
core_schema.model_fields_schema(
626+
{
627+
'width': core_schema.model_field(core_schema.int_schema()),
628+
'height': core_schema.model_field(core_schema.int_schema()),
629+
},
630+
computed_fields=[
631+
core_schema.computed_field('area', core_schema.int_schema(), alias='Area'),
632+
core_schema.computed_field('volume', core_schema.int_schema()),
633+
],
634+
),
635+
)
636+
)
637+
assert s.to_python(Model(3, 4), exclude_none=False) == {'width': 3, 'height': 4, 'Area': 12, 'volume': None}
638+
assert s.to_python(Model(3, 4), exclude_none=True) == {'width': 3, 'height': 4, 'Area': 12}
639+
assert s.to_python(Model(3, 4), mode='json', exclude_none=False) == {
640+
'width': 3,
641+
'height': 4,
642+
'Area': 12,
643+
'volume': None,
644+
}
645+
assert s.to_python(Model(3, 4), mode='json', exclude_none=True) == {'width': 3, 'height': 4, 'Area': 12}
646+
647+
608648
@pytest.mark.skipif(cached_property is None, reason='cached_property is not available')
609649
def test_cached_property_alias():
610650
@dataclasses.dataclass

tests/test_hypothesis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_datetime_int(datetime_schema, data):
3636
except OverflowError:
3737
pytest.skip('OverflowError, see pyodide/pyodide#2841, this can happen on 32-bit systems')
3838
else:
39-
assert datetime_schema.validate_python(data) == expected, data
39+
assert datetime_schema.validate_python(data).replace(tzinfo=None) == expected, data
4040

4141

4242
@given(strategies.binary())

0 commit comments

Comments
 (0)