Repository: pandas-dev/pandas
Pull request: BUG: Assorted DTA/TDA/PI bugfixes
Fixes a problem with tz-aware DatetimeArray.round (ref #24038)
Fixes PI, PA, DTA failing to validate lengths when doing comparisons against length-1 instances (ref #23078)
Pins names to a handful of methods that currently don't have them (working on a separate branch that checks these systematically; these are the low-hanging fruit)

- [x] closes #23078
- [x] tests added / passed
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [ ] whatsnew entry


Problem statement:
comparison between two PeriodIndexes doesn't validate length
```python
In [5]: idx = pd.period_range('2017', periods=12, freq="A-DEC")

In [6]: idx <= idx[[0]]
Out[6]:
array([ True, False, False, False, False, False, False, False, False,
       False, False, False])

```

This should raise.

Both `DatetimeIndex` and `TimedeltaIndex` do raise

```python
In [8]: idx <= idx[[0]]
```

```pytb
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-8-8a2a887e6efd> in <module>
----> 1 idx <= idx[[0]]

~/sandbox/pandas/pandas/core/indexes/datetimes.py in wrapper(self, other)
     90
     91     def wrapper(self, other):
---> 92         result = getattr(DatetimeArrayMixin, opname)(self, other)
     93         if is_bool_dtype(result):
     94             return result

~/sandbox/pandas/pandas/core/arrays/datetimes.py in wrapper(self, other)
    133             else:
    134                 self._assert_tzawareness_compat(other)
--> 135                 result = meth(self, np.asarray(other))
    136
    137             result = com.values_from_object(result)

~/sandbox/pandas/pandas/core/arrays/datetimelike.py in cmp_method(self, other)
     51         if isinstance(other, (np.ndarray, ABCIndexClass, ABCSeries)):
     52             if other.ndim > 0 and len(self) != len(other):
---> 53                 raise ValueError('Lengths must match to compare')
     54
     55         if needs_i8_conversion(self) and needs_i8_conversion(other):

ValueError: Lengths must match to compare

```

Hints from issue discussion:
cc @jbrockmendel 


I think we had this before on other data structures (Index, Series ?) as well, at least I seem to remember something about it (long time ago, PeriodIndex might be an oversight). Only thing I can quickly find is https://github.com/pandas-dev/pandas/issues/12346
Should be fixed in the same pass as #21793.  Possibly also #19804, #19088, #23063.
So we indeed fixed this at a certain point of indexes in general: pandas.pydata.org/pandas-docs/stable/whatsnew.html#changes-to-index-comparisons

Commit message: py2/py3 compat test fixes

Patch to review:
diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py
index c01b04991e52b..866dc348b44f9 100644
--- a/pandas/core/arrays/datetimelike.py
+++ b/pandas/core/arrays/datetimelike.py
@@ -1263,7 +1263,8 @@ def _ensure_datetimelike_to_i8(other, to_utc=False):
 
     if lib.is_scalar(other) and isna(other):
         return iNaT
-    elif isinstance(other, (PeriodArray, ABCIndexClass)):
+    elif isinstance(other, (PeriodArray, ABCIndexClass,
+                            DatetimeLikeArrayMixin)):
         # convert tz if needed
         if getattr(other, 'tz', None) is not None:
             if to_utc:
diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py
index c197d6d6e634b..2ba3639e46f66 100644
--- a/pandas/core/arrays/datetimes.py
+++ b/pandas/core/arrays/datetimes.py
@@ -96,6 +96,8 @@ def _dt_array_cmp(cls, op):
     def wrapper(self, other):
         meth = getattr(dtl.DatetimeLikeArrayMixin, opname)
 
+        other = lib.item_from_zerodim(other)
+
         if isinstance(other, (datetime, np.datetime64, compat.string_types)):
             if isinstance(other, (datetime, np.datetime64)):
                 # GH#18435 strings get a pass from tzawareness compat
@@ -110,9 +112,12 @@ def wrapper(self, other):
             result = op(self.asi8, other.view('i8'))
             if isna(other):
                 result.fill(nat_result)
-        elif lib.is_scalar(other):
+        elif lib.is_scalar(other) or np.ndim(other) == 0:
             return ops.invalid_comparison(self, other, op)
         else:
+            if len(other) != len(self):
+                raise ValueError("Lengths must match")
+
             if isinstance(other, list):
                 try:
                     other = type(self)(other)
diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py
index 60febc5f5636d..ac2db6521bf6b 100644
--- a/pandas/core/arrays/period.py
+++ b/pandas/core/arrays/period.py
@@ -57,6 +57,9 @@ def wrapper(self, other):
         # return here with an unboxed PeriodArray). But before we do that,
         # we do a bit of validation on type (Period) and freq, so that our
         # error messages are sensible
+        if is_list_like(other) and len(other) != len(self):
+            raise ValueError("Lengths must match")
+
         not_implemented = isinstance(other, (ABCSeries, ABCIndexClass))
         if not_implemented:
             other = other._values
diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py
index a5d074df338ee..b7907a37e0c56 100644
--- a/pandas/core/arrays/timedeltas.py
+++ b/pandas/core/arrays/timedeltas.py
@@ -90,6 +90,9 @@ def wrapper(self, other):
             return ops.invalid_comparison(self, other, op)
 
         else:
+            if len(other) != len(self):
+                raise ValueError("Lengths must match")
+
             try:
                 other = type(self)._from_sequence(other)._data
             except (ValueError, TypeError):
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
index a2cf88fa9cb1a..b6cb30514eaeb 100644
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -5085,6 +5085,7 @@ def _evaluate_numeric_unary(self):
                 attrs = self._maybe_update_attributes(attrs)
                 return Index(op(self.values), **attrs)
 
+            _evaluate_numeric_unary.__name__ = opstr
             return _evaluate_numeric_unary
 
         cls.__neg__ = _make_evaluate_unary(operator.neg, '__neg__')
diff --git a/pandas/core/ops.py b/pandas/core/ops.py
index 61bf73cbc280f..7cab52ddda87f 100644
--- a/pandas/core/ops.py
+++ b/pandas/core/ops.py
@@ -1390,6 +1390,7 @@ def f(self, other):
 
             return self
 
+        f.__name__ = "__i{name}__".format(name=method.__name__.strip("__"))
         return f
 
     new_methods.update(
@@ -1574,6 +1575,7 @@ def wrapper(left, right):
         return construct_result(left, result,
                                 index=left.index, name=res_name, dtype=None)
 
+    wrapper.__name__ = op_name
     return wrapper
 
 
@@ -1762,6 +1764,7 @@ def wrapper(self, other, axis=None):
             return self._constructor(res_values, index=self.index,
                                      name=res_name, dtype='bool')
 
+    wrapper.__name__ = op_name
     return wrapper
 
 
diff --git a/pandas/core/series.py b/pandas/core/series.py
index d642a221e4494..ff8d242e78ab1 100644
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -91,6 +91,7 @@ def wrapper(self):
         raise TypeError("cannot convert the series to "
                         "{0}".format(str(converter)))
 
+    wrapper.__name__ = "__{name}__".format(name=converter.__name__)
     return wrapper
 
 # ----------------------------------------------------------------------
diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py
index 902a3dda92bd6..ebe84232d7f6d 100644
--- a/pandas/tests/arrays/test_datetimelike.py
+++ b/pandas/tests/arrays/test_datetimelike.py
@@ -59,6 +59,21 @@ def timedelta_index(request):
 class SharedTests(object):
     index_cls = None
 
+    def test_compare_len1_raises(self):
+        # make sure we raise when comparing with different lengths, specific
+        #  to the case where one has length-1, which numpy would broadcast
+        data = np.arange(10, dtype='i8')
+
+        idx = self.index_cls._simple_new(data, freq='D')
+        arr = self.array_cls(idx)
+
+        with pytest.raises(ValueError, match="Lengths must match"):
+            arr == arr[:1]
+
+        # test the index classes while we're at it, GH#23078
+        with pytest.raises(ValueError, match="Lengths must match"):
+            idx <= idx[[0]]
+
     def test_take(self):
         data = np.arange(100, dtype='i8')
         np.random.shuffle(data)
@@ -119,6 +134,15 @@ class TestDatetimeArray(SharedTests):
     index_cls = pd.DatetimeIndex
     array_cls = DatetimeArray
 
+    def test_round(self, tz_naive_fixture):
+        # GH#24064
+        tz = tz_naive_fixture
+        dti = pd.date_range('2016-01-01 01:01:00', periods=3, freq='H', tz=tz)
+
+        result = dti.round(freq='2T')
+        expected = dti - pd.Timedelta(minutes=1)
+        tm.assert_index_equal(result, expected)
+
     def test_array_object_dtype(self, tz_naive_fixture):
         # GH#23524
         tz = tz_naive_fixture


Code review comments:
diff_hunk: @@ -110,9 +112,12 @@ def wrapper(self, other):
             result = op(self.asi8, other.view('i8'))
             if isna(other):
                 result.fill(nat_result)
-        elif lib.is_scalar(other):
+        elif lib.is_scalar(other) or np.ndim(other) == 0:
             return ops.invalid_comparison(self, other, op)
         else:
+            if len(other) != len(self):
path: pandas/core/arrays/datetimes.py
text: @user1:
should be part of the elif

diff_hunk: @@ -90,6 +90,9 @@ def wrapper(self, other):
             return ops.invalid_comparison(self, other, op)
 
         else:
+            if len(other) != len(self):
path: pandas/core/arrays/timedeltas.py
text: @user1:
make a part of the elif