Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add time travel for testing #626

Merged
merged 2 commits into from
Sep 21, 2022
Merged
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
25 changes: 15 additions & 10 deletions pendulum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,13 @@
from pendulum.formatting import Formatter
from pendulum.helpers import format_diff
from pendulum.helpers import get_locale
from pendulum.helpers import get_test_now
from pendulum.helpers import has_test_now
from pendulum.helpers import locale
from pendulum.helpers import set_locale
from pendulum.helpers import set_test_now
from pendulum.helpers import test
from pendulum.helpers import week_ends_at
from pendulum.helpers import week_starts_at
from pendulum.parser import parse
from pendulum.period import Period
from pendulum.testing.traveller import Traveller
from pendulum.time import Time
from pendulum.tz import UTC
from pendulum.tz import local_timezone
Expand Down Expand Up @@ -239,8 +236,7 @@ def from_format(
"""
Creates a DateTime instance from a specific format.
"""
parts = _formatter.parse(string, fmt, now(), locale=locale)

parts = _formatter.parse(string, fmt, now(tz=tz), locale=locale)
if parts["tz"] is None:
parts["tz"] = tz

Expand Down Expand Up @@ -297,6 +293,15 @@ def period(start: DateTime, end: DateTime, absolute: bool = False) -> Period:
return Period(start, end, absolute=absolute)


# Testing

_traveller = Traveller(DateTime)

freeze = _traveller.freeze
travel = _traveller.travel
travel_to = _traveller.travel_to
travel_back = _traveller.travel_back

__all__ = [
"__version__",
"DAYS_PER_WEEK",
Expand Down Expand Up @@ -324,20 +329,17 @@ def period(start: DateTime, end: DateTime, absolute: bool = False) -> Period:
"datetime",
"duration",
"format_diff",
"freeze",
"from_format",
"from_timestamp",
"get_locale",
"get_test_now",
"has_test_now",
"instance",
"local",
"locale",
"naive",
"now",
"period",
"set_locale",
"set_test_now",
"test",
"week_ends_at",
"week_starts_at",
"parse",
Expand All @@ -352,6 +354,9 @@ def period(start: DateTime, end: DateTime, absolute: bool = False) -> Period:
"timezones",
"today",
"tomorrow",
"travel",
"travel_back",
"travel_to",
"FixedTimezone",
"Timezone",
"yesterday",
Expand Down
5 changes: 0 additions & 5 deletions pendulum/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
from pendulum.constants import YEARS_PER_DECADE
from pendulum.exceptions import PendulumException
from pendulum.helpers import add_duration
from pendulum.helpers import get_test_now
from pendulum.helpers import has_test_now
from pendulum.mixins.default import FormattableMixin
from pendulum.period import Period

Expand Down Expand Up @@ -733,9 +731,6 @@ def average(self, dt: date | None = None) -> Date:

@classmethod
def today(cls) -> Date:
if has_test_now():
return cast(pendulum.DateTime, get_test_now()).date()

dt = date.today()

return cls(dt.year, dt.month, dt.day)
Expand Down
11 changes: 0 additions & 11 deletions pendulum/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@
from pendulum.date import Date
from pendulum.exceptions import PendulumException
from pendulum.helpers import add_duration
from pendulum.helpers import get_test_now
from pendulum.helpers import has_test_now
from pendulum.period import Period
from pendulum.time import Time
from pendulum.tz import UTC
Expand Down Expand Up @@ -135,15 +133,6 @@ def now(
"""
Get a DateTime instance for the current date and time.
"""
if has_test_now():
test_instance: DateTime = cast(DateTime, get_test_now())
_tz = pendulum._safe_timezone(tz)

if tz is not None and _tz != test_instance.timezone:
test_instance = test_instance.in_tz(_tz)

return test_instance

if tz is None or tz == "local":
dt = datetime.datetime.now(local_timezone())
elif tz is UTC or tz == "UTC":
Expand Down
27 changes: 0 additions & 27 deletions pendulum/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
import os
import struct

from contextlib import contextmanager
from datetime import date
from datetime import datetime
from datetime import timedelta
from math import copysign
from typing import TYPE_CHECKING
from typing import Iterator
from typing import TypeVar
from typing import overload

Expand Down Expand Up @@ -178,27 +176,6 @@ def _sign(x: float) -> int:
# Global helpers


@contextmanager
def test(mock: pendulum.DateTime) -> Iterator[None]:
set_test_now(mock)
try:
yield
finally:
set_test_now()


def set_test_now(test_now: pendulum.DateTime | None = None) -> None:
pendulum._TEST_NOW = test_now


def get_test_now() -> pendulum.DateTime | None:
return pendulum._TEST_NOW


def has_test_now() -> bool:
return pendulum._TEST_NOW is not None


def locale(name: str) -> Locale:
return Locale.load(name)

Expand Down Expand Up @@ -238,10 +215,6 @@ def week_ends_at(wday: int) -> None:
"week_day",
"add_duration",
"format_diff",
"test",
"set_test_now",
"get_test_now",
"has_test_now",
"locale",
"set_locale",
"get_locale",
Expand Down
2 changes: 1 addition & 1 deletion pendulum/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration:
# Use the mock now value if it exists
options["now"] = options.get("now", pendulum.get_test_now())
options["now"] = options.get("now")

return _parse(text, **options)

Expand Down
Empty file added pendulum/testing/__init__.py
Empty file.
139 changes: 139 additions & 0 deletions pendulum/testing/traveller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from typing import cast

from pendulum.datetime import DateTime
from pendulum.utils._compat import PYPY

if TYPE_CHECKING:
from types import TracebackType


class BaseTraveller:
def __init__(self, datetime_class: type[DateTime] = DateTime) -> None:
self._datetime_class: type[DateTime] = datetime_class

def freeze(self: BaseTraveller) -> BaseTraveller:
raise NotImplementedError()

def travel_back(self: BaseTraveller) -> BaseTraveller:
raise NotImplementedError()

def travel(
self,
years: int = 0,
months: int = 0,
weeks: int = 0,
days: int = 0,
hours: int = 0,
minutes: int = 0,
seconds: int = 0,
microseconds: int = 0,
) -> BaseTraveller:
raise NotImplementedError()

def travel_to(self, dt: DateTime) -> BaseTraveller:
raise NotImplementedError()


if not PYPY:
import time_machine

class Traveller(BaseTraveller):
def __init__(self, datetime_class: type[DateTime] = DateTime) -> None:
super().__init__(datetime_class)

self._started: bool = False
self._traveller: time_machine.travel | None = None
self._coordinates: time_machine.Coordinates | None = None

def freeze(self) -> Traveller:
if self._started:
cast(time_machine.Coordinates, self._coordinates).move_to(
self._datetime_class.now(), tick=False
)
else:
self._start(freeze=True)

return self

def travel_back(self) -> Traveller:
if not self._started:
return self

cast(time_machine.travel, self._traveller).stop()
self._coordinates = None
self._traveller = None
self._started = False

return self

def travel(
self,
years: int = 0,
months: int = 0,
weeks: int = 0,
days: int = 0,
hours: int = 0,
minutes: int = 0,
seconds: int = 0,
microseconds: int = 0,
*,
freeze: bool = False,
) -> Traveller:
self._start(freeze=freeze)

cast(time_machine.Coordinates, self._coordinates).move_to(
self._datetime_class.now().add(
years=years,
months=months,
weeks=weeks,
days=days,
hours=hours,
minutes=minutes,
seconds=seconds,
microseconds=microseconds,
)
)

return self

def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Traveller:
self._start(freeze=freeze)

cast(time_machine.Coordinates, self._coordinates).move_to(dt)

return self

def _start(self, freeze: bool = False) -> None:
if self._started:
return

if not self._traveller:
self._traveller = time_machine.travel(
self._datetime_class.now(), tick=not freeze
)

self._coordinates = self._traveller.start()

self._started = True

def __enter__(self) -> Traveller:
self._start()

return self

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType,
) -> None:
self.travel_back()

else:

class Traveller(BaseTraveller): # type: ignore[no-redef]

...
27 changes: 18 additions & 9 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading