Skip to content

Commit

Permalink
Fix a Pendulum date time object to have correct typing
Browse files Browse the repository at this point in the history
* removed handler that vas coercing it to native datetime values
* added test to verify correct typing after parsing
  • Loading branch information
07pepa committed Jun 10, 2024
1 parent 2fa55d6 commit 7434305
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 26 deletions.
53 changes: 29 additions & 24 deletions pydantic_extra_types/pendulum_dt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
CoreSchema implementation. This allows Pydantic to validate the DateTime object.
"""

import pendulum

try:
from pendulum import Date as _Date
from pendulum import DateTime as _DateTime
Expand Down Expand Up @@ -68,18 +66,26 @@ def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler
The validated value or raises a PydanticCustomError.
"""
# if we are passed an existing instance, pass it straight through.
if isinstance(value, _DateTime):
return handler(value)

if isinstance(value, datetime):
return handler(DateTime.instance(value))
if isinstance(value, (_DateTime, datetime)):
return DateTime.instance(value)

# otherwise, parse it.
try:
data = parse(value)
value = parse(value, exact=True)
if not isinstance(value, _DateTime):
raise ValueError(f'value is not a valid datetime it is a {type(value)}')
return DateTime(
value.year,
value.month,
value.day,
value.hour,
value.minute,
value.second,
value.microsecond,
value.tzinfo,
)
except Exception as exc:
raise PydanticCustomError('value_error', 'value is not a valid timestamp') from exc
return handler(data)


class Date(_Date):
Expand Down Expand Up @@ -129,18 +135,19 @@ def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler
The validated value or raises a PydanticCustomError.
"""
# if we are passed an existing instance, pass it straight through.
if isinstance(value, _Date):
return handler(value)

if isinstance(value, date):
return handler(pendulum.instance(value))
if isinstance(value, (_Date, date)):
return Date(value.year, value.month, value.day)

# otherwise, parse it.
try:
data = parse(value)
parsed = parse(value)
if not isinstance(parsed, _Date):
if isinstance(parsed, DateTime):
return Date(parsed.year, parsed.month, parsed.day)
raise ValueError('value is not a valid date it is a {type(parsed)}')
return Date(parsed.year, parsed.month, parsed.day)
except Exception as exc:
raise PydanticCustomError('value_error', 'value is not a valid date') from exc
return handler(data)


class Duration(_Duration):
Expand Down Expand Up @@ -190,15 +197,13 @@ def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler
The validated value or raises a PydanticCustomError.
"""
# if we are passed an existing instance, pass it straight through.
if isinstance(value, _Duration):
return handler(value)
if isinstance(value, (_Duration, timedelta)):
return Duration(seconds=value.total_seconds())

if isinstance(value, timedelta):
return handler(_Duration(seconds=value.total_seconds()))

# otherwise, parse it.
try:
data = parse(value)
parsed = parse(value, exact=True)
if not isinstance(parsed, timedelta):
raise ValueError(f'value is not a valid duration it is a {type(parsed)}')
return Duration(seconds=parsed.total_seconds())
except Exception as exc:
raise PydanticCustomError('value_error', 'value is not a valid duration') from exc
return handler(data)
2 changes: 1 addition & 1 deletion requirements/pyproject.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#
# pip-compile --extra=all --no-emit-index-url --output-file=requirements/pyproject.txt pyproject.toml
#
annotated-types==0.6.0
annotated-types==0.7.0
# via pydantic
pendulum==3.0.0
# via pydantic-extra-types (pyproject.toml)
Expand Down
35 changes: 34 additions & 1 deletion tests/test_pendulum_dt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pendulum
import pytest
from pydantic import BaseModel, ValidationError
from pydantic import BaseModel, TypeAdapter, ValidationError

from pydantic_extra_types.pendulum_dt import Date, DateTime, Duration

Expand Down Expand Up @@ -51,6 +51,8 @@ def test_existing_instance(instance):
assert dt.minute == instance.minute
assert dt.second == instance.second
assert dt.microsecond == instance.microsecond
assert isinstance(dt, pendulum.DateTime)
assert type(dt) is DateTime
if dt.tzinfo != instance.tzinfo:
assert dt.tzinfo.utcoffset(dt) == instance.tzinfo.utcoffset(instance)

Expand All @@ -75,6 +77,8 @@ def test_pendulum_date_existing_instance(instance):
assert d.day == instance.day
assert d.month == instance.month
assert d.year == instance.year
assert isinstance(d, pendulum.Date)
assert type(d) is Date


@pytest.mark.parametrize(
Expand All @@ -93,6 +97,8 @@ def test_duration_timedelta__existing_instance(instance):
model = DurationModel(delta_t=instance)

assert model.delta_t.total_seconds() == instance.total_seconds()
assert isinstance(model.delta_t, pendulum.Duration)
assert model.delta_t


@pytest.mark.parametrize(
Expand All @@ -110,6 +116,8 @@ def test_pendulum_dt_from_serialized(dt):
dt_actual = pendulum.parse(dt)
model = DtModel(dt=dt)
assert model.dt == dt_actual
assert type(model.dt) is DateTime
assert isinstance(model.dt, pendulum.DateTime)


def test_pendulum_date_from_serialized():
Expand All @@ -119,6 +127,8 @@ def test_pendulum_date_from_serialized():
date_actual = pendulum.parse('2024-03-18').date()
model = DateModel(d='2024-03-18')
assert model.d == date_actual
assert type(model.d) is Date
assert isinstance(model.d, pendulum.Date)


@pytest.mark.parametrize(
Expand All @@ -138,6 +148,8 @@ def test_pendulum_duration_from_serialized(delta_t_str):
true_delta_t = pendulum.parse(delta_t_str)
model = DurationModel(delta_t=delta_t_str)
assert model.delta_t == true_delta_t
assert type(model.delta_t) is Duration
assert isinstance(model.delta_t, pendulum.Duration)


@pytest.mark.parametrize('dt', [None, 'malformed', pendulum.now().to_iso8601_string()[:5], 42])
Expand Down Expand Up @@ -168,3 +180,24 @@ def test_pendulum_duration_malformed(delta_t):
"""
with pytest.raises(ValidationError):
DurationModel(delta_t=delta_t)


@pytest.mark.parametrize(
'input_type, value, is_instance',
[
(Date, '2021-01-01', pendulum.Date),
(Date, date(2021, 1, 1), pendulum.Date),
(Date, pendulum.date(2021, 1, 1), pendulum.Date),
(DateTime, '2021-01-01T12:00:00', pendulum.DateTime),
(DateTime, datetime(2021, 1, 1, 12, 0, 0), pendulum.DateTime),
(DateTime, pendulum.datetime(2021, 1, 1, 12, 0, 0), pendulum.DateTime),
(Duration, 'P1DT25H', pendulum.Duration),
(Duration, timedelta(days=1, hours=25), pendulum.Duration),
(Duration, pendulum.duration(days=1, hours=25), pendulum.Duration),
],
)
def test_date_type_adapter(input_type: type, value, is_instance: type):
validated = TypeAdapter(input_type).validate_python(value)
assert type(validated) is input_type
assert isinstance(validated, input_type)
assert isinstance(validated, is_instance)

0 comments on commit 7434305

Please sign in to comment.