Repository: pandas-dev/pandas
Pull request: BUG: assert_index_equal does not raise error for check_categorical=False when comparing 2 CategoricalIndex objects
- [x] closes #19776 
- [x] tests added / passed
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry

* tests added for `check_categorical` parameter in `assert_series_equal`
* cleaned up `assert_categorical_equal` parameter order and docstring to match other functions (`obj` at the end)

Thanks to all maintainers who helped answer questions on Gitter during the informal PyCon sprint!

Problem statement:
assert_index_equal checks categories with check_categorical=False
The behavior of `assert_index_equal` should match `assert_series_equal` here with `check_categorical=False`.

```python
In [3]: import pandas.util.testing as tm

In [4]: import pandas as pd

In [5]: tm.assert_series_equal(
   ...:     pd.Series(pd.Categorical(['a', 'b'])),
   ...:     pd.Series(pd.Categorical(['a', 'b'], categories=['a', 'b', 'c'])),
   ...:     check_categorical=False
   ...: )

In [8]: tm.assert_index_equal(
   ...:     pd.Index(pd.Categorical(['a', 'b'])),
   ...:     pd.Index(pd.Categorical(['a', 'b'], categories=['a', 'b', 'c'])),
   ...:     check_categorical=False
   ...: )
```


```pytb
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-8-554e162c7f12> in <module>()
      2     pd.Index(pd.Categorical(['a', 'b'])),
      3     pd.Index(pd.Categorical(['a', 'b'], categories=['a', 'b', 'c'])),
----> 4     check_categorical=False
      5 )

~/sandbox/pandas-ip/pandas/pandas/util/testing.py in assert_index_equal(left, right, exact, check_names, check_less_precise, check_exact, check_categorical, obj)
    798
    799     # class / dtype comparison
--> 800     _check_types(left, right, obj=obj)
    801
    802     # level comparison

~/sandbox/pandas-ip/pandas/pandas/util/testing.py in _check_types(l, r, obj)
    779         if exact:
    780             assert_class_equal(left, right, exact=exact, obj=obj)
--> 781             assert_attr_equal('dtype', l, r, obj=obj)
    782             # allow string-like to have different inferred_types
    783             if l.inferred_type in ('string', 'unicode'):

~/sandbox/pandas-ip/pandas/pandas/util/testing.py in assert_attr_equal(attr, left, right, obj)
    921     else:
    922         msg = 'Attribute "{attr}" are different'.format(attr=attr)
--> 923         raise_assert_detail(obj, msg, left_attr, right_attr)
    924
    925

~/sandbox/pandas-ip/pandas/pandas/util/testing.py in raise_assert_detail(obj, message, left, right, diff)
   1006         msg += "\n[diff]: {diff}".format(diff=diff)
   1007
-> 1008     raise AssertionError(msg)
   1009
   1010

AssertionError: Index are different

Attribute "dtype" are different
[left]:  CategoricalDtype(categories=['a', 'b'], ordered=False)
[right]: CategoricalDtype(categories=['a', 'b', 'c'], ordered=False)
```

Hints from issue discussion:
I'll tackle this one.

Commit message: properly fix unicode string failures

Patch to review:
diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt
index b94377af770f4..17b464eb6590b 100644
--- a/doc/source/whatsnew/v0.24.0.txt
+++ b/doc/source/whatsnew/v0.24.0.txt
@@ -76,7 +76,7 @@ Bug Fixes
 Categorical
 ^^^^^^^^^^^
 
--
+- Bug in :func:`pandas.util.testing.assert_index_equal` raised ``AssertionError`` if comparing two :class:`CategoricalIndex` objects when ``check_categorical=False`` (:issue:`19776`)
 -
 -
 
diff --git a/pandas/tests/util/test_testing.py b/pandas/tests/util/test_testing.py
index d6f58d16bcf64..880c4ea2f77b2 100644
--- a/pandas/tests/util/test_testing.py
+++ b/pandas/tests/util/test_testing.py
@@ -503,6 +503,24 @@ def test_index_equal_metadata_message(self):
         with tm.assert_raises_regex(AssertionError, expected):
             assert_index_equal(idx1, idx2)
 
+    def test_categorical_index_equality(self):
+        expected = """Index are different
+
+Attribute "dtype" are different
+\\[left\\]:  CategoricalDtype\\(categories=\\[u?'a', u?'b'\\], ordered=False\\)
+\\[right\\]: CategoricalDtype\\(categories=\\[u?'a', u?'b', u?'c'\\], ordered=False\\)"""  # noqa
+
+        with tm.assert_raises_regex(AssertionError, expected):
+            assert_index_equal(pd.Index(pd.Categorical(['a', 'b'])),
+                               pd.Index(pd.Categorical(['a', 'b'],
+                                        categories=['a', 'b', 'c'])))
+
+    def test_categorical_index_equality_relax_categories_check(self):
+        assert_index_equal(pd.Index(pd.Categorical(['a', 'b'])),
+                           pd.Index(pd.Categorical(['a', 'b'],
+                                    categories=['a', 'b', 'c'])),
+                           check_categorical=False)
+
 
 class TestAssertSeriesEqual(object):
 
@@ -600,6 +618,24 @@ def test_series_equal_message(self):
             assert_series_equal(pd.Series([1, 2, 3]), pd.Series([1, 2, 4]),
                                 check_less_precise=True)
 
+    def test_categorical_series_equality(self):
+        expected = """Attributes are different
+
+Attribute "dtype" are different
+\\[left\\]:  CategoricalDtype\\(categories=\\[u?'a', u?'b'\\], ordered=False\\)
+\\[right\\]: CategoricalDtype\\(categories=\\[u?'a', u?'b', u?'c'\\], ordered=False\\)"""  # noqa
+
+        with tm.assert_raises_regex(AssertionError, expected):
+            assert_series_equal(pd.Series(pd.Categorical(['a', 'b'])),
+                                pd.Series(pd.Categorical(['a', 'b'],
+                                          categories=['a', 'b', 'c'])))
+
+    def test_categorical_series_equality_relax_categories_check(self):
+        assert_series_equal(pd.Series(pd.Categorical(['a', 'b'])),
+                            pd.Series(pd.Categorical(['a', 'b'],
+                                      categories=['a', 'b', 'c'])),
+                            check_categorical=False)
+
 
 class TestAssertFrameEqual(object):
 
diff --git a/pandas/util/testing.py b/pandas/util/testing.py
index e1484a9c1b390..ce3f29fa508e4 100644
--- a/pandas/util/testing.py
+++ b/pandas/util/testing.py
@@ -778,8 +778,12 @@ def assert_index_equal(left, right, exact='equiv', check_names=True,
 
     def _check_types(l, r, obj='Index'):
         if exact:
-            assert_class_equal(left, right, exact=exact, obj=obj)
-            assert_attr_equal('dtype', l, r, obj=obj)
+            assert_class_equal(l, r, exact=exact, obj=obj)
+
+            # Skip exact dtype checking with `check_categorical` is False
+            if check_categorical:
+                assert_attr_equal('dtype', l, r, obj=obj)
+
             # allow string-like to have different inferred_types
             if l.inferred_type in ('string', 'unicode'):
                 assert r.inferred_type in ('string', 'unicode')
@@ -829,7 +833,8 @@ def _get_ilevel_values(index, level):
             # get_level_values may change dtype
             _check_types(left.levels[level], right.levels[level], obj=obj)
 
-    if check_exact:
+    run_exact_index_check = check_exact and check_categorical
+    if run_exact_index_check:
         if not left.equals(right):
             diff = np.sum((left.values != right.values)
                           .astype(int)) * 100.0 / len(left)
@@ -950,23 +955,23 @@ def is_sorted(seq):
 
 
 def assert_categorical_equal(left, right, check_dtype=True,
-                             obj='Categorical', check_category_order=True):
+                             check_category_order=True, obj='Categorical'):
     """Test that Categoricals are equivalent.
 
     Parameters
     ----------
-    left, right : Categorical
-        Categoricals to compare
+    left : Categorical
+    right : Categorical
     check_dtype : bool, default True
         Check that integer dtype of the codes are the same
-    obj : str, default 'Categorical'
-        Specify object name being compared, internally used to show appropriate
-        assertion message
     check_category_order : bool, default True
         Whether the order of the categories should be compared, which
         implies identical integer codes.  If False, only the resulting
         values are compared.  The ordered attribute is
         checked regardless.
+    obj : str, default 'Categorical'
+        Specify object name being compared, internally used to show appropriate
+        assertion message
     """
     _check_isinstance(left, right, Categorical)
 


Code review comments:
diff_hunk: @@ -600,6 +618,24 @@ def test_series_equal_message(self):
             assert_series_equal(pd.Series([1, 2, 3]), pd.Series([1, 2, 4]),
                                 check_less_precise=True)
 
+    def test_categorical_series_equality(self):
+        expected = """Attributes are different
+
+Attribute "dtype" are different
+\\[left\\]:  CategoricalDtype\\(categories=\\[u?'a', u?'b'\\], ordered=False\\)
+\\[right\\]: CategoricalDtype\\(categories=\\[u?'a', u?'b', u?'c'\\], ordered=False\\)"""  # noqa
path: pandas/tests/util/test_testing.py
text: @user1:
I have a slight preference for avoiding the `noqa` if possible. Can you use a `\` to escape the newline? Something like

```
        expected = """Attributes are different

Attribute "dtype" are different
\\[left\\]:  CategoricalDtype\\(categories=\\[u?'a', u?'b'\\], ordered=False\\)
\\[right\\]: CategoricalDtype\\(categories=\\[u?'a', u?'b', u?'c'\\], \
ordered=False\\)"""
```

@author:
Good call, didn't feel right to me either. I was wondering how to split lines.