Skip to content

Commit 7097d98

Browse files
07pepa07pepa
andauthored
🐛 Fix Pendulum date time object to have correct typing (#184)
* removed handler that vas coercing it to native datetime values * added test to verify correct typing after parsing Co-authored-by: 07pepa <pepe@wont_share.com>
1 parent 2fa55d6 commit 7097d98

File tree

3 files changed

+72
-33
lines changed

3 files changed

+72
-33
lines changed

pydantic_extra_types/pendulum_dt.py

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
CoreSchema implementation. This allows Pydantic to validate the DateTime object.
44
"""
55

6-
import pendulum
7-
86
try:
97
from pendulum import Date as _Date
108
from pendulum import DateTime as _DateTime
@@ -68,18 +66,26 @@ def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler
6866
The validated value or raises a PydanticCustomError.
6967
"""
7068
# if we are passed an existing instance, pass it straight through.
71-
if isinstance(value, _DateTime):
72-
return handler(value)
73-
74-
if isinstance(value, datetime):
75-
return handler(DateTime.instance(value))
69+
if isinstance(value, (_DateTime, datetime)):
70+
return DateTime.instance(value)
7671

7772
# otherwise, parse it.
7873
try:
79-
data = parse(value)
74+
value = parse(value, exact=True)
75+
if not isinstance(value, _DateTime):
76+
raise ValueError(f'value is not a valid datetime it is a {type(value)}')
77+
return DateTime(
78+
value.year,
79+
value.month,
80+
value.day,
81+
value.hour,
82+
value.minute,
83+
value.second,
84+
value.microsecond,
85+
value.tzinfo,
86+
)
8087
except Exception as exc:
8188
raise PydanticCustomError('value_error', 'value is not a valid timestamp') from exc
82-
return handler(data)
8389

8490

8591
class Date(_Date):
@@ -129,18 +135,17 @@ def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler
129135
The validated value or raises a PydanticCustomError.
130136
"""
131137
# if we are passed an existing instance, pass it straight through.
132-
if isinstance(value, _Date):
133-
return handler(value)
134-
135-
if isinstance(value, date):
136-
return handler(pendulum.instance(value))
138+
if isinstance(value, (_Date, date)):
139+
return Date(value.year, value.month, value.day)
137140

138141
# otherwise, parse it.
139142
try:
140-
data = parse(value)
143+
parsed = parse(value)
144+
if isinstance(parsed, (_DateTime, _Date)):
145+
return Date(parsed.year, parsed.month, parsed.day)
146+
raise ValueError('value is not a valid date it is a {type(parsed)}')
141147
except Exception as exc:
142148
raise PydanticCustomError('value_error', 'value is not a valid date') from exc
143-
return handler(data)
144149

145150

146151
class Duration(_Duration):
@@ -190,15 +195,13 @@ def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler
190195
The validated value or raises a PydanticCustomError.
191196
"""
192197
# if we are passed an existing instance, pass it straight through.
193-
if isinstance(value, _Duration):
194-
return handler(value)
198+
if isinstance(value, (_Duration, timedelta)):
199+
return Duration(seconds=value.total_seconds())
195200

196-
if isinstance(value, timedelta):
197-
return handler(_Duration(seconds=value.total_seconds()))
198-
199-
# otherwise, parse it.
200201
try:
201-
data = parse(value)
202+
parsed = parse(value, exact=True)
203+
if not isinstance(parsed, timedelta):
204+
raise ValueError(f'value is not a valid duration it is a {type(parsed)}')
205+
return Duration(seconds=parsed.total_seconds())
202206
except Exception as exc:
203207
raise PydanticCustomError('value_error', 'value is not a valid duration') from exc
204-
return handler(data)

requirements/pyproject.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#
55
# pip-compile --extra=all --no-emit-index-url --output-file=requirements/pyproject.txt pyproject.toml
66
#
7-
annotated-types==0.6.0
7+
annotated-types==0.7.0
88
# via pydantic
99
pendulum==3.0.0
1010
# via pydantic-extra-types (pyproject.toml)

tests/test_pendulum_dt.py

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pendulum
55
import pytest
6-
from pydantic import BaseModel, ValidationError
6+
from pydantic import BaseModel, TypeAdapter, ValidationError
77

88
from pydantic_extra_types.pendulum_dt import Date, DateTime, Duration
99

@@ -51,6 +51,8 @@ def test_existing_instance(instance):
5151
assert dt.minute == instance.minute
5252
assert dt.second == instance.second
5353
assert dt.microsecond == instance.microsecond
54+
assert isinstance(dt, pendulum.DateTime)
55+
assert type(dt) is DateTime
5456
if dt.tzinfo != instance.tzinfo:
5557
assert dt.tzinfo.utcoffset(dt) == instance.tzinfo.utcoffset(instance)
5658

@@ -75,6 +77,8 @@ def test_pendulum_date_existing_instance(instance):
7577
assert d.day == instance.day
7678
assert d.month == instance.month
7779
assert d.year == instance.year
80+
assert isinstance(d, pendulum.Date)
81+
assert type(d) is Date
7882

7983

8084
@pytest.mark.parametrize(
@@ -93,14 +97,15 @@ def test_duration_timedelta__existing_instance(instance):
9397
model = DurationModel(delta_t=instance)
9498

9599
assert model.delta_t.total_seconds() == instance.total_seconds()
100+
assert isinstance(model.delta_t, pendulum.Duration)
101+
assert model.delta_t
96102

97103

98104
@pytest.mark.parametrize(
99105
'dt',
100106
[
101107
pendulum.now().to_iso8601_string(),
102108
pendulum.now().to_w3c_string(),
103-
pendulum.now().to_iso8601_string(),
104109
],
105110
)
106111
def test_pendulum_dt_from_serialized(dt):
@@ -110,15 +115,23 @@ def test_pendulum_dt_from_serialized(dt):
110115
dt_actual = pendulum.parse(dt)
111116
model = DtModel(dt=dt)
112117
assert model.dt == dt_actual
118+
assert type(model.dt) is DateTime
119+
assert isinstance(model.dt, pendulum.DateTime)
113120

114121

115-
def test_pendulum_date_from_serialized():
122+
@pytest.mark.parametrize(
123+
'd',
124+
[pendulum.now().date().isoformat(), pendulum.now().to_w3c_string(), pendulum.now().to_iso8601_string()],
125+
)
126+
def test_pendulum_date_from_serialized(d):
116127
"""
117128
Verifies that building an instance from serialized, well-formed strings decode properly.
118129
"""
119-
date_actual = pendulum.parse('2024-03-18').date()
120-
model = DateModel(d='2024-03-18')
130+
date_actual = pendulum.parse(d).date()
131+
model = DateModel(d=d)
121132
assert model.d == date_actual
133+
assert type(model.d) is Date
134+
assert isinstance(model.d, pendulum.Date)
122135

123136

124137
@pytest.mark.parametrize(
@@ -138,9 +151,11 @@ def test_pendulum_duration_from_serialized(delta_t_str):
138151
true_delta_t = pendulum.parse(delta_t_str)
139152
model = DurationModel(delta_t=delta_t_str)
140153
assert model.delta_t == true_delta_t
154+
assert type(model.delta_t) is Duration
155+
assert isinstance(model.delta_t, pendulum.Duration)
141156

142157

143-
@pytest.mark.parametrize('dt', [None, 'malformed', pendulum.now().to_iso8601_string()[:5], 42])
158+
@pytest.mark.parametrize('dt', [None, 'malformed', pendulum.now().to_iso8601_string()[:5], 42, 'P10Y10M10D'])
144159
def test_pendulum_dt_malformed(dt):
145160
"""
146161
Verifies that the instance fails to validate if malformed dt are passed.
@@ -149,7 +164,7 @@ def test_pendulum_dt_malformed(dt):
149164
DtModel(dt=dt)
150165

151166

152-
@pytest.mark.parametrize('date', [None, 'malformed', pendulum.today().to_iso8601_string()[:5], 42])
167+
@pytest.mark.parametrize('date', [None, 'malformed', pendulum.today().to_iso8601_string()[:5], 42, 'P10Y10M10D'])
153168
def test_pendulum_date_malformed(date):
154169
"""
155170
Verifies that the instance fails to validate if malformed date are passed.
@@ -160,11 +175,32 @@ def test_pendulum_date_malformed(date):
160175

161176
@pytest.mark.parametrize(
162177
'delta_t',
163-
[None, 'malformed', pendulum.today().to_iso8601_string()[:5], 42, '12m'],
178+
[None, 'malformed', pendulum.today().to_iso8601_string()[:5], 42, '12m', '2021-01-01T12:00:00'],
164179
)
165180
def test_pendulum_duration_malformed(delta_t):
166181
"""
167182
Verifies that the instance fails to validate if malformed durations are passed.
168183
"""
169184
with pytest.raises(ValidationError):
170185
DurationModel(delta_t=delta_t)
186+
187+
188+
@pytest.mark.parametrize(
189+
'input_type, value, is_instance',
190+
[
191+
(Date, '2021-01-01', pendulum.Date),
192+
(Date, date(2021, 1, 1), pendulum.Date),
193+
(Date, pendulum.date(2021, 1, 1), pendulum.Date),
194+
(DateTime, '2021-01-01T12:00:00', pendulum.DateTime),
195+
(DateTime, datetime(2021, 1, 1, 12, 0, 0), pendulum.DateTime),
196+
(DateTime, pendulum.datetime(2021, 1, 1, 12, 0, 0), pendulum.DateTime),
197+
(Duration, 'P1DT25H', pendulum.Duration),
198+
(Duration, timedelta(days=1, hours=25), pendulum.Duration),
199+
(Duration, pendulum.duration(days=1, hours=25), pendulum.Duration),
200+
],
201+
)
202+
def test_date_type_adapter(input_type: type, value, is_instance: type):
203+
validated = TypeAdapter(input_type).validate_python(value)
204+
assert type(validated) is input_type
205+
assert isinstance(validated, input_type)
206+
assert isinstance(validated, is_instance)

0 commit comments

Comments
 (0)