Skip to content

Commit f6a61db

Browse files
authored
Make the test helpers opt-in via a test extra (#778)
1 parent 482bb80 commit f6a61db

File tree

3 files changed

+117
-91
lines changed

3 files changed

+117
-91
lines changed

poetry.lock

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ version = "3.0.0b1"
44
description = "Python datetimes made easy"
55
readme = "README.rst"
66
requires-python = ">=3.8"
7-
license = {text = "MIT License"}
8-
authors = [{name = "Sébastien Eustace", email="[email protected]>"}]
7+
license = { text = "MIT License" }
8+
authors = [{ name = "Sébastien Eustace", email = "[email protected]>" }]
99
keywords = ['datetime', 'date', 'time']
1010

1111
classifiers = [
@@ -18,7 +18,7 @@ classifiers = [
1818
"Programming Language :: Python :: 3.12",
1919
]
2020

21-
dependencies =[
21+
dependencies = [
2222
"python-dateutil>=2.6",
2323
"tzdata>=2020.1",
2424
'backports.zoneinfo>=0.2.1; python_version < "3.9"',
@@ -49,14 +49,14 @@ keywords = ['datetime', 'date', 'time']
4949
python = ">=3.8"
5050
python-dateutil = ">=2.6"
5151
"backports.zoneinfo" = { version = ">=0.2.1", python = "<3.9" }
52-
time-machine = { version = ">=2.6.0", markers = "implementation_name != 'pypy'" }
52+
time-machine = { version = ">=2.6.0", markers = "implementation_name != 'pypy'", optional = true }
5353
tzdata = ">=2020.1"
5454
importlib-resources = { version = ">=5.9.0", python = "<3.9" }
5555

5656
[tool.poetry.group.test.dependencies]
5757
pytest = "^7.1.2"
5858
pytz = ">=2022.1"
59-
time-machine = "^2.7.1"
59+
time-machine = ">=2.6.0"
6060
pytest-benchmark = "^4.0.0"
6161

6262
[tool.poetry.group.doc.dependencies]
@@ -75,7 +75,7 @@ types-pytz = ">=2022.7.1.2"
7575

7676
[tool.poetry.group.dev.dependencies]
7777
babel = "^2.10.3"
78-
cleo = {version = "^2.0.1", python = ">=3.8,<4.0"}
78+
cleo = { version = "^2.0.1", python = ">=3.8,<4.0" }
7979
tox = "^4.0.0"
8080

8181
[tool.poetry.group.benchmark.dependencies]
@@ -84,6 +84,9 @@ pytest-codspeed = "^1.2.2"
8484
[tool.poetry.group.build.dependencies]
8585
maturin = ">=1.0,<2.0"
8686

87+
[tool.poetry.extras]
88+
test = ["time-machine"]
89+
8790
[tool.maturin]
8891
module-name = "pendulum._pendulum"
8992

@@ -96,18 +99,18 @@ unfixable = [
9699
target-version = "py38"
97100
line-length = 88
98101
extend-select = [
99-
"B", # flake8-bugbear
100-
"C4", # flake8-comprehensions
102+
"B", # flake8-bugbear
103+
"C4", # flake8-comprehensions
101104
"ERA", # flake8-eradicate/eradicate
102-
"I", # isort
103-
"N", # pep8-naming
105+
"I", # isort
106+
"N", # pep8-naming
104107
"PIE", # flake8-pie
105108
"PGH", # pygrep
106109
"RUF", # ruff checks
107110
"SIM", # flake8-simplify
108111
"TCH", # flake8-type-checking
109112
"TID", # flake8-tidy-imports
110-
"UP", # pyupgrade
113+
"UP", # pyupgrade
111114
]
112115
ignore = [
113116
"B904", # use 'raise ... from err'

src/pendulum/testing/traveller.py

Lines changed: 99 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ def __init__(self, datetime_class: type[DateTime] = DateTime) -> None:
1818
self._datetime_class: type[DateTime] = datetime_class
1919

2020
def freeze(self) -> Self:
21-
raise NotImplementedError()
21+
raise self._not_implemented()
2222

2323
def travel_back(self) -> Self:
24-
raise NotImplementedError()
24+
raise self._not_implemented()
2525

2626
def travel(
2727
self,
@@ -34,10 +34,10 @@ def travel(
3434
seconds: int = 0,
3535
microseconds: int = 0,
3636
) -> Self:
37-
raise NotImplementedError()
37+
raise self._not_implemented()
3838

3939
def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Self:
40-
raise NotImplementedError()
40+
raise self._not_implemented()
4141

4242
def __enter__(self) -> Self:
4343
return self
@@ -50,103 +50,123 @@ def __exit__(
5050
) -> None:
5151
...
5252

53+
def _not_implemented(self) -> NotImplementedError:
54+
return NotImplementedError()
55+
5356

5457
if not PYPY:
55-
import time_machine
58+
try:
59+
import time_machine
60+
except ImportError:
61+
time_machine = None # type: ignore[assignment]
5662

57-
class Traveller(BaseTraveller):
58-
def __init__(self, datetime_class: type[DateTime] = DateTime) -> None:
59-
super().__init__(datetime_class)
63+
if time_machine is not None:
6064

61-
self._started: bool = False
62-
self._traveller: time_machine.travel | None = None
63-
self._coordinates: time_machine.Coordinates | None = None
65+
class Traveller(BaseTraveller):
66+
def __init__(self, datetime_class: type[DateTime] = DateTime) -> None:
67+
super().__init__(datetime_class)
6468

65-
def freeze(self) -> Self:
66-
if self._started:
67-
cast(time_machine.Coordinates, self._coordinates).move_to(
68-
self._datetime_class.now(), tick=False
69-
)
70-
else:
71-
self._start(freeze=True)
69+
self._started: bool = False
70+
self._traveller: time_machine.travel | None = None
71+
self._coordinates: time_machine.Coordinates | None = None
7272

73-
return self
73+
def freeze(self) -> Self:
74+
if self._started:
75+
cast(time_machine.Coordinates, self._coordinates).move_to(
76+
self._datetime_class.now(), tick=False
77+
)
78+
else:
79+
self._start(freeze=True)
7480

75-
def travel_back(self) -> Self:
76-
if not self._started:
7781
return self
7882

79-
cast(time_machine.travel, self._traveller).stop()
80-
self._coordinates = None
81-
self._traveller = None
82-
self._started = False
83-
84-
return self
85-
86-
def travel(
87-
self,
88-
years: int = 0,
89-
months: int = 0,
90-
weeks: int = 0,
91-
days: int = 0,
92-
hours: int = 0,
93-
minutes: int = 0,
94-
seconds: int = 0,
95-
microseconds: int = 0,
96-
*,
97-
freeze: bool = False,
98-
) -> Self:
99-
self._start(freeze=freeze)
100-
101-
cast(time_machine.Coordinates, self._coordinates).move_to(
102-
self._datetime_class.now().add(
103-
years=years,
104-
months=months,
105-
weeks=weeks,
106-
days=days,
107-
hours=hours,
108-
minutes=minutes,
109-
seconds=seconds,
110-
microseconds=microseconds,
83+
def travel_back(self) -> Self:
84+
if not self._started:
85+
return self
86+
87+
cast(time_machine.travel, self._traveller).stop()
88+
self._coordinates = None
89+
self._traveller = None
90+
self._started = False
91+
92+
return self
93+
94+
def travel(
95+
self,
96+
years: int = 0,
97+
months: int = 0,
98+
weeks: int = 0,
99+
days: int = 0,
100+
hours: int = 0,
101+
minutes: int = 0,
102+
seconds: int = 0,
103+
microseconds: int = 0,
104+
*,
105+
freeze: bool = False,
106+
) -> Self:
107+
self._start(freeze=freeze)
108+
109+
cast(time_machine.Coordinates, self._coordinates).move_to(
110+
self._datetime_class.now().add(
111+
years=years,
112+
months=months,
113+
weeks=weeks,
114+
days=days,
115+
hours=hours,
116+
minutes=minutes,
117+
seconds=seconds,
118+
microseconds=microseconds,
119+
)
111120
)
112-
)
113121

114-
return self
122+
return self
115123

116-
def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Self:
117-
self._start(freeze=freeze)
124+
def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Self:
125+
self._start(freeze=freeze)
118126

119-
cast(time_machine.Coordinates, self._coordinates).move_to(dt)
127+
cast(time_machine.Coordinates, self._coordinates).move_to(dt)
120128

121-
return self
129+
return self
122130

123-
def _start(self, freeze: bool = False) -> None:
124-
if self._started:
125-
return
131+
def _start(self, freeze: bool = False) -> None:
132+
if self._started:
133+
return
126134

127-
if not self._traveller:
128-
self._traveller = time_machine.travel(
129-
self._datetime_class.now(), tick=not freeze
130-
)
135+
if not self._traveller:
136+
self._traveller = time_machine.travel(
137+
self._datetime_class.now(), tick=not freeze
138+
)
131139

132-
self._coordinates = self._traveller.start()
140+
self._coordinates = self._traveller.start()
133141

134-
self._started = True
142+
self._started = True
135143

136-
def __enter__(self) -> Self:
137-
self._start()
144+
def __enter__(self) -> Self:
145+
self._start()
138146

139-
return self
147+
return self
140148

141-
def __exit__(
142-
self,
143-
exc_type: type[BaseException] | None,
144-
exc_val: BaseException | None,
145-
exc_tb: TracebackType,
146-
) -> None:
147-
self.travel_back()
149+
def __exit__(
150+
self,
151+
exc_type: type[BaseException] | None,
152+
exc_val: BaseException | None,
153+
exc_tb: TracebackType,
154+
) -> None:
155+
self.travel_back()
156+
157+
else:
158+
159+
class Traveller(BaseTraveller): # type: ignore[no-redef]
160+
def _not_implemented(self) -> NotImplementedError:
161+
return NotImplementedError(
162+
"Time travelling is an optional feature. "
163+
'You can add it by installing Pendulum with the "test" extra.'
164+
)
148165

149166
else:
150167

151168
class Traveller(BaseTraveller): # type: ignore[no-redef]
152-
...
169+
def _not_implemented(self) -> NotImplementedError:
170+
return NotImplementedError(
171+
"Time travelling is not supported on the PyPy Python implementation."
172+
)

0 commit comments

Comments
 (0)