Skip to content

Commit fc3544e

Browse files
committed
Add property-based tests for 'false friends'
1 parent 89ff88b commit fc3544e

File tree

1 file changed

+128
-2
lines changed

1 file changed

+128
-2
lines changed

Lib/test/test_zoneinfo/test_zoneinfo_property.py

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
import pickle
55
import unittest
66
import zoneinfo
7-
87
from test.support.hypothesis_helper import hypothesis
9-
108
import test.test_zoneinfo._support as test_support
119

1210
ZoneInfoTestBase = test_support.ZoneInfoTestBase
@@ -61,6 +59,50 @@ def valid_key(key):
6159
return output
6260

6361

62+
def _make_datetime_class(missing):
63+
all_attrs = (
64+
"year",
65+
"month",
66+
"day",
67+
"hour",
68+
"minute",
69+
"second",
70+
"microsecond",
71+
"tzinfo",
72+
"fold",
73+
)
74+
included_attrs = set(all_attrs) - set(missing)
75+
76+
class ClassWithMissing:
77+
def __init__(self, *args, **kwargs):
78+
self._datetime = datetime.datetime(*args, **kwargs)
79+
for attr, arg in zip(all_attrs, args):
80+
if attr in kwargs:
81+
raise ValueError(
82+
f"{attr} specified more than once in argument list"
83+
)
84+
85+
kwargs[attr] = arg
86+
87+
for attr, arg in kwargs.items():
88+
if attr in included_attrs:
89+
setattr(self, attr, arg)
90+
91+
def utcoffset(self):
92+
return self.tzinfo.utcoffset(self)
93+
94+
def dst(self):
95+
return self.tzinfo.dst(self)
96+
97+
def tzname(self):
98+
return self.tzinfo.tzname(self)
99+
100+
def toordinal(self):
101+
return self._datetime.toordinal()
102+
103+
return ClassWithMissing
104+
105+
64106
VALID_KEYS = _valid_keys()
65107
if not VALID_KEYS:
66108
raise unittest.SkipTest("No time zone data available")
@@ -127,6 +169,90 @@ def test_utc(self, dt):
127169
self.assertEqual(dt_zi.dst(), ZERO)
128170
self.assertEqual(dt_zi.tzname(), "UTC")
129171

172+
@hypothesis.given(
173+
dt=hypothesis.strategies.datetimes(),
174+
key=valid_keys(),
175+
missing=hypothesis.strategies.lists(
176+
hypothesis.strategies.sampled_from(
177+
(
178+
"year",
179+
"month",
180+
"day",
181+
"hour",
182+
"minute",
183+
"second",
184+
"microsecond",
185+
"tzinfo",
186+
"fold",
187+
)
188+
),
189+
unique=True,
190+
min_size=1,
191+
),
192+
)
193+
@hypothesis.example(
194+
dt=datetime.datetime(1995, 9, 2, 14, 34, 56),
195+
key="Europe/Berlin",
196+
missing=["fold"],
197+
)
198+
@hypothesis.example(
199+
dt=datetime.datetime(1995, 9, 2, 14, 34, 56),
200+
key="Europe/Berlin",
201+
missing=["year"],
202+
)
203+
@hypothesis.example(
204+
dt=datetime.datetime(1995, 9, 2, 14, 34, 56),
205+
key="Europe/Berlin",
206+
missing=["tzinfo"],
207+
)
208+
@hypothesis.example(
209+
dt=datetime.datetime(1995, 9, 2, 14, 34, 56),
210+
key="Europe/Berlin",
211+
missing=[
212+
"year",
213+
"month",
214+
"day",
215+
"hour",
216+
"minute",
217+
"second",
218+
"microsecond",
219+
"tzinfo",
220+
"fold",
221+
],
222+
)
223+
def test_bad_duck_typed_class(self, dt, key, missing):
224+
# Passing duck typed `datetime`-lookalikes is not actively supported,
225+
# but it also shouldn't raise segfaults.
226+
227+
tzi = self.module.ZoneInfo(key)
228+
DateTimeClass = _make_datetime_class(missing=missing)
229+
false_friend = DateTimeClass(
230+
dt.year,
231+
dt.month,
232+
dt.day,
233+
dt.hour,
234+
dt.minute,
235+
dt.second,
236+
dt.microsecond,
237+
tzinfo=tzi,
238+
fold=dt.fold,
239+
)
240+
241+
try:
242+
false_friend.utcoffset()
243+
except Exception:
244+
pass
245+
246+
try:
247+
false_friend.dst()
248+
except Exception:
249+
pass
250+
251+
try:
252+
false_friend.tzname()
253+
except Exception:
254+
pass
255+
130256

131257
class CZoneInfoTest(ZoneInfoTest):
132258
module = c_zoneinfo

0 commit comments

Comments
 (0)