Merging upstream version 2.8.2.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
0a2b6bee40
commit
2886109347
7 changed files with 276 additions and 45 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -25,3 +25,4 @@ _build/
|
|||
/.ghtopdep_cache/
|
||||
/worktrees/
|
||||
.ruff_cache/
|
||||
.python-version
|
||||
|
|
15
HISTORY.md
15
HISTORY.md
|
@ -2,6 +2,21 @@
|
|||
|
||||
## Latest Changes
|
||||
|
||||
## 2.8.2
|
||||
|
||||
* 🐛 Preserve timezone information when validating Pendulum DateTimes. [#189](https://github.com/pydantic/pydantic-extra-types/pull/189) by [@chrisguidry
|
||||
](https://github.com/chrisguidry)
|
||||
|
||||
## 2.8.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix Pendulum date time object to have correct typing. [#184](https://github.com/pydantic/pydantic-extra-types/pull/184) by [@07pepa](https://github.com/07pepa)
|
||||
|
||||
### Types
|
||||
|
||||
* ✨ Add parsing of pendulum_dt from unix time and non-strict parsing. [#185](https://github.com/pydantic/pydantic-extra-types/pull/185) by [@07pepa](https://github.com/07pepa)
|
||||
|
||||
## 2.8.0
|
||||
|
||||
### Refactor
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = '2.8.0'
|
||||
__version__ = '2.8.2'
|
||||
|
|
|
@ -3,8 +3,6 @@ Native Pendulum DateTime object implementation. This is a copy of the Pendulum D
|
|||
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
|
||||
|
@ -21,7 +19,17 @@ from pydantic import GetCoreSchemaHandler
|
|||
from pydantic_core import PydanticCustomError, core_schema
|
||||
|
||||
|
||||
class DateTime(_DateTime):
|
||||
class DateTimeSettings(type):
|
||||
def __new__(cls, name, bases, dct, **kwargs): # type: ignore[no-untyped-def]
|
||||
dct['strict'] = kwargs.pop('strict', True)
|
||||
return super().__new__(cls, name, bases, dct)
|
||||
|
||||
def __init__(cls, name, bases, dct, **kwargs): # type: ignore[no-untyped-def]
|
||||
super().__init__(name, bases, dct)
|
||||
cls.strict = kwargs.get('strict', True)
|
||||
|
||||
|
||||
class DateTime(_DateTime, metaclass=DateTimeSettings):
|
||||
"""
|
||||
A `pendulum.DateTime` object. At runtime, this type decomposes into pendulum.DateTime automatically.
|
||||
This type exists because Pydantic throws a fit on unknown types.
|
||||
|
@ -56,7 +64,7 @@ class DateTime(_DateTime):
|
|||
return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.datetime_schema())
|
||||
|
||||
@classmethod
|
||||
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any:
|
||||
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> 'DateTime':
|
||||
"""
|
||||
Validate the datetime object and return it.
|
||||
|
||||
|
@ -68,18 +76,23 @@ class DateTime(_DateTime):
|
|||
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))
|
||||
|
||||
# otherwise, parse it.
|
||||
if isinstance(value, (_DateTime, datetime)):
|
||||
return DateTime.instance(value)
|
||||
try:
|
||||
data = parse(value)
|
||||
except Exception as exc:
|
||||
raise PydanticCustomError('value_error', 'value is not a valid timestamp') from exc
|
||||
return handler(data)
|
||||
# probably the best way to have feature parity with
|
||||
# https://docs.pydantic.dev/latest/api/standard_library_types/#datetimedatetime
|
||||
value = handler(value)
|
||||
return DateTime.instance(value)
|
||||
except ValueError:
|
||||
try:
|
||||
value = parse(value, strict=cls.strict)
|
||||
if isinstance(value, _DateTime):
|
||||
return DateTime.instance(value)
|
||||
raise ValueError(f'value is not a valid datetime it is a {type(value)}')
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise PydanticCustomError('value_error', 'value is not a valid datetime') from exc
|
||||
|
||||
|
||||
class Date(_Date):
|
||||
|
@ -117,7 +130,7 @@ class Date(_Date):
|
|||
return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.date_schema())
|
||||
|
||||
@classmethod
|
||||
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any:
|
||||
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> 'Date':
|
||||
"""
|
||||
Validate the date object and return it.
|
||||
|
||||
|
@ -129,18 +142,17 @@ class Date(_Date):
|
|||
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 isinstance(parsed, (_DateTime, _Date)):
|
||||
return Date(parsed.year, parsed.month, parsed.day)
|
||||
raise ValueError('value is not a valid date it is a {type(parsed)}')
|
||||
except Exception as exc:
|
||||
raise PydanticCustomError('value_error', 'value is not a valid date') from exc
|
||||
return handler(data)
|
||||
|
||||
|
||||
class Duration(_Duration):
|
||||
|
@ -178,7 +190,7 @@ class Duration(_Duration):
|
|||
return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.timedelta_schema())
|
||||
|
||||
@classmethod
|
||||
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any:
|
||||
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> 'Duration':
|
||||
"""
|
||||
Validate the Duration object and return it.
|
||||
|
||||
|
@ -190,15 +202,13 @@ class Duration(_Duration):
|
|||
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)
|
||||
|
|
|
@ -119,6 +119,8 @@ filterwarnings = [
|
|||
'error',
|
||||
# This ignore will be removed when pycountry will drop py36 & support py311
|
||||
'ignore:::pkg_resources',
|
||||
# This ignore will be removed when pendulum fixes https://github.com/sdispater/pendulum/issues/834
|
||||
'ignore:datetime.datetime.utcfromtimestamp.*:DeprecationWarning'
|
||||
]
|
||||
|
||||
# configuring https://github.com/pydantic/hooky
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -3,17 +3,27 @@ from datetime import timezone as tz
|
|||
|
||||
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
|
||||
|
||||
UTC = tz.utc
|
||||
|
||||
DtTypeAdapter = TypeAdapter(datetime)
|
||||
|
||||
|
||||
class DtModel(BaseModel):
|
||||
dt: DateTime
|
||||
|
||||
|
||||
class DateTimeNonStrict(DateTime, strict=False):
|
||||
pass
|
||||
|
||||
|
||||
class DtModelNotStrict(BaseModel):
|
||||
dt: DateTimeNonStrict
|
||||
|
||||
|
||||
class DateModel(BaseModel):
|
||||
d: Date
|
||||
|
||||
|
@ -51,6 +61,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)
|
||||
|
||||
|
@ -75,6 +87,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(
|
||||
|
@ -93,6 +107,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(
|
||||
|
@ -100,7 +116,6 @@ def test_duration_timedelta__existing_instance(instance):
|
|||
[
|
||||
pendulum.now().to_iso8601_string(),
|
||||
pendulum.now().to_w3c_string(),
|
||||
pendulum.now().to_iso8601_string(),
|
||||
],
|
||||
)
|
||||
def test_pendulum_dt_from_serialized(dt):
|
||||
|
@ -110,15 +125,134 @@ 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():
|
||||
@pytest.mark.parametrize(
|
||||
'dt',
|
||||
[
|
||||
pendulum.now().to_iso8601_string(),
|
||||
pendulum.now().to_w3c_string(),
|
||||
],
|
||||
)
|
||||
def test_pendulum_dt_from_serialized_preserves_timezones(dt):
|
||||
"""
|
||||
Verifies that building an instance from serialized, well-formed strings decode
|
||||
properly and preserves the timezone information across all of the Pendulum DateTime
|
||||
properties. Regression test for pydantic/pydantic-extra-types#188.
|
||||
"""
|
||||
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)
|
||||
assert model.dt.tzinfo is not None
|
||||
assert model.dt.tzinfo.utcoffset(model.dt) == dt_actual.tzinfo.utcoffset(dt_actual)
|
||||
assert model.dt.tz is not None
|
||||
assert model.dt.tz.utcoffset(model.dt) == dt_actual.tz.utcoffset(dt_actual)
|
||||
assert model.dt.timezone is not None
|
||||
assert model.dt.timezone.utcoffset(model.dt) == dt_actual.timezone.utcoffset(dt_actual)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'dt',
|
||||
[
|
||||
pendulum.now().to_iso8601_string(),
|
||||
pendulum.now().to_w3c_string(),
|
||||
'Sat Oct 11 17:13:46 UTC 2003', # date util parsing
|
||||
pendulum.now().to_iso8601_string()[:5], # actualy valid or pendulum.parse(dt, strict=False) would fail here
|
||||
],
|
||||
)
|
||||
def test_pendulum_dt_not_strict_from_serialized(dt):
|
||||
"""
|
||||
Verifies that building an instance from serialized, well-formed strings decode properly.
|
||||
"""
|
||||
date_actual = pendulum.parse('2024-03-18').date()
|
||||
model = DateModel(d='2024-03-18')
|
||||
dt_actual = pendulum.parse(dt, strict=False)
|
||||
model = DtModelNotStrict(dt=dt)
|
||||
assert model.dt == dt_actual
|
||||
assert type(model.dt) is DateTime
|
||||
assert isinstance(model.dt, pendulum.DateTime)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'dt',
|
||||
[
|
||||
pendulum.now().to_iso8601_string(),
|
||||
pendulum.now().to_w3c_string(),
|
||||
1718096578,
|
||||
1718096578.5,
|
||||
-5,
|
||||
-5.5,
|
||||
float('-0'),
|
||||
'1718096578',
|
||||
'1718096578.5',
|
||||
'-5',
|
||||
'-5.5',
|
||||
'-0',
|
||||
'-0.0',
|
||||
'+0.0',
|
||||
'+1718096578.5',
|
||||
float('-2e10') - 1.0,
|
||||
float('2e10') + 1.0,
|
||||
-2e10 - 1,
|
||||
2e10 + 1,
|
||||
],
|
||||
)
|
||||
def test_pendulum_dt_from_str_unix_timestamp(dt):
|
||||
"""
|
||||
Verifies that building an instance from serialized, well-formed strings decode properly.
|
||||
"""
|
||||
dt_actual = pendulum.instance(DtTypeAdapter.validate_python(dt))
|
||||
model = DtModel(dt=dt)
|
||||
assert model.dt == dt_actual
|
||||
assert type(model.dt) is DateTime
|
||||
assert isinstance(model.dt, pendulum.DateTime)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'dt',
|
||||
[
|
||||
1718096578,
|
||||
1718096578.5,
|
||||
-5,
|
||||
-5.5,
|
||||
float('-0'),
|
||||
'1718096578',
|
||||
'1718096578.5',
|
||||
'-5',
|
||||
'-5.5',
|
||||
'-0',
|
||||
'-0.0',
|
||||
'+0.0',
|
||||
'+1718096578.5',
|
||||
float('-2e10') - 1.0,
|
||||
float('2e10') + 1.0,
|
||||
-2e10 - 1,
|
||||
2e10 + 1,
|
||||
],
|
||||
)
|
||||
def test_pendulum_dt_from_str_unix_timestamp_is_utc(dt):
|
||||
"""
|
||||
Verifies that without timezone information, it is coerced to UTC. As in pendulum
|
||||
"""
|
||||
model = DtModel(dt=dt)
|
||||
assert model.dt.tzinfo.tzname(model.dt) == 'UTC'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'd',
|
||||
[pendulum.now().date().isoformat(), pendulum.now().to_w3c_string(), pendulum.now().to_iso8601_string()],
|
||||
)
|
||||
def test_pendulum_date_from_serialized(d):
|
||||
"""
|
||||
Verifies that building an instance from serialized, well-formed strings decode properly.
|
||||
"""
|
||||
date_actual = pendulum.parse(d).date()
|
||||
model = DateModel(d=d)
|
||||
assert model.d == date_actual
|
||||
assert type(model.d) is Date
|
||||
assert isinstance(model.d, pendulum.Date)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -138,29 +272,77 @@ 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])
|
||||
def get_invalid_dt_common():
|
||||
return [
|
||||
None,
|
||||
'malformed',
|
||||
'P10Y10M10D',
|
||||
float('inf'),
|
||||
float('-inf'),
|
||||
'inf',
|
||||
'-inf',
|
||||
'INF',
|
||||
'-INF',
|
||||
'+inf',
|
||||
'Infinity',
|
||||
'+Infinity',
|
||||
'-Infinity',
|
||||
'INFINITY',
|
||||
'+INFINITY',
|
||||
'-INFINITY',
|
||||
'infinity',
|
||||
'+infinity',
|
||||
'-infinity',
|
||||
float('nan'),
|
||||
'nan',
|
||||
'NaN',
|
||||
'NAN',
|
||||
'+nan',
|
||||
'-nan',
|
||||
]
|
||||
|
||||
|
||||
dt_strict = get_invalid_dt_common()
|
||||
dt_strict.append(pendulum.now().to_iso8601_string()[:5])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'dt',
|
||||
dt_strict,
|
||||
)
|
||||
def test_pendulum_dt_malformed(dt):
|
||||
"""
|
||||
Verifies that the instance fails to validate if malformed dt are passed.
|
||||
Verifies that the instance fails to validate if malformed dt is passed.
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
DtModel(dt=dt)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('date', [None, 'malformed', pendulum.today().to_iso8601_string()[:5], 42])
|
||||
def test_pendulum_date_malformed(date):
|
||||
@pytest.mark.parametrize('dt', get_invalid_dt_common())
|
||||
def test_pendulum_dt_non_strict_malformed(dt):
|
||||
"""
|
||||
Verifies that the instance fails to validate if malformed dt are passed.
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
DtModelNotStrict(dt=dt)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('invalid_value', [None, 'malformed', pendulum.today().to_iso8601_string()[:5], 'P10Y10M10D'])
|
||||
def test_pendulum_date_malformed(invalid_value):
|
||||
"""
|
||||
Verifies that the instance fails to validate if malformed date are passed.
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
DateModel(d=date)
|
||||
DateModel(d=invalid_value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'delta_t',
|
||||
[None, 'malformed', pendulum.today().to_iso8601_string()[:5], 42, '12m'],
|
||||
[None, 'malformed', pendulum.today().to_iso8601_string()[:5], 42, '12m', '2021-01-01T12:00:00'],
|
||||
)
|
||||
def test_pendulum_duration_malformed(delta_t):
|
||||
"""
|
||||
|
@ -168,3 +350,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)
|
||||
|
|
Loading…
Add table
Reference in a new issue