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:
Marcelo Trylesinski
2021-06-04 23:30:25 +02:00
committed by GitHub
parent 2e2edf4f11
commit a4cb4ee3bc
7 changed files with 150 additions and 5 deletions
+1
View File
@@ -0,0 +1 @@
Add `PastDate` and `FutureDate` types.
+10 -4
View File
@@ -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`.
+2
View File
@@ -108,6 +108,8 @@ __all__ = [
'PaymentCardNumber',
'PrivateAttr',
'ByteSize',
'PastDate',
'FutureDate',
# version
'VERSION',
]
+12
View File
@@ -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'
+38
View File
@@ -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
+6 -1
View File
@@ -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
+81
View File
@@ -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',
}
]