mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
feat: add PastDate and FutureDate types (#2425)
* feat: add PastDate and FutureDate types * add changes file * add tests * fix: json schema * fix: black format * less code duplicated * add dates on success.py * fix past and future dates * ♻️ Apply Samuel's comments * 🚨 Satisfy flake8 * 🔥 Remove _DateValueError * test: add last explicit case Co-authored-by: PrettyWood <em.jolibois@gmail.com>
This commit is contained in:
committed by
GitHub
parent
2e2edf4f11
commit
a4cb4ee3bc
@@ -0,0 +1 @@
|
||||
Add `PastDate` and `FutureDate` types.
|
||||
+10
-4
@@ -104,7 +104,7 @@ with custom properties and validation.
|
||||
: see [Typing Iterables](#typing-iterables) below for more detail on parsing and validation
|
||||
|
||||
`subclass of typing.TypedDict`
|
||||
: Same as `dict` but _pydantic_ will validate the dictionary since keys are annotated.
|
||||
: Same as `dict` but _pydantic_ will validate the dictionary since keys are annotated.
|
||||
See [Annotated Types](#annotated-types) below for more detail on parsing and validation
|
||||
|
||||
`typing.Set`
|
||||
@@ -444,6 +444,12 @@ _(This script is complete, it should run "as is")_
|
||||
`DirectoryPath`
|
||||
: like `Path`, but the path must exist and be a directory
|
||||
|
||||
`PastDate`
|
||||
: like `date`, but the date should be in the past
|
||||
|
||||
`FutureDate`
|
||||
: like `date`, but the date should be in the future
|
||||
|
||||
`EmailStr`
|
||||
: requires [email-validator](https://github.com/JoshData/python-email-validator) to be installed;
|
||||
the input string must be a valid email address, and the output is a simple string
|
||||
@@ -829,16 +835,16 @@ The following arguments are available when using the `conbytes` type function
|
||||
|
||||
## Strict Types
|
||||
|
||||
You can use the `StrictStr`, `StrictBytes`, `StrictInt`, `StrictFloat`, and `StrictBool` types
|
||||
You can use the `StrictStr`, `StrictBytes`, `StrictInt`, `StrictFloat`, and `StrictBool` types
|
||||
to prevent coercion from compatible types.
|
||||
These types will only pass validation when the validated value is of the respective type or is a subtype of that type.
|
||||
This behavior is also exposed via the `strict` field of the `ConstrainedStr`, `ConstrainedBytes`,
|
||||
This behavior is also exposed via the `strict` field of the `ConstrainedStr`, `ConstrainedBytes`,
|
||||
`ConstrainedFloat` and `ConstrainedInt` classes and can be combined with a multitude of complex validation rules.
|
||||
|
||||
The following caveats apply:
|
||||
|
||||
- `StrictBytes` (and the `strict` option of `ConstrainedBytes`) will accept both `bytes`,
|
||||
and `bytearray` types.
|
||||
and `bytearray` types.
|
||||
- `StrictInt` (and the `strict` option of `ConstrainedInt`) will not accept `bool` types,
|
||||
even though `bool` is a subclass of `int` in Python. Other subclasses will work.
|
||||
- `StrictFloat` (and the `strict` option of `ConstrainedFloat`) will not accept `int`.
|
||||
|
||||
@@ -108,6 +108,8 @@ __all__ = [
|
||||
'PaymentCardNumber',
|
||||
'PrivateAttr',
|
||||
'ByteSize',
|
||||
'PastDate',
|
||||
'FutureDate',
|
||||
# version
|
||||
'VERSION',
|
||||
]
|
||||
|
||||
@@ -66,6 +66,8 @@ __all__ = (
|
||||
'DecimalWholeDigitsError',
|
||||
'DateTimeError',
|
||||
'DateError',
|
||||
'DateNotInThePastError',
|
||||
'DateNotInTheFutureError',
|
||||
'TimeError',
|
||||
'DurationError',
|
||||
'HashableError',
|
||||
@@ -438,6 +440,16 @@ class DateError(PydanticValueError):
|
||||
msg_template = 'invalid date format'
|
||||
|
||||
|
||||
class DateNotInThePastError(PydanticValueError):
|
||||
code = 'date.not_in_the_past'
|
||||
msg_template = 'date is not in the past'
|
||||
|
||||
|
||||
class DateNotInTheFutureError(PydanticValueError):
|
||||
code = 'date.not_in_the_future'
|
||||
msg_template = 'date is not in the future'
|
||||
|
||||
|
||||
class TimeError(PydanticValueError):
|
||||
msg_template = 'invalid time format'
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import math
|
||||
import re
|
||||
import warnings
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
@@ -26,6 +27,7 @@ from uuid import UUID
|
||||
from weakref import WeakSet
|
||||
|
||||
from . import errors
|
||||
from .datetime_parse import parse_date
|
||||
from .utils import import_string, update_not_none
|
||||
from .validators import (
|
||||
bytes_validator,
|
||||
@@ -93,6 +95,8 @@ __all__ = [
|
||||
'StrictFloat',
|
||||
'PaymentCardNumber',
|
||||
'ByteSize',
|
||||
'PastDate',
|
||||
'FutureDate',
|
||||
]
|
||||
|
||||
NoneStr = Optional[str]
|
||||
@@ -1017,3 +1021,37 @@ class ByteSize(int):
|
||||
raise errors.InvalidByteSizeUnit(unit=unit)
|
||||
|
||||
return self / unit_div
|
||||
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DATE TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
if TYPE_CHECKING:
|
||||
PastDate = date
|
||||
FutureDate = date
|
||||
else:
|
||||
|
||||
class PastDate(date):
|
||||
@classmethod
|
||||
def __get_validators__(cls) -> 'CallableGenerator':
|
||||
yield parse_date
|
||||
yield cls.validate
|
||||
|
||||
@classmethod
|
||||
def validate(cls, value: date) -> date:
|
||||
if value >= date.today():
|
||||
raise errors.DateNotInThePastError()
|
||||
|
||||
return value
|
||||
|
||||
class FutureDate(date):
|
||||
@classmethod
|
||||
def __get_validators__(cls) -> 'CallableGenerator':
|
||||
yield parse_date
|
||||
yield cls.validate
|
||||
|
||||
@classmethod
|
||||
def validate(cls, value: date) -> date:
|
||||
if value <= date.today():
|
||||
raise errors.DateNotInTheFutureError()
|
||||
|
||||
return value
|
||||
|
||||
@@ -5,7 +5,7 @@ Do a little skipping about with types to demonstrate its usage.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Generic, List, Optional, TypeVar
|
||||
from uuid import UUID
|
||||
@@ -15,6 +15,7 @@ from pydantic import (
|
||||
BaseModel,
|
||||
DirectoryPath,
|
||||
FilePath,
|
||||
FutureDate,
|
||||
Json,
|
||||
NegativeFloat,
|
||||
NegativeInt,
|
||||
@@ -23,6 +24,7 @@ from pydantic import (
|
||||
NonNegativeInt,
|
||||
NonPositiveFloat,
|
||||
NonPositiveInt,
|
||||
PastDate,
|
||||
PositiveFloat,
|
||||
PositiveInt,
|
||||
PyObject,
|
||||
@@ -220,6 +222,9 @@ class PydanticTypes(BaseModel):
|
||||
my_dir_path_str: DirectoryPath = '.' # type: ignore
|
||||
# Json
|
||||
my_json: Json = '{"hello": "world"}'
|
||||
# Date
|
||||
my_past_date: PastDate = date.today() - timedelta(1)
|
||||
my_future_date: FutureDate = date.today() + timedelta(1)
|
||||
|
||||
class Config:
|
||||
validate_all = True
|
||||
|
||||
@@ -40,6 +40,7 @@ from pydantic import (
|
||||
EmailStr,
|
||||
Field,
|
||||
FilePath,
|
||||
FutureDate,
|
||||
Json,
|
||||
NameEmail,
|
||||
NegativeFloat,
|
||||
@@ -48,6 +49,7 @@ from pydantic import (
|
||||
NonNegativeInt,
|
||||
NonPositiveFloat,
|
||||
NonPositiveInt,
|
||||
PastDate,
|
||||
PositiveFloat,
|
||||
PositiveInt,
|
||||
PyObject,
|
||||
@@ -2813,3 +2815,82 @@ def test_none(value_type):
|
||||
{'loc': ('my_none_dict', 'a'), 'msg': 'value is not None', 'type': 'type_error.not_none'},
|
||||
{'loc': ('my_json_none',), 'msg': 'value is not None', 'type': 'type_error.not_none'},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value,result',
|
||||
(
|
||||
('1996-01-22', date(1996, 1, 22)),
|
||||
(date(1996, 1, 22), date(1996, 1, 22)),
|
||||
),
|
||||
)
|
||||
def test_past_date_validation_success(value, result):
|
||||
class Model(BaseModel):
|
||||
foo: PastDate
|
||||
|
||||
assert Model(foo=value).foo == result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value',
|
||||
(
|
||||
date.today(),
|
||||
date.today() + timedelta(1),
|
||||
datetime.today(),
|
||||
datetime.today() + timedelta(1),
|
||||
'2064-06-01',
|
||||
),
|
||||
)
|
||||
def test_past_date_validation_fails(value):
|
||||
class Model(BaseModel):
|
||||
foo: PastDate
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Model(foo=value)
|
||||
assert exc_info.value.errors() == [
|
||||
{
|
||||
'loc': ('foo',),
|
||||
'msg': 'date is not in the past',
|
||||
'type': 'value_error.date.not_in_the_past',
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value,result',
|
||||
(
|
||||
(date.today() + timedelta(1), date.today() + timedelta(1)),
|
||||
(datetime.today() + timedelta(1), date.today() + timedelta(1)),
|
||||
('2064-06-01', date(2064, 6, 1)),
|
||||
),
|
||||
)
|
||||
def test_future_date_validation_success(value, result):
|
||||
class Model(BaseModel):
|
||||
foo: FutureDate
|
||||
|
||||
assert Model(foo=value).foo == result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value',
|
||||
(
|
||||
date.today(),
|
||||
date.today() - timedelta(1),
|
||||
datetime.today(),
|
||||
datetime.today() - timedelta(1),
|
||||
'1996-01-22',
|
||||
),
|
||||
)
|
||||
def test_future_date_validation_fails(value):
|
||||
class Model(BaseModel):
|
||||
foo: FutureDate
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Model(foo=value)
|
||||
assert exc_info.value.errors() == [
|
||||
{
|
||||
'loc': ('foo',),
|
||||
'msg': 'date is not in the future',
|
||||
'type': 'value_error.date.not_in_the_future',
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user