Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions polyfactory/factories/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
handle_constrained_collection,
handle_constrained_mapping,
)
from polyfactory.value_generators.constrained_dates import handle_constrained_date
from polyfactory.value_generators.constrained_dates import handle_constrained_date, handle_constrained_datetime
from polyfactory.value_generators.constrained_numbers import (
handle_constrained_decimal,
handle_constrained_float,
Expand Down Expand Up @@ -625,7 +625,7 @@ def create_factory(
)

@classmethod
def get_constrained_field_value( # noqa: C901, PLR0911
def get_constrained_field_value( # noqa: C901, PLR0911, PLR0912
cls,
annotation: Any,
field_meta: FieldMeta,
Expand Down Expand Up @@ -706,6 +706,16 @@ def get_constrained_field_value( # noqa: C901, PLR0911
build_context=build_context,
)

if is_safe_subclass(annotation, datetime):
return handle_constrained_datetime(
faker=cls.__faker__,
ge=cast("Any", constraints.get("ge")),
gt=cast("Any", constraints.get("gt")),
le=cast("Any", constraints.get("le")),
lt=cast("Any", constraints.get("lt")),
tz=cast("Any", constraints.get("tz")),
)

if is_safe_subclass(annotation, date):
return handle_constrained_date(
faker=cls.__faker__,
Expand Down
2 changes: 1 addition & 1 deletion polyfactory/field_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class Constraints(TypedDict):
multiple_of: NotRequired[int | float | Decimal]
path_type: NotRequired[Literal["file", "dir", "new"]]
pattern: NotRequired[str | Pattern]
tz: NotRequired[datetime.tzinfo]
tz: NotRequired[datetime.tzinfo | bool]
unique_items: NotRequired[bool]
upper_case: NotRequired[bool]
url: NotRequired[UrlConstraints]
Expand Down
48 changes: 48 additions & 0 deletions polyfactory/value_generators/constrained_dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
from faker import Faker


def _normalize_tz(tz: tzinfo | bool | None) -> tzinfo | None:
if tz is True:
return timezone.utc
if tz is False:
return None
return tz


def handle_constrained_date(
faker: Faker,
ge: date | None = None,
Expand Down Expand Up @@ -39,3 +47,43 @@ def handle_constrained_date(
end_date = lt - timedelta(days=1)

return faker.date_between(start_date=start_date, end_date=end_date)


def handle_constrained_datetime(
faker: Faker,
ge: datetime | None = None,
gt: datetime | None = None,
le: datetime | None = None,
lt: datetime | None = None,
tz: tzinfo | bool | None = None,
) -> datetime:
"""Generates a datetime value fulfilling the expected constraints.

:param faker: An instance of faker.
:param lt: Less than value.
:param le: Less than or equal value.
:param gt: Greater than value.
:param ge: Greater than or equal value.
:param tz: A timezone, or a msgspec-style boolean timezone constraint.

:returns: A datetime instance.
"""
tzinfo = _normalize_tz(tz)

start_datetime = datetime.now(tz=tzinfo) - timedelta(days=100)
if ge:
start_datetime = ge
elif gt:
start_datetime = gt + timedelta(microseconds=1)

end_datetime = datetime.now(tz=tzinfo) + timedelta(days=100)
if le:
end_datetime = le
elif lt:
end_datetime = lt - timedelta(microseconds=1)

return faker.date_time_between_dates(
datetime_start=start_datetime,
datetime_end=end_datetime,
tzinfo=tzinfo,
)
15 changes: 9 additions & 6 deletions tests/test_msgspec_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import pytest
from msgspec import Meta, Struct, structs

from polyfactory.exceptions import ParameterException
from polyfactory.factories.msgspec_factory import MsgspecFactory


Expand Down Expand Up @@ -166,16 +165,20 @@ class FooFactory(MsgspecFactory[Foo]):


@pytest.mark.skipif(sys.version_info < (3, 9), reason="flaky in 3.8")
@pytest.mark.parametrize("t", (dt.datetime, dt.time))
def test_datetime_constraints(t: Union[type[dt.datetime], type[dt.time]]) -> None:
@pytest.mark.parametrize(("tz", "expects_tzinfo"), ((True, True), (False, False)))
def test_datetime_constraints(tz: bool, expects_tzinfo: bool) -> None:
class Foo(Struct):
date_field: Annotated[t, msgspec.Meta(tz=True)] # type: ignore[valid-type]
datetime_field: Annotated[dt.datetime, msgspec.Meta(tz=tz)]

class FooFactory(MsgspecFactory[Foo]):
__model__ = Foo

with pytest.raises(ParameterException):
_ = FooFactory.build()
foo = FooFactory.build()
foo_dict = structs.asdict(foo)

assert (foo.datetime_field.tzinfo is not None) is expects_tzinfo
validated_foo = msgspec.convert(foo_dict, type=Foo)
assert foo == validated_foo


def test_inheritance() -> None:
Expand Down
Loading