Skip to content

Commit 7f6d069

Browse files
authored
Merge pull request #626 from sdispater/feature/travel
Add time travel for testing
2 parents 2b44bbd + a29852e commit 7f6d069

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+478
-311
lines changed

pendulum/__init__.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,13 @@
2929
from pendulum.formatting import Formatter
3030
from pendulum.helpers import format_diff
3131
from pendulum.helpers import get_locale
32-
from pendulum.helpers import get_test_now
33-
from pendulum.helpers import has_test_now
3432
from pendulum.helpers import locale
3533
from pendulum.helpers import set_locale
36-
from pendulum.helpers import set_test_now
37-
from pendulum.helpers import test
3834
from pendulum.helpers import week_ends_at
3935
from pendulum.helpers import week_starts_at
4036
from pendulum.parser import parse
4137
from pendulum.period import Period
38+
from pendulum.testing.traveller import Traveller
4239
from pendulum.time import Time
4340
from pendulum.tz import UTC
4441
from pendulum.tz import local_timezone
@@ -239,8 +236,7 @@ def from_format(
239236
"""
240237
Creates a DateTime instance from a specific format.
241238
"""
242-
parts = _formatter.parse(string, fmt, now(), locale=locale)
243-
239+
parts = _formatter.parse(string, fmt, now(tz=tz), locale=locale)
244240
if parts["tz"] is None:
245241
parts["tz"] = tz
246242

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

299295

296+
# Testing
297+
298+
_traveller = Traveller(DateTime)
299+
300+
freeze = _traveller.freeze
301+
travel = _traveller.travel
302+
travel_to = _traveller.travel_to
303+
travel_back = _traveller.travel_back
304+
300305
__all__ = [
301306
"__version__",
302307
"DAYS_PER_WEEK",
@@ -324,20 +329,17 @@ def period(start: DateTime, end: DateTime, absolute: bool = False) -> Period:
324329
"datetime",
325330
"duration",
326331
"format_diff",
332+
"freeze",
327333
"from_format",
328334
"from_timestamp",
329335
"get_locale",
330-
"get_test_now",
331-
"has_test_now",
332336
"instance",
333337
"local",
334338
"locale",
335339
"naive",
336340
"now",
337341
"period",
338342
"set_locale",
339-
"set_test_now",
340-
"test",
341343
"week_ends_at",
342344
"week_starts_at",
343345
"parse",
@@ -352,6 +354,9 @@ def period(start: DateTime, end: DateTime, absolute: bool = False) -> Period:
352354
"timezones",
353355
"today",
354356
"tomorrow",
357+
"travel",
358+
"travel_back",
359+
"travel_to",
355360
"FixedTimezone",
356361
"Timezone",
357362
"yesterday",

pendulum/date.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
from pendulum.constants import YEARS_PER_DECADE
2525
from pendulum.exceptions import PendulumException
2626
from pendulum.helpers import add_duration
27-
from pendulum.helpers import get_test_now
28-
from pendulum.helpers import has_test_now
2927
from pendulum.mixins.default import FormattableMixin
3028
from pendulum.period import Period
3129

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

734732
@classmethod
735733
def today(cls) -> Date:
736-
if has_test_now():
737-
return cast(pendulum.DateTime, get_test_now()).date()
738-
739734
dt = date.today()
740735

741736
return cls(dt.year, dt.month, dt.day)

pendulum/datetime.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@
3232
from pendulum.date import Date
3333
from pendulum.exceptions import PendulumException
3434
from pendulum.helpers import add_duration
35-
from pendulum.helpers import get_test_now
36-
from pendulum.helpers import has_test_now
3735
from pendulum.period import Period
3836
from pendulum.time import Time
3937
from pendulum.tz import UTC
@@ -135,15 +133,6 @@ def now(
135133
"""
136134
Get a DateTime instance for the current date and time.
137135
"""
138-
if has_test_now():
139-
test_instance: DateTime = cast(DateTime, get_test_now())
140-
_tz = pendulum._safe_timezone(tz)
141-
142-
if tz is not None and _tz != test_instance.timezone:
143-
test_instance = test_instance.in_tz(_tz)
144-
145-
return test_instance
146-
147136
if tz is None or tz == "local":
148137
dt = datetime.datetime.now(local_timezone())
149138
elif tz is UTC or tz == "UTC":

pendulum/helpers.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
import os
44
import struct
55

6-
from contextlib import contextmanager
76
from datetime import date
87
from datetime import datetime
98
from datetime import timedelta
109
from math import copysign
1110
from typing import TYPE_CHECKING
12-
from typing import Iterator
1311
from typing import TypeVar
1412
from typing import overload
1513

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

180178

181-
@contextmanager
182-
def test(mock: pendulum.DateTime) -> Iterator[None]:
183-
set_test_now(mock)
184-
try:
185-
yield
186-
finally:
187-
set_test_now()
188-
189-
190-
def set_test_now(test_now: pendulum.DateTime | None = None) -> None:
191-
pendulum._TEST_NOW = test_now
192-
193-
194-
def get_test_now() -> pendulum.DateTime | None:
195-
return pendulum._TEST_NOW
196-
197-
198-
def has_test_now() -> bool:
199-
return pendulum._TEST_NOW is not None
200-
201-
202179
def locale(name: str) -> Locale:
203180
return Locale.load(name)
204181

@@ -238,10 +215,6 @@ def week_ends_at(wday: int) -> None:
238215
"week_day",
239216
"add_duration",
240217
"format_diff",
241-
"test",
242-
"set_test_now",
243-
"get_test_now",
244-
"has_test_now",
245218
"locale",
246219
"set_locale",
247220
"get_locale",

pendulum/parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

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

2929
return _parse(text, **options)
3030

pendulum/testing/__init__.py

Whitespace-only changes.

pendulum/testing/traveller.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
from typing import cast
5+
6+
from pendulum.datetime import DateTime
7+
from pendulum.utils._compat import PYPY
8+
9+
if TYPE_CHECKING:
10+
from types import TracebackType
11+
12+
13+
class BaseTraveller:
14+
def __init__(self, datetime_class: type[DateTime] = DateTime) -> None:
15+
self._datetime_class: type[DateTime] = datetime_class
16+
17+
def freeze(self: BaseTraveller) -> BaseTraveller:
18+
raise NotImplementedError()
19+
20+
def travel_back(self: BaseTraveller) -> BaseTraveller:
21+
raise NotImplementedError()
22+
23+
def travel(
24+
self,
25+
years: int = 0,
26+
months: int = 0,
27+
weeks: int = 0,
28+
days: int = 0,
29+
hours: int = 0,
30+
minutes: int = 0,
31+
seconds: int = 0,
32+
microseconds: int = 0,
33+
) -> BaseTraveller:
34+
raise NotImplementedError()
35+
36+
def travel_to(self, dt: DateTime) -> BaseTraveller:
37+
raise NotImplementedError()
38+
39+
40+
if not PYPY:
41+
import time_machine
42+
43+
class Traveller(BaseTraveller):
44+
def __init__(self, datetime_class: type[DateTime] = DateTime) -> None:
45+
super().__init__(datetime_class)
46+
47+
self._started: bool = False
48+
self._traveller: time_machine.travel | None = None
49+
self._coordinates: time_machine.Coordinates | None = None
50+
51+
def freeze(self) -> Traveller:
52+
if self._started:
53+
cast(time_machine.Coordinates, self._coordinates).move_to(
54+
self._datetime_class.now(), tick=False
55+
)
56+
else:
57+
self._start(freeze=True)
58+
59+
return self
60+
61+
def travel_back(self) -> Traveller:
62+
if not self._started:
63+
return self
64+
65+
cast(time_machine.travel, self._traveller).stop()
66+
self._coordinates = None
67+
self._traveller = None
68+
self._started = False
69+
70+
return self
71+
72+
def travel(
73+
self,
74+
years: int = 0,
75+
months: int = 0,
76+
weeks: int = 0,
77+
days: int = 0,
78+
hours: int = 0,
79+
minutes: int = 0,
80+
seconds: int = 0,
81+
microseconds: int = 0,
82+
*,
83+
freeze: bool = False,
84+
) -> Traveller:
85+
self._start(freeze=freeze)
86+
87+
cast(time_machine.Coordinates, self._coordinates).move_to(
88+
self._datetime_class.now().add(
89+
years=years,
90+
months=months,
91+
weeks=weeks,
92+
days=days,
93+
hours=hours,
94+
minutes=minutes,
95+
seconds=seconds,
96+
microseconds=microseconds,
97+
)
98+
)
99+
100+
return self
101+
102+
def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Traveller:
103+
self._start(freeze=freeze)
104+
105+
cast(time_machine.Coordinates, self._coordinates).move_to(dt)
106+
107+
return self
108+
109+
def _start(self, freeze: bool = False) -> None:
110+
if self._started:
111+
return
112+
113+
if not self._traveller:
114+
self._traveller = time_machine.travel(
115+
self._datetime_class.now(), tick=not freeze
116+
)
117+
118+
self._coordinates = self._traveller.start()
119+
120+
self._started = True
121+
122+
def __enter__(self) -> Traveller:
123+
self._start()
124+
125+
return self
126+
127+
def __exit__(
128+
self,
129+
exc_type: type[BaseException] | None,
130+
exc_val: BaseException | None,
131+
exc_tb: TracebackType,
132+
) -> None:
133+
self.travel_back()
134+
135+
else:
136+
137+
class Traveller(BaseTraveller): # type: ignore[no-redef]
138+
139+
...

poetry.lock

Lines changed: 18 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)