Repository: hgrecco/pint-pandas

Issue:
Fixing the tests
I'd like to add a PR which fixes #51 but it's not possible at the moment because the tests wouldn't pass, even if I made the right change. More generally, at the moment we can't really do any meaningful pull request reviews because the tests are failing. 

@andrewgsavage I think you have the best overview of this, can you see a way to get the tests going again? For example, could we split #48 into two: 1. fix tests, 2. fix offset units? That would help things move and give a clearer sense of what functionality is still not yet implemented (because it's waiting on fixes to e.g. https://github.com/pandas-dev/pandas/issues/35131 

Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index 61162eb..0ae0912 100644
--- a/CHANGES
+++ b/CHANGES
@@ -4,7 +4,9 @@ pint-pandas Changelog
 0.2 (unreleased)
 ----------------
 
-- Nothing changed yet.
+- Partial support for pandas 1.2, which allows DataFrames with PintArrays to be plotted #53 #60
+- Fixed bug which prevented creating a PintArray with offset units like temperature. #48
+- Fixed bug which caused Series.min() to throw an exception. #47
 
 
 0.1 (2020-07-01)
diff --git a/pint_pandas/pint_array.py b/pint_pandas/pint_array.py
index 4397991..4ad2404 100644
--- a/pint_pandas/pint_array.py
+++ b/pint_pandas/pint_array.py
@@ -334,10 +334,10 @@ class PintArray(ExtensionArray, ExtensionOpsMixin):
         ):
             dtype = PintType(dtype)
         if isinstance(dtype, PintType):
-            if dtype == self._dtype:
+            if dtype == self._dtype and not copy:
                 return self
             else:
-                return PintArray(self.quantity.to(dtype.units), dtype)
+                return PintArray(self.quantity.to(dtype.units).magnitude, dtype)
         return self.__array__(dtype, copy)
 
     @property


Test patch:
diff --git a/pint_pandas/testsuite/test_pandas_interface.py b/pint_pandas/testsuite/test_pandas_interface.py
index 9d4e47a..404f6f4 100644
--- a/pint_pandas/testsuite/test_pandas_interface.py
+++ b/pint_pandas/testsuite/test_pandas_interface.py
@@ -4,7 +4,6 @@ from os.path import dirname, join
 
 import numpy as np
 import pandas as pd
-import pint
 import pytest
 from pandas.core import ops
 from pandas.tests.extension import base
@@ -22,7 +21,7 @@ from pint.testsuite.test_quantity import QuantityTestCase
 import pint_pandas as ppi
 from pint_pandas import PintArray
 
-ureg = pint.UnitRegistry()
+ureg = ppi.PintType.ureg
 
 
 @pytest.fixture(params=[True, False])
@@ -193,19 +192,76 @@ def all_boolean_reductions(request):
 
 
 class TestCasting(base.BaseCastingTests):
-    pass
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    def test_astype_str(self, data):
+        result = pd.Series(data[:5]).astype(str)
+        expected = pd.Series([str(x) for x in data[:5]], dtype=str)
+        self.assert_series_equal(result, expected)
+
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    def test_astype_string(self, data):
+        # GH-33465
+        result = pd.Series(data[:5]).astype("string")
+        expected = pd.Series([str(x) for x in data[:5]], dtype="string")
+        self.assert_series_equal(result, expected)
 
 
 class TestConstructors(base.BaseConstructorsTests):
-    pass
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    def test_series_constructor_no_data_with_index(self, dtype, na_value):
+        result = pd.Series(index=[1, 2, 3], dtype=dtype)
+        expected = pd.Series([na_value] * 3, index=[1, 2, 3], dtype=dtype)
+        self.assert_series_equal(result, expected)
+
+        # GH 33559 - empty index
+        result = pd.Series(index=[], dtype=dtype)
+        expected = pd.Series([], index=pd.Index([], dtype="object"), dtype=dtype)
+        self.assert_series_equal(result, expected)
+
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    def test_series_constructor_scalar_na_with_index(self, dtype, na_value):
+        result = pd.Series(na_value, index=[1, 2, 3], dtype=dtype)
+        expected = pd.Series([na_value] * 3, index=[1, 2, 3], dtype=dtype)
+        self.assert_series_equal(result, expected)
+
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    def test_series_constructor_scalar_with_index(self, data, dtype):
+        scalar = data[0]
+        result = pd.Series(scalar, index=[1, 2, 3], dtype=dtype)
+        expected = pd.Series([scalar] * 3, index=[1, 2, 3], dtype=dtype)
+        self.assert_series_equal(result, expected)
+
+        result = pd.Series(scalar, index=["foo"], dtype=dtype)
+        expected = pd.Series([scalar], index=["foo"], dtype=dtype)
+        self.assert_series_equal(result, expected)
 
 
 class TestDtype(base.BaseDtypeTests):
-    pass
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    def test_construct_from_string_another_type_raises(self, dtype):
+        msg = f"Cannot construct a '{type(dtype).__name__}' from 'another_type'"
+        with pytest.raises(TypeError, match=msg):
+            type(dtype).construct_from_string("another_type")
+
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    def test_construct_from_string_wrong_type_raises(self, dtype):
+        with pytest.raises(
+            TypeError,
+            match="'construct_from_string' expects a string, got <class 'int'>",
+        ):
+            type(dtype).construct_from_string(0)
 
 
 class TestGetitem(base.BaseGetitemTests):
-    pass
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    def test_getitem_mask_raises(self, data):
+        mask = np.array([True, False])
+        with pytest.raises(IndexError):
+            data[mask]
+
+        mask = pd.array(mask, dtype="boolean")
+        with pytest.raises(IndexError):
+            data[mask]
 
 
 class TestGroupby(base.BaseGroupbyTests):
@@ -247,7 +303,31 @@ class TestGroupby(base.BaseGroupbyTests):
 
 
 class TestInterface(base.BaseInterfaceTests):
-    pass
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    def test_contains(self, data, data_missing):
+        # GH-37867
+        # Tests for membership checks. Membership checks for nan-likes is tricky and
+        # the settled on rule is: `nan_like in arr` is True if nan_like is
+        # arr.dtype.na_value and arr.isna().any() is True. Else the check returns False.
+
+        na_value = data.dtype.na_value
+        # ensure data without missing values
+        data = data[~data.isna()]
+
+        # first elements are non-missing
+        assert data[0] in data
+        assert data_missing[0] in data_missing
+
+        # check the presence of na_value
+        assert na_value in data_missing
+        assert na_value not in data
+
+        # the data can never contain other nan-likes than na_value
+        # for na_value_obj in tm.NULL_OBJECTS:
+        #     if na_value_obj is na_value:
+        #         continue
+        #     assert na_value_obj not in data
+        #     assert na_value_obj not in data_missing
 
 
 class TestMethods(base.BaseMethodsTests):
@@ -359,6 +439,30 @@ class TestMethods(base.BaseMethodsTests):
             expected = expected.to_frame(name="a")
         self.assert_equal(result, expected)
 
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    @pytest.mark.parametrize("ascending", [True, False])
+    def test_sort_values(self, data_for_sorting, ascending, sort_by_key):
+        ser = pd.Series(data_for_sorting)
+        result = ser.sort_values(ascending=ascending, key=sort_by_key)
+        expected = ser.iloc[[2, 0, 1]]
+        if not ascending:
+            expected = expected[::-1]
+
+        self.assert_series_equal(result, expected)
+
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    @pytest.mark.parametrize("ascending", [True, False])
+    def test_sort_values_missing(
+        self, data_missing_for_sorting, ascending, sort_by_key
+    ):
+        ser = pd.Series(data_missing_for_sorting)
+        result = ser.sort_values(ascending=ascending, key=sort_by_key)
+        if ascending:
+            expected = ser.iloc[[2, 0, 1]]
+        else:
+            expected = ser.iloc[[0, 2, 1]]
+        self.assert_series_equal(result, expected)
+
 
 class TestArithmeticOps(base.BaseArithmeticOpsTests):
     def check_opname(self, s, op_name, other, exc=None):
@@ -456,6 +560,20 @@ class TestArithmeticOps(base.BaseArithmeticOpsTests):
         with pytest.raises(ValueError):
             opa(np.arange(len(s)).reshape(-1, len(s)))
 
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    @pytest.mark.parametrize("box", [pd.Series, pd.DataFrame])
+    def test_direct_arith_with_ndframe_returns_not_implemented(self, data, box):
+        # EAs should return NotImplemented for ops with Series/DataFrame
+        # Pandas takes care of unboxing the series and calling the EA's op.
+        other = pd.Series(data)
+        if box is pd.DataFrame:
+            other = other.to_frame()
+        if hasattr(data, "__add__"):
+            result = data.__add__(other)
+            assert result is NotImplemented
+        else:
+            raise pytest.skip(f"{type(data).__name__} does not implement add")
+
 
 class TestComparisonOps(base.BaseComparisonOpsTests):
     def _compare_other(self, s, data, op_name, other):
@@ -479,6 +597,27 @@ class TestComparisonOps(base.BaseComparisonOpsTests):
         other = data
         self._compare_other(s, data, op_name, other)
 
+    @pytest.mark.xfail(run=True, reason="TODO: fix pd 1.2 tests")
+    @pytest.mark.parametrize("box", [pd.Series, pd.DataFrame])
+    def test_direct_arith_with_ndframe_returns_not_implemented(self, data, box):
+        # EAs should return NotImplemented for ops with Series/DataFrame
+        # Pandas takes care of unboxing the series and calling the EA's op.
+        other = pd.Series(data)
+        if box is pd.DataFrame:
+            other = other.to_frame()
+
+        if hasattr(data, "__eq__"):
+            result = data.__eq__(other)
+            assert result is NotImplemented
+        else:
+            raise pytest.skip(f"{type(data).__name__} does not implement __eq__")
+
+        if hasattr(data, "__ne__"):
+            result = data.__ne__(other)
+            assert result is NotImplemented
+        else:
+            raise pytest.skip(f"{type(data).__name__} does not implement __ne__")
+
 
 class TestOpsUtil(base.BaseOpsUtil):
     pass
@@ -661,11 +800,17 @@ class TestSetitem(base.BaseSetitemTests):
 
 
 class TestOffsetUnits(object):
-    def test_offset_concat():
-        a = pd.Series(PintArray(range(5), ureg.Unit("degC")))
-        b = pd.Series(PintArray(range(6), ureg.Unit("degC")))
+    @pytest.mark.xfail(run=True, reason="TODO untested issue that was fixed")
+    def test_offset_concat(self):
+        q_a = ureg.Quantity(np.arange(5), ureg.Unit("degC"))
+        q_b = ureg.Quantity(np.arange(6), ureg.Unit("degC"))
 
-        pd.concat([a, b], axis=1)
+        a = pd.Series(PintArray(q_a))
+        b = pd.Series(PintArray(q_b))
+
+        result = pd.concat([a, b], axis=1)
+        expected = pd.Series(PintArray(np.concatenate([q_b, q_b]), dtype="pint[degC]"))
+        self.assert_equal(result, expected)
 
 
 # would be ideal to just test all of this by running the example notebook