Skip to content

Commit 6244f35

Browse files
jbrockmendelJustinZhengBC
authored andcommitted
TST: Tests and Helpers for Datetime/Period Arrays (pandas-dev#23502)
1 parent 2c193a0 commit 6244f35

File tree

12 files changed

+132
-24
lines changed

12 files changed

+132
-24
lines changed

pandas/_libs/tslibs/offsets.pyx

+1
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ class _BaseOffset(object):
346346

347347
def __add__(self, other):
348348
if getattr(other, "_typ", None) in ["datetimeindex", "periodindex",
349+
"datetimearray", "periodarray",
349350
"series", "period", "dataframe"]:
350351
# defer to the other class's implementation
351352
return other + self

pandas/core/arrays/datetimelike.py

+3
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ def astype(self, dtype, copy=True):
200200
# ------------------------------------------------------------------
201201
# Null Handling
202202

203+
def isna(self):
204+
return self._isnan
205+
203206
@property # NB: override with cache_readonly in immutable subclasses
204207
def _isnan(self):
205208
""" return if each value is nan"""

pandas/core/arrays/datetimes.py

+17-5
Original file line numberDiff line numberDiff line change
@@ -117,28 +117,36 @@ def wrapper(self, other):
117117
return ops.invalid_comparison(self, other, op)
118118
else:
119119
if isinstance(other, list):
120-
# FIXME: This can break for object-dtype with mixed types
121-
other = type(self)(other)
122-
elif not isinstance(other, (np.ndarray, ABCIndexClass, ABCSeries)):
120+
try:
121+
other = type(self)(other)
122+
except ValueError:
123+
other = np.array(other, dtype=np.object_)
124+
elif not isinstance(other, (np.ndarray, ABCIndexClass, ABCSeries,
125+
DatetimeArrayMixin)):
123126
# Following Timestamp convention, __eq__ is all-False
124127
# and __ne__ is all True, others raise TypeError.
125128
return ops.invalid_comparison(self, other, op)
126129

127130
if is_object_dtype(other):
128131
result = op(self.astype('O'), np.array(other))
132+
o_mask = isna(other)
129133
elif not (is_datetime64_dtype(other) or
130134
is_datetime64tz_dtype(other)):
131135
# e.g. is_timedelta64_dtype(other)
132136
return ops.invalid_comparison(self, other, op)
133137
else:
134138
self._assert_tzawareness_compat(other)
135-
result = meth(self, np.asarray(other))
139+
if not hasattr(other, 'asi8'):
140+
# ndarray, Series
141+
other = type(self)(other)
142+
result = meth(self, other)
143+
o_mask = other._isnan
136144

137145
result = com.values_from_object(result)
138146

139147
# Make sure to pass an array to result[...]; indexing with
140148
# Series breaks with older version of numpy
141-
o_mask = np.array(isna(other))
149+
o_mask = np.array(o_mask)
142150
if o_mask.any():
143151
result[o_mask] = nat_result
144152

@@ -157,6 +165,7 @@ class DatetimeArrayMixin(dtl.DatetimeLikeArrayMixin):
157165
_freq
158166
_data
159167
"""
168+
_typ = "datetimearray"
160169
_bool_ops = ['is_month_start', 'is_month_end',
161170
'is_quarter_start', 'is_quarter_end', 'is_year_start',
162171
'is_year_end', 'is_leap_year']
@@ -166,6 +175,9 @@ class DatetimeArrayMixin(dtl.DatetimeLikeArrayMixin):
166175
# by returning NotImplemented
167176
timetuple = None
168177

178+
# ensure that operations with numpy arrays defer to our implementation
179+
__array_priority__ = 1000
180+
169181
# -----------------------------------------------------------------
170182
# Constructors
171183

pandas/core/arrays/period.py

-3
Original file line numberDiff line numberDiff line change
@@ -403,9 +403,6 @@ def take(self, indices, allow_fill=False, fill_value=None):
403403

404404
return type(self)(new_values, self.freq)
405405

406-
def isna(self):
407-
return self._data == iNaT
408-
409406
def fillna(self, value=None, method=None, limit=None):
410407
# TODO(#20300)
411408
# To avoid converting to object, we re-implement here with the changes

pandas/core/arrays/timedeltas.py

+2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ def wrapper(self, other):
9898

9999

100100
class TimedeltaArrayMixin(dtl.DatetimeLikeArrayMixin):
101+
_typ = "timedeltaarray"
102+
101103
@property
102104
def _box_func(self):
103105
return lambda x: Timedelta(x, unit='ns')

pandas/core/dtypes/generic.py

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ def _check(cls, inst):
5353
('sparse_array', 'sparse_series'))
5454
ABCCategorical = create_pandas_abc_type("ABCCategorical", "_typ",
5555
("categorical"))
56+
ABCDatetimeArray = create_pandas_abc_type("ABCDatetimeArray", "_typ",
57+
("datetimearray"))
58+
ABCTimedeltaArray = create_pandas_abc_type("ABCTimedeltaArray", "_typ",
59+
("timedeltaarray"))
5660
ABCPeriodArray = create_pandas_abc_type("ABCPeriodArray", "_typ",
5761
("periodarray", ))
5862
ABCPeriod = create_pandas_abc_type("ABCPeriod", "_typ", ("period", ))

pandas/tests/arithmetic/conftest.py

+19
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pandas as pd
66

77
from pandas.compat import long
8+
from pandas.core.arrays import PeriodArray, DatetimeArrayMixin as DatetimeArray
89

910

1011
@pytest.fixture(params=[1, np.array(1, dtype=np.int64)])
@@ -171,3 +172,21 @@ def box_df_broadcast_failure(request):
171172
the DataFrame operation tries to broadcast incorrectly.
172173
"""
173174
return request.param
175+
176+
177+
@pytest.fixture(params=[pd.Index, pd.Series, pd.DataFrame, PeriodArray],
178+
ids=lambda x: x.__name__)
179+
def box_with_period(request):
180+
"""
181+
Like `box`, but specific to PeriodDtype for also testing PeriodArray
182+
"""
183+
return request.param
184+
185+
186+
@pytest.fixture(params=[pd.Index, pd.Series, pd.DataFrame, DatetimeArray],
187+
ids=lambda x: x.__name__)
188+
def box_with_datetime(request):
189+
"""
190+
Like `box`, but specific to datetime64 for also testing DatetimeArray
191+
"""
192+
return request.param

pandas/tests/arithmetic/test_datetime64.py

+11-10
Original file line numberDiff line numberDiff line change
@@ -1037,10 +1037,10 @@ def test_dti_add_sub_float(self, op, other):
10371037
with pytest.raises(TypeError):
10381038
op(dti, other)
10391039

1040-
def test_dti_add_timestamp_raises(self, box):
1040+
def test_dti_add_timestamp_raises(self, box_with_datetime):
10411041
# GH#22163 ensure DataFrame doesn't cast Timestamp to i8
10421042
idx = DatetimeIndex(['2011-01-01', '2011-01-02'])
1043-
idx = tm.box_expected(idx, box)
1043+
idx = tm.box_expected(idx, box_with_datetime)
10441044
msg = "cannot add"
10451045
with tm.assert_raises_regex(TypeError, msg):
10461046
idx + Timestamp('2011-01-01')
@@ -1152,16 +1152,17 @@ def test_dti_add_intarray_no_freq(self, box):
11521152
# -------------------------------------------------------------
11531153
# Binary operations DatetimeIndex and timedelta-like
11541154

1155-
def test_dti_add_timedeltalike(self, tz_naive_fixture, two_hours, box):
1155+
def test_dti_add_timedeltalike(self, tz_naive_fixture, two_hours,
1156+
box_with_datetime):
11561157
# GH#22005, GH#22163 check DataFrame doesn't raise TypeError
11571158
tz = tz_naive_fixture
11581159
rng = pd.date_range('2000-01-01', '2000-02-01', tz=tz)
1159-
rng = tm.box_expected(rng, box)
1160+
rng = tm.box_expected(rng, box_with_datetime)
11601161

11611162
result = rng + two_hours
11621163
expected = pd.date_range('2000-01-01 02:00',
11631164
'2000-02-01 02:00', tz=tz)
1164-
expected = tm.box_expected(expected, box)
1165+
expected = tm.box_expected(expected, box_with_datetime)
11651166
tm.assert_equal(result, expected)
11661167

11671168
def test_dti_iadd_timedeltalike(self, tz_naive_fixture, two_hours):
@@ -1431,13 +1432,13 @@ def test_sub_dti_dti(self):
14311432
tm.assert_index_equal(result, expected)
14321433

14331434
@pytest.mark.parametrize('freq', [None, 'D'])
1434-
def test_sub_period(self, freq, box):
1435+
def test_sub_period(self, freq, box_with_datetime):
14351436
# GH#13078
14361437
# not supported, check TypeError
14371438
p = pd.Period('2011-01-01', freq='D')
14381439

14391440
idx = pd.DatetimeIndex(['2011-01-01', '2011-01-02'], freq=freq)
1440-
idx = tm.box_expected(idx, box)
1441+
idx = tm.box_expected(idx, box_with_datetime)
14411442

14421443
with pytest.raises(TypeError):
14431444
idx - p
@@ -1779,7 +1780,7 @@ def test_dti_with_offset_series(self, tz_naive_fixture, names):
17791780
res3 = dti - other
17801781
tm.assert_series_equal(res3, expected_sub)
17811782

1782-
def test_dti_add_offset_tzaware(self, tz_aware_fixture, box):
1783+
def test_dti_add_offset_tzaware(self, tz_aware_fixture, box_with_datetime):
17831784
# GH#21610, GH#22163 ensure DataFrame doesn't return object-dtype
17841785
timezone = tz_aware_fixture
17851786
if timezone == 'US/Pacific':
@@ -1792,8 +1793,8 @@ def test_dti_add_offset_tzaware(self, tz_aware_fixture, box):
17921793
expected = DatetimeIndex(['2010-11-01 05:00', '2010-11-01 06:00',
17931794
'2010-11-01 07:00'], freq='H', tz=timezone)
17941795

1795-
dates = tm.box_expected(dates, box)
1796-
expected = tm.box_expected(expected, box)
1796+
dates = tm.box_expected(dates, box_with_datetime)
1797+
expected = tm.box_expected(expected, box_with_datetime)
17971798

17981799
# TODO: parametrize over the scalar being added? radd? sub?
17991800
offset = dates + pd.offsets.Hour(5)

pandas/tests/arithmetic/test_period.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -579,15 +579,15 @@ def test_pi_add_offset_n_gt1(self, box):
579579
result = per.freq + pi
580580
tm.assert_equal(result, expected)
581581

582-
def test_pi_add_offset_n_gt1_not_divisible(self, box):
582+
def test_pi_add_offset_n_gt1_not_divisible(self, box_with_period):
583583
# GH#23215
584584
# PeriodIndex with freq.n > 1 add offset with offset.n % freq.n != 0
585585

586586
pi = pd.PeriodIndex(['2016-01'], freq='2M')
587-
pi = tm.box_expected(pi, box)
587+
pi = tm.box_expected(pi, box_with_period)
588588

589589
expected = pd.PeriodIndex(['2016-04'], freq='2M')
590-
expected = tm.box_expected(expected, box)
590+
expected = tm.box_expected(expected, box_with_period)
591591

592592
result = pi + to_offset('3M')
593593
tm.assert_equal(result, expected)
@@ -901,10 +901,10 @@ def test_pi_ops(self):
901901
tm.assert_index_equal(result, exp)
902902

903903
@pytest.mark.parametrize('ng', ["str", 1.5])
904-
def test_pi_ops_errors(self, ng, box):
904+
def test_pi_ops_errors(self, ng, box_with_period):
905905
idx = PeriodIndex(['2011-01', '2011-02', '2011-03', '2011-04'],
906906
freq='M', name='idx')
907-
obj = tm.box_expected(idx, box)
907+
obj = tm.box_expected(idx, box_with_period)
908908

909909
msg = r"unsupported operand type\(s\)"
910910
with tm.assert_raises_regex(TypeError, msg):

pandas/tests/arrays/test_datetimes.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""
2+
Tests for DatetimeArray
3+
"""
4+
import operator
5+
6+
import numpy as np
7+
8+
import pandas as pd
9+
from pandas.core.arrays import DatetimeArrayMixin as DatetimeArray
10+
import pandas.util.testing as tm
11+
12+
13+
class TestDatetimeArrayComparisons(object):
14+
# TODO: merge this into tests/arithmetic/test_datetime64 once it is
15+
# sufficiently robust
16+
17+
def test_cmp_dt64_arraylike_tznaive(self, all_compare_operators):
18+
# arbitrary tz-naive DatetimeIndex
19+
opname = all_compare_operators.strip('_')
20+
op = getattr(operator, opname)
21+
22+
dti = pd.date_range('2016-01-1', freq='MS', periods=9, tz=None)
23+
arr = DatetimeArray(dti)
24+
assert arr.freq == dti.freq
25+
assert arr.tz == dti.tz
26+
27+
right = dti
28+
29+
expected = np.ones(len(arr), dtype=bool)
30+
if opname in ['ne', 'gt', 'lt']:
31+
# for these the comparisons should be all-False
32+
expected = ~expected
33+
34+
result = op(arr, arr)
35+
tm.assert_numpy_array_equal(result, expected)
36+
for other in [right, np.array(right)]:
37+
# TODO: add list and tuple, and object-dtype once those
38+
# are fixed in the constructor
39+
result = op(arr, other)
40+
tm.assert_numpy_array_equal(result, expected)
41+
42+
result = op(other, arr)
43+
tm.assert_numpy_array_equal(result, expected)

pandas/tests/dtypes/test_generic.py

+8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class TestABCClasses(object):
1919
sparse_series = pd.Series([1, 2, 3]).to_sparse()
2020
sparse_array = pd.SparseArray(np.random.randn(10))
2121
sparse_frame = pd.SparseDataFrame({'a': [1, -1, None]})
22+
datetime_array = pd.core.arrays.DatetimeArrayMixin(datetime_index)
23+
timedelta_array = pd.core.arrays.TimedeltaArrayMixin(timedelta_index)
2224

2325
def test_abc_types(self):
2426
assert isinstance(pd.Index(['a', 'b', 'c']), gt.ABCIndex)
@@ -51,6 +53,12 @@ def test_abc_types(self):
5153
assert isinstance(pd.Interval(0, 1.5), gt.ABCInterval)
5254
assert not isinstance(pd.Period('2012', freq='A-DEC'), gt.ABCInterval)
5355

56+
assert isinstance(self.datetime_array, gt.ABCDatetimeArray)
57+
assert not isinstance(self.datetime_index, gt.ABCDatetimeArray)
58+
59+
assert isinstance(self.timedelta_array, gt.ABCTimedeltaArray)
60+
assert not isinstance(self.timedelta_index, gt.ABCTimedeltaArray)
61+
5462

5563
def test_setattr_warnings():
5664
# GH7175 - GOTCHA: You can't use dot notation to add a column...

pandas/util/testing.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@
3434
IntervalIndex, MultiIndex, Panel, PeriodIndex, RangeIndex, Series,
3535
TimedeltaIndex, bdate_range)
3636
from pandas.core.algorithms import take_1d
37-
from pandas.core.arrays import ExtensionArray, IntervalArray, PeriodArray
37+
from pandas.core.arrays import (
38+
DatetimeArrayMixin as DatetimeArray, ExtensionArray, IntervalArray,
39+
PeriodArray, period_array)
3840
import pandas.core.common as com
3941

4042
from pandas.io.common import urlopen
@@ -1049,6 +1051,15 @@ def assert_period_array_equal(left, right, obj='PeriodArray'):
10491051
assert_attr_equal('freq', left, right, obj=obj)
10501052

10511053

1054+
def assert_datetime_array_equal(left, right, obj='DatetimeArray'):
1055+
_check_isinstance(left, right, DatetimeArray)
1056+
1057+
assert_numpy_array_equal(left._data, right._data,
1058+
obj='{obj}._data'.format(obj=obj))
1059+
assert_attr_equal('freq', left, right, obj=obj)
1060+
assert_attr_equal('tz', left, right, obj=obj)
1061+
1062+
10521063
def raise_assert_detail(obj, message, left, right, diff=None):
10531064
__tracebackhide__ = True
10541065

@@ -1546,6 +1557,8 @@ def assert_equal(left, right, **kwargs):
15461557
assert_interval_array_equal(left, right, **kwargs)
15471558
elif isinstance(left, PeriodArray):
15481559
assert_period_array_equal(left, right, **kwargs)
1560+
elif isinstance(left, DatetimeArray):
1561+
assert_datetime_array_equal(left, right, **kwargs)
15491562
elif isinstance(left, ExtensionArray):
15501563
assert_extension_array_equal(left, right, **kwargs)
15511564
elif isinstance(left, np.ndarray):
@@ -1573,6 +1586,11 @@ def box_expected(expected, box_cls):
15731586
expected = pd.Series(expected)
15741587
elif box_cls is pd.DataFrame:
15751588
expected = pd.Series(expected).to_frame()
1589+
elif box_cls is PeriodArray:
1590+
# the PeriodArray constructor is not as flexible as period_array
1591+
expected = period_array(expected)
1592+
elif box_cls is DatetimeArray:
1593+
expected = DatetimeArray(expected)
15761594
elif box_cls is np.ndarray:
15771595
expected = np.array(expected)
15781596
else:

0 commit comments

Comments
 (0)