Skip to content

Commit 5bd0836

Browse files
jbrockmendeljreback
authored andcommitted
implement assert_tzawareness_compat for DatetimeIndex (#18376)
1 parent 07dc117 commit 5bd0836

File tree

7 files changed

+134
-5
lines changed

7 files changed

+134
-5
lines changed

doc/source/whatsnew/v0.23.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ Conversion
342342
- Bug in :class:`Series`` with ``dtype='timedelta64[ns]`` where addition or subtraction of ``TimedeltaIndex`` had results cast to ``dtype='int64'`` (:issue:`17250`)
343343
- Bug in :class:`TimedeltaIndex` where division by a ``Series`` would return a ``TimedeltaIndex`` instead of a ``Series`` (issue:`19042`)
344344
- Bug in :class:`Series` with ``dtype='timedelta64[ns]`` where addition or subtraction of ``TimedeltaIndex`` could return a ``Series`` with an incorrect name (issue:`19043`)
345+
- Fixed bug where comparing :class:`DatetimeIndex` failed to raise ``TypeError`` when attempting to compare timezone-aware and timezone-naive datetimelike objects (:issue:`18162`)
345346
-
346347

347348
Indexing

pandas/core/indexes/datetimes.py

+26-4
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313
_INT64_DTYPE,
1414
_NS_DTYPE,
1515
is_object_dtype,
16-
is_datetime64_dtype,
16+
is_datetime64_dtype, is_datetime64tz_dtype,
1717
is_datetimetz,
1818
is_dtype_equal,
1919
is_timedelta64_dtype,
2020
is_integer,
2121
is_float,
2222
is_integer_dtype,
23-
is_datetime64_ns_dtype,
23+
is_datetime64_ns_dtype, is_datetimelike,
2424
is_period_dtype,
2525
is_bool_dtype,
2626
is_string_like,
@@ -106,8 +106,12 @@ def _dt_index_cmp(opname, cls, nat_result=False):
106106

107107
def wrapper(self, other):
108108
func = getattr(super(DatetimeIndex, self), opname)
109-
if (isinstance(other, datetime) or
110-
isinstance(other, compat.string_types)):
109+
110+
if isinstance(other, (datetime, compat.string_types)):
111+
if isinstance(other, datetime):
112+
# GH#18435 strings get a pass from tzawareness compat
113+
self._assert_tzawareness_compat(other)
114+
111115
other = _to_m8(other, tz=self.tz)
112116
result = func(other)
113117
if isna(other):
@@ -117,6 +121,10 @@ def wrapper(self, other):
117121
other = DatetimeIndex(other)
118122
elif not isinstance(other, (np.ndarray, Index, ABCSeries)):
119123
other = _ensure_datetime64(other)
124+
125+
if is_datetimelike(other):
126+
self._assert_tzawareness_compat(other)
127+
120128
result = func(np.asarray(other))
121129
result = _values_from_object(result)
122130

@@ -652,6 +660,20 @@ def _simple_new(cls, values, name=None, freq=None, tz=None,
652660
result._reset_identity()
653661
return result
654662

663+
def _assert_tzawareness_compat(self, other):
664+
# adapted from _Timestamp._assert_tzawareness_compat
665+
other_tz = getattr(other, 'tzinfo', None)
666+
if is_datetime64tz_dtype(other):
667+
# Get tzinfo from Series dtype
668+
other_tz = other.dtype.tz
669+
if self.tz is None:
670+
if other_tz is not None:
671+
raise TypeError('Cannot compare tz-naive and tz-aware '
672+
'datetime-like objects.')
673+
elif other_tz is None:
674+
raise TypeError('Cannot compare tz-naive and tz-aware '
675+
'datetime-like objects')
676+
655677
@property
656678
def tzinfo(self):
657679
"""

pandas/tests/indexes/datetimes/test_datetime.py

+38
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import operator
2+
13
import pytest
24

35
import numpy as np
@@ -248,6 +250,42 @@ def test_append_join_nondatetimeindex(self):
248250
# it works
249251
rng.join(idx, how='outer')
250252

253+
@pytest.mark.parametrize('op', [operator.eq, operator.ne,
254+
operator.gt, operator.ge,
255+
operator.lt, operator.le])
256+
def test_comparison_tzawareness_compat(self, op):
257+
# GH#18162
258+
dr = pd.date_range('2016-01-01', periods=6)
259+
dz = dr.tz_localize('US/Pacific')
260+
261+
with pytest.raises(TypeError):
262+
op(dr, dz)
263+
with pytest.raises(TypeError):
264+
op(dr, list(dz))
265+
with pytest.raises(TypeError):
266+
op(dz, dr)
267+
with pytest.raises(TypeError):
268+
op(dz, list(dr))
269+
270+
# Check that there isn't a problem aware-aware and naive-naive do not
271+
# raise
272+
assert (dr == dr).all()
273+
assert (dr == list(dr)).all()
274+
assert (dz == dz).all()
275+
assert (dz == list(dz)).all()
276+
277+
# Check comparisons against scalar Timestamps
278+
ts = pd.Timestamp('2000-03-14 01:59')
279+
ts_tz = pd.Timestamp('2000-03-14 01:59', tz='Europe/Amsterdam')
280+
281+
assert (dr > ts).all()
282+
with pytest.raises(TypeError):
283+
op(dr, ts_tz)
284+
285+
assert (dz > ts_tz).all()
286+
with pytest.raises(TypeError):
287+
op(dz, ts)
288+
251289
def test_comparisons_coverage(self):
252290
rng = date_range('1/1/2000', periods=10)
253291

pandas/tests/indexes/test_base.py

+20
Original file line numberDiff line numberDiff line change
@@ -2262,6 +2262,26 @@ def test_intersect_str_dates(self):
22622262

22632263
assert len(res) == 0
22642264

2265+
@pytest.mark.parametrize('op', [operator.eq, operator.ne,
2266+
operator.gt, operator.ge,
2267+
operator.lt, operator.le])
2268+
def test_comparison_tzawareness_compat(self, op):
2269+
# GH#18162
2270+
dr = pd.date_range('2016-01-01', periods=6)
2271+
dz = dr.tz_localize('US/Pacific')
2272+
2273+
# Check that there isn't a problem aware-aware and naive-naive do not
2274+
# raise
2275+
naive_series = Series(dr)
2276+
aware_series = Series(dz)
2277+
with pytest.raises(TypeError):
2278+
op(dz, naive_series)
2279+
with pytest.raises(TypeError):
2280+
op(dr, aware_series)
2281+
2282+
# TODO: implement _assert_tzawareness_compat for the reverse
2283+
# comparison with the Series on the left-hand side
2284+
22652285

22662286
class TestIndexUtils(object):
22672287

pandas/tests/indexing/test_coercion.py

+34
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,9 @@ def test_replace_series(self, how, to_key, from_key):
821821
if (from_key.startswith('datetime') and to_key.startswith('datetime')):
822822
# tested below
823823
return
824+
elif from_key in ['datetime64[ns, US/Eastern]', 'datetime64[ns, UTC]']:
825+
# tested below
826+
return
824827

825828
if how == 'dict':
826829
replacer = dict(zip(self.rep[from_key], self.rep[to_key]))
@@ -849,6 +852,37 @@ def test_replace_series(self, how, to_key, from_key):
849852

850853
tm.assert_series_equal(result, exp)
851854

855+
# TODO(jbrockmendel) commented out to only have a single xfail printed
856+
@pytest.mark.xfail(reason='GH #18376, tzawareness-compat bug '
857+
'in BlockManager.replace_list')
858+
# @pytest.mark.parametrize('how', ['dict', 'series'])
859+
# @pytest.mark.parametrize('to_key', ['timedelta64[ns]', 'bool', 'object',
860+
# 'complex128', 'float64', 'int64'])
861+
# @pytest.mark.parametrize('from_key', ['datetime64[ns, UTC]',
862+
# 'datetime64[ns, US/Eastern]'])
863+
# def test_replace_series_datetime_tz(self, how, to_key, from_key):
864+
def test_replace_series_datetime_tz(self):
865+
how = 'series'
866+
from_key = 'datetime64[ns, US/Eastern]'
867+
to_key = 'timedelta64[ns]'
868+
869+
index = pd.Index([3, 4], name='xxx')
870+
obj = pd.Series(self.rep[from_key], index=index, name='yyy')
871+
assert obj.dtype == from_key
872+
873+
if how == 'dict':
874+
replacer = dict(zip(self.rep[from_key], self.rep[to_key]))
875+
elif how == 'series':
876+
replacer = pd.Series(self.rep[to_key], index=self.rep[from_key])
877+
else:
878+
raise ValueError
879+
880+
result = obj.replace(replacer)
881+
exp = pd.Series(self.rep[to_key], index=index, name='yyy')
882+
assert exp.dtype == to_key
883+
884+
tm.assert_series_equal(result, exp)
885+
852886
# TODO(jreback) commented out to only have a single xfail printed
853887
@pytest.mark.xfail(reason="different tz, "
854888
"currently mask_missing raises SystemError")

pandas/tests/series/test_indexing.py

+14
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,13 @@ def test_getitem_setitem_datetimeindex(self):
450450

451451
lb = "1990-01-01 04:00:00"
452452
rb = "1990-01-01 07:00:00"
453+
# GH#18435 strings get a pass from tzawareness compat
454+
result = ts[(ts.index >= lb) & (ts.index <= rb)]
455+
expected = ts[4:8]
456+
assert_series_equal(result, expected)
457+
458+
lb = "1990-01-01 04:00:00-0500"
459+
rb = "1990-01-01 07:00:00-0500"
453460
result = ts[(ts.index >= lb) & (ts.index <= rb)]
454461
expected = ts[4:8]
455462
assert_series_equal(result, expected)
@@ -475,6 +482,13 @@ def test_getitem_setitem_datetimeindex(self):
475482

476483
lb = datetime(1990, 1, 1, 4)
477484
rb = datetime(1990, 1, 1, 7)
485+
with pytest.raises(TypeError):
486+
# tznaive vs tzaware comparison is invalid
487+
# see GH#18376, GH#18162
488+
ts[(ts.index >= lb) & (ts.index <= rb)]
489+
490+
lb = pd.Timestamp(datetime(1990, 1, 1, 4)).tz_localize(rng.tzinfo)
491+
rb = pd.Timestamp(datetime(1990, 1, 1, 7)).tz_localize(rng.tzinfo)
478492
result = ts[(ts.index >= lb) & (ts.index <= rb)]
479493
expected = ts[4:8]
480494
assert_series_equal(result, expected)

pandas/tests/test_base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def __init__(self, obj):
114114
def setup_method(self, method):
115115
pass
116116

117-
def test_invalida_delgation(self):
117+
def test_invalid_delegation(self):
118118
# these show that in order for the delegation to work
119119
# the _delegate_* methods need to be overridden to not raise
120120
# a TypeError

0 commit comments

Comments
 (0)