Skip to content

Commit 912e298

Browse files
Zac-HDjbrockmendel
andcommitted
TST: Add Hypothesis tests for ticks, offsets
These tests are derived from GH18761, by jbrockmendel Co-authored-by: jbrockmendel <[email protected]>
1 parent 8010882 commit 912e298

File tree

2 files changed

+166
-0
lines changed

2 files changed

+166
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Behavioral based tests for offsets and date_range.
4+
5+
This file is adapted from https://github.com/pandas-dev/pandas/pull/18761 -
6+
which was more ambitious but less idiomatic in its use of Hypothesis.
7+
8+
You may wish to consult the previous version for inspiration on further
9+
tests, or when trying to pin down the bugs exposed by the tests below.
10+
"""
11+
12+
import pytest
13+
from hypothesis import given, assume, strategies as st
14+
from hypothesis.extra.pytz import timezones as pytz_timezones
15+
from hypothesis.extra.dateutil import timezones as dateutil_timezones
16+
17+
import pandas as pd
18+
19+
from pandas.tseries.offsets import (
20+
Hour, Minute, Second, Milli, Micro, Nano,
21+
MonthEnd, MonthBegin, BMonthEnd, BMonthBegin,
22+
QuarterEnd, QuarterBegin, BQuarterEnd, BQuarterBegin,
23+
YearEnd, YearBegin, BYearEnd, BYearBegin,
24+
)
25+
26+
27+
tick_classes = [Hour, Minute, Second, Milli, Micro, Nano]
28+
yqm_classes = [MonthBegin, MonthEnd, BMonthBegin, BMonthEnd,
29+
QuarterBegin, QuarterEnd, BQuarterBegin, BQuarterEnd,
30+
YearBegin, YearEnd, BYearBegin, BYearEnd]
31+
32+
# ----------------------------------------------------------------
33+
# Helpers for generating random data
34+
35+
gen_date_range = st.builds(
36+
pd.date_range,
37+
start=st.datetimes(
38+
# TODO: Choose the min/max values more systematically
39+
min_value=pd.Timestamp(1900, 1, 1).to_pydatetime(),
40+
max_value=pd.Timestamp(2100, 1, 1).to_pydatetime()
41+
),
42+
periods=st.integers(min_value=2, max_value=100),
43+
freq=st.sampled_from('Y Q M D H T s ms us ns'.split()),
44+
tz=st.one_of(st.none(), dateutil_timezones(), pytz_timezones()),
45+
)
46+
47+
gen_random_datetime = st.datetimes(
48+
min_value=pd.Timestamp.min.to_pydatetime(),
49+
max_value=pd.Timestamp.max.to_pydatetime(),
50+
timezones=st.one_of(st.none(), dateutil_timezones(), pytz_timezones())
51+
)
52+
53+
# Register the various offset classes so st.from_type can create instances.
54+
# We *could* just append the strategies to a list, but this provides a nice
55+
# demo and enables future tests to use a simple e.g. `from_type(Hour)`.
56+
for cls in tick_classes + [MonthBegin, MonthEnd, BMonthBegin, BMonthEnd]:
57+
st.register_type_strategy(cls, st.builds(
58+
cls,
59+
n=st.integers(-99, 99),
60+
normalize=st.booleans(),
61+
))
62+
63+
for cls in [YearBegin, YearEnd, BYearBegin, BYearEnd]:
64+
st.register_type_strategy(cls, st.builds(
65+
cls,
66+
n=st.integers(-5, 5),
67+
normalize=st.booleans(),
68+
month=st.integers(min_value=1, max_value=12),
69+
))
70+
71+
for cls in [QuarterBegin, QuarterEnd, BQuarterBegin, BQuarterEnd]:
72+
st.register_type_strategy(cls, st.builds(
73+
cls,
74+
n=st.integers(-24, 24),
75+
normalize=st.booleans(),
76+
startingMonth=st.integers(min_value=1, max_value=12)
77+
))
78+
79+
# This strategy can generate any kind of Offset in `tick_classes` or
80+
# `yqm_classes`, with arguments as specified directly above in registration.
81+
gen_yqm_offset = st.one_of([st.from_type(cls) for cls in yqm_classes])
82+
83+
84+
# ----------------------------------------------------------------
85+
# Offset-specific behaviour tests
86+
87+
88+
@pytest.mark.xfail(strict=True)
89+
@given(gen_random_datetime, gen_yqm_offset)
90+
def test_on_offset_implementations(dt, offset):
91+
assume(not offset.normalize)
92+
# check that the class-specific implementations of onOffset match
93+
# the general case definition:
94+
# (dt + offset) - offset == dt
95+
compare = (dt + offset) - offset
96+
assert offset.onOffset(dt) == (compare == dt)
97+
98+
99+
@pytest.mark.xfail(strict=True)
100+
@given(gen_yqm_offset, gen_date_range)
101+
def test_apply_index_implementations(offset, rng):
102+
# offset.apply_index(dti)[i] should match dti[i] + offset
103+
assume(offset.n != 0) # TODO: test for that case separately
104+
105+
# rng = pd.date_range(start='1/1/2000', periods=100000, freq='T')
106+
ser = pd.Series(rng)
107+
108+
res = rng + offset
109+
res_v2 = offset.apply_index(rng)
110+
assert (res == res_v2).all()
111+
112+
assert res[0] == rng[0] + offset
113+
assert res[-1] == rng[-1] + offset
114+
res2 = ser + offset
115+
# apply_index is only for indexes, not series, so no res2_v2
116+
assert res2.iloc[0] == ser.iloc[0] + offset
117+
assert res2.iloc[-1] == ser.iloc[-1] + offset
118+
# TODO: Check randomly assorted entries, not just first/last
119+
120+
121+
@pytest.mark.xfail(strict=True)
122+
@given(gen_yqm_offset)
123+
def test_shift_across_dst(offset):
124+
# GH#18319 check that 1) timezone is correctly normalized and
125+
# 2) that hour is not incorrectly changed by this normalization
126+
# Note that dti includes a transition across DST boundary
127+
dti = pd.date_range(start='2017-10-30 12:00:00', end='2017-11-06',
128+
freq='D', tz='US/Eastern')
129+
assert (dti.hour == 12).all() # we haven't screwed up yet
130+
131+
res = dti + offset
132+
assert (res.hour == 12).all()

pandas/tests/tseries/offsets/test_ticks.py

+34
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import pytest
88
import numpy as np
9+
from hypothesis import given, assume, strategies as st
910

1011
from pandas import Timedelta, Timestamp
1112
from pandas.tseries import offsets
@@ -35,6 +36,39 @@ def test_delta_to_tick():
3536
assert (tick == offsets.Day(3))
3637

3738

39+
@given(cls=st.sampled_from(tick_classes),
40+
n=st.integers(-999, 999),
41+
m=st.integers(-999, 999))
42+
def test_tick_add_sub(cls, n, m):
43+
# For all Tick subclasses and all integers n, m, we should have
44+
# tick(n) + tick(m) == tick(n+m)
45+
# tick(n) - tick(m) == tick(n-m)
46+
left = cls(n)
47+
right = cls(m)
48+
expected = cls(n + m)
49+
50+
assert left + right == expected
51+
assert left.apply(right) == expected
52+
53+
expected = cls(n - m)
54+
assert left - right == expected
55+
56+
57+
@given(cls=st.sampled_from(tick_classes),
58+
n=st.integers(-999, 999), m=st.integers(-999, 999))
59+
def test_tick_equality(cls, n, m):
60+
assume(m != n)
61+
# tick == tock iff tick.n == tock.n
62+
left = cls(n)
63+
right = cls(m)
64+
assert left != right
65+
assert not (left == right)
66+
67+
right = cls(n)
68+
assert left == right
69+
assert not (left != right)
70+
71+
3872
# ---------------------------------------------------------------------
3973

4074

0 commit comments

Comments
 (0)