From 95082cb84ce2ee8c015e5310c6197605c37ee1b0 Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Sat, 12 Oct 2019 15:52:41 -0500 Subject: [PATCH 01/16] Add test --- pandas/tests/extension/test_integer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pandas/tests/extension/test_integer.py b/pandas/tests/extension/test_integer.py index d051345fdd12d..9a37fe076d37a 100644 --- a/pandas/tests/extension/test_integer.py +++ b/pandas/tests/extension/test_integer.py @@ -168,6 +168,15 @@ def check_opname(self, s, op_name, other, exc=None): def _compare_other(self, s, data, op_name, other): self.check_opname(s, op_name, other) + def test_can_compare_to_string(self): + # GH 28930 + s = pd.Series([0, None], dtype="Int64") + + result = s == "a" + expected = pd.Series([False, False]) + + self.assert_series_equal(result, expected) + class TestInterface(base.BaseInterfaceTests): pass From bb53568dd48cd91041bf12bf255204315ba3d1a9 Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Sat, 12 Oct 2019 16:01:27 -0500 Subject: [PATCH 02/16] Check if method is not implemented --- pandas/core/arrays/integer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index 630c3e50f2c09..9af351edc4680 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -27,6 +27,7 @@ from pandas.core import nanops, ops from pandas.core.algorithms import take from pandas.core.arrays import ExtensionArray, ExtensionOpsMixin +from pandas.core.ops import invalid_comparison from pandas.core.tools.numeric import to_numeric @@ -628,7 +629,11 @@ def cmp_method(self, other): with warnings.catch_warnings(): warnings.filterwarnings("ignore", "elementwise", FutureWarning) with np.errstate(all="ignore"): - result = op(self._data, other) + method = getattr(self._data, f"__{op_name}__") + result = method(other) + + if result is NotImplemented: + result = invalid_comparison(self._data, other, op) # nans propagate if mask is None: From 832ddf6871c3e8dbb04f7e0cb46583c3ef74f52f Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Sat, 12 Oct 2019 16:03:17 -0500 Subject: [PATCH 03/16] Add release note --- doc/source/whatsnew/v1.0.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index 1112e42489342..d788e43106160 100644 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -360,6 +360,7 @@ ExtensionArray ^^^^^^^^^^^^^^ - Bug in :class:`arrays.PandasArray` when setting a scalar string (:issue:`28118`, :issue:`28150`). +- Bug where nullable integers could not be compared to strings (:issue:`28930`) - From 18bc454b3ff599cf06b0c9aae350b273993a0c37 Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Sat, 12 Oct 2019 18:17:00 -0500 Subject: [PATCH 04/16] Use format --- pandas/core/arrays/integer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index 9af351edc4680..e5915585884c8 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -629,7 +629,9 @@ def cmp_method(self, other): with warnings.catch_warnings(): warnings.filterwarnings("ignore", "elementwise", FutureWarning) with np.errstate(all="ignore"): - method = getattr(self._data, f"__{op_name}__") + method = getattr( + self._data, "__{op_name}__".format(op_name=op_name) + ) result = method(other) if result is NotImplemented: From b1238a809b58a02a7f961973673eda0aef44868e Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Sun, 13 Oct 2019 22:20:52 -0500 Subject: [PATCH 05/16] Add test cases --- pandas/tests/extension/test_common.py | 13 +++++++++++++ pandas/tests/extension/test_integer.py | 9 --------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/pandas/tests/extension/test_common.py b/pandas/tests/extension/test_common.py index 9b5f9d64f6b67..61c7e32f6c88a 100644 --- a/pandas/tests/extension/test_common.py +++ b/pandas/tests/extension/test_common.py @@ -79,3 +79,16 @@ def test_astype_no_copy(): def test_is_extension_array_dtype(dtype): assert isinstance(dtype, dtypes.ExtensionDtype) assert is_extension_array_dtype(dtype) + + +@pytest.mark.parametrize("array", [ + pd.Series([1, None], dtype="Int64"), + pd.Series(["2019", "2020"], dtype="datetime64[ns, UTC]"), + pd.Series([0, 0], dtype="timedelta64[ns]"), +]) +def test_compare_unequal_to_string(array): + # GH 28930 + result = array == "a" + expected = pd.Series([False, False]) + + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/extension/test_integer.py b/pandas/tests/extension/test_integer.py index 9a37fe076d37a..d051345fdd12d 100644 --- a/pandas/tests/extension/test_integer.py +++ b/pandas/tests/extension/test_integer.py @@ -168,15 +168,6 @@ def check_opname(self, s, op_name, other, exc=None): def _compare_other(self, s, data, op_name, other): self.check_opname(s, op_name, other) - def test_can_compare_to_string(self): - # GH 28930 - s = pd.Series([0, None], dtype="Int64") - - result = s == "a" - expected = pd.Series([False, False]) - - self.assert_series_equal(result, expected) - class TestInterface(base.BaseInterfaceTests): pass From 2d8a58762592502d0f8ee9b41be265cafee09b05 Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Mon, 14 Oct 2019 16:43:05 -0500 Subject: [PATCH 06/16] Blacken --- pandas/tests/extension/test_common.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pandas/tests/extension/test_common.py b/pandas/tests/extension/test_common.py index 61c7e32f6c88a..e1ae29ce782c5 100644 --- a/pandas/tests/extension/test_common.py +++ b/pandas/tests/extension/test_common.py @@ -81,11 +81,14 @@ def test_is_extension_array_dtype(dtype): assert is_extension_array_dtype(dtype) -@pytest.mark.parametrize("array", [ - pd.Series([1, None], dtype="Int64"), - pd.Series(["2019", "2020"], dtype="datetime64[ns, UTC]"), - pd.Series([0, 0], dtype="timedelta64[ns]"), -]) +@pytest.mark.parametrize( + "array", + [ + pd.Series([1, None], dtype="Int64"), + pd.Series(["2019", "2020"], dtype="datetime64[ns, UTC]"), + pd.Series([0, 0], dtype="timedelta64[ns]"), + ], +) def test_compare_unequal_to_string(array): # GH 28930 result = array == "a" From acfc645a813644d75468dc8110c9efc086444e97 Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Mon, 14 Oct 2019 16:52:16 -0500 Subject: [PATCH 07/16] Add tests for invalid comparisons --- pandas/tests/extension/test_common.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pandas/tests/extension/test_common.py b/pandas/tests/extension/test_common.py index e1ae29ce782c5..370a7d41d1728 100644 --- a/pandas/tests/extension/test_common.py +++ b/pandas/tests/extension/test_common.py @@ -95,3 +95,20 @@ def test_compare_unequal_to_string(array): expected = pd.Series([False, False]) tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize( + "array", + [ + pd.Series([1, None], dtype="Int64"), + pd.Series(["2019", "2020"], dtype="datetime64[ns, UTC]"), + pd.Series([0, 0], dtype="timedelta64[ns]"), + ], +) +@pytest.mark.parametrize("op", ["__lt__", "__le__", "__gt__", "__ge__"]) +def test_compare_to_string_invalid(array, op): + # GH 28930 + method = getattr(array, op) + + with pytest.raises(TypeError): + method("a") From becf519fffc5d10129027dff5985e7912ef9f8fe Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Tue, 15 Oct 2019 15:39:08 -0500 Subject: [PATCH 08/16] Fix tests --- pandas/tests/extension/test_common.py | 16 ---------------- pandas/tests/extension/test_integer.py | 9 +++++++++ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/pandas/tests/extension/test_common.py b/pandas/tests/extension/test_common.py index 370a7d41d1728..05b4dece83eb8 100644 --- a/pandas/tests/extension/test_common.py +++ b/pandas/tests/extension/test_common.py @@ -81,22 +81,6 @@ def test_is_extension_array_dtype(dtype): assert is_extension_array_dtype(dtype) -@pytest.mark.parametrize( - "array", - [ - pd.Series([1, None], dtype="Int64"), - pd.Series(["2019", "2020"], dtype="datetime64[ns, UTC]"), - pd.Series([0, 0], dtype="timedelta64[ns]"), - ], -) -def test_compare_unequal_to_string(array): - # GH 28930 - result = array == "a" - expected = pd.Series([False, False]) - - tm.assert_series_equal(result, expected) - - @pytest.mark.parametrize( "array", [ diff --git a/pandas/tests/extension/test_integer.py b/pandas/tests/extension/test_integer.py index d051345fdd12d..c1e8fbba86b47 100644 --- a/pandas/tests/extension/test_integer.py +++ b/pandas/tests/extension/test_integer.py @@ -168,6 +168,15 @@ def check_opname(self, s, op_name, other, exc=None): def _compare_other(self, s, data, op_name, other): self.check_opname(s, op_name, other) + @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) + def test_compare_unequal_to_string(self, dtype): + # GH 28930 + s = pd.Series([1, None], dtype=dtype) + result = s == "a" + expected = pd.Series([False, False]) + + self.assert_series_equal(result, expected) + class TestInterface(base.BaseInterfaceTests): pass From 6cef8e11f0ffe529e0fc080091e82bdf0211031c Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Tue, 15 Oct 2019 20:22:37 -0500 Subject: [PATCH 09/16] Update tests --- pandas/tests/extension/test_common.py | 17 ----------------- pandas/tests/extension/test_integer.py | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/pandas/tests/extension/test_common.py b/pandas/tests/extension/test_common.py index 05b4dece83eb8..9b5f9d64f6b67 100644 --- a/pandas/tests/extension/test_common.py +++ b/pandas/tests/extension/test_common.py @@ -79,20 +79,3 @@ def test_astype_no_copy(): def test_is_extension_array_dtype(dtype): assert isinstance(dtype, dtypes.ExtensionDtype) assert is_extension_array_dtype(dtype) - - -@pytest.mark.parametrize( - "array", - [ - pd.Series([1, None], dtype="Int64"), - pd.Series(["2019", "2020"], dtype="datetime64[ns, UTC]"), - pd.Series([0, 0], dtype="timedelta64[ns]"), - ], -) -@pytest.mark.parametrize("op", ["__lt__", "__le__", "__gt__", "__ge__"]) -def test_compare_to_string_invalid(array, op): - # GH 28930 - method = getattr(array, op) - - with pytest.raises(TypeError): - method("a") diff --git a/pandas/tests/extension/test_integer.py b/pandas/tests/extension/test_integer.py index c1e8fbba86b47..704608f599a92 100644 --- a/pandas/tests/extension/test_integer.py +++ b/pandas/tests/extension/test_integer.py @@ -177,6 +177,30 @@ def test_compare_unequal_to_string(self, dtype): self.assert_series_equal(result, expected) + @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) + @pytest.mark.parametrize("op", ["__lt__", "__le__"]) + def test_compare_lt(self, dtype, op): + # GH 28930 + s = pd.Series([0, 0], dtype=dtype) + method = getattr(s, op) + result = method(1) + + expected = pd.Series([True, True]) + + self.assert_series_equal(result, expected) + + @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) + @pytest.mark.parametrize("op", ["__gt__", "__ge__"]) + def test_compare_gt(self, dtype, op): + # GH 28930 + s = pd.Series([0, 0], dtype=dtype) + method = getattr(s, op) + result = method(-1) + + expected = pd.Series([True, True]) + + self.assert_series_equal(result, expected) + class TestInterface(base.BaseInterfaceTests): pass From adc272d83f7336af433f65432e18fdad4a9e0c96 Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Wed, 16 Oct 2019 17:58:56 -0500 Subject: [PATCH 10/16] Create fixture --- pandas/conftest.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pandas/conftest.py b/pandas/conftest.py index b032e14d8f7e1..8e10bfd883b75 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -640,6 +640,24 @@ def any_int_dtype(request): return request.param +@pytest.fixture(params=ALL_EA_INT_DTYPES) +def Int_dtype(request): + """ + Parameterized fixture for any nullable integer dtype. + + * 'UInt8' + * 'Int8' + * 'UInt16' + * 'Int16' + * 'UInt32' + * 'Int32' + * 'UInt64' + * 'Int64' + """ + + return request.param + + @pytest.fixture(params=ALL_REAL_DTYPES) def any_real_dtype(request): """ From af252c30a2c79a66029a44f11961f1b95a774b8e Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Wed, 16 Oct 2019 18:13:18 -0500 Subject: [PATCH 11/16] Use fixtures --- pandas/tests/extension/test_integer.py | 29 +++++++------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/pandas/tests/extension/test_integer.py b/pandas/tests/extension/test_integer.py index 704608f599a92..9fd97b3b5a13d 100644 --- a/pandas/tests/extension/test_integer.py +++ b/pandas/tests/extension/test_integer.py @@ -168,36 +168,23 @@ def check_opname(self, s, op_name, other, exc=None): def _compare_other(self, s, data, op_name, other): self.check_opname(s, op_name, other) - @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) - def test_compare_unequal_to_string(self, dtype): + def test_compare_unequal_to_string(self, Int_dtype): # GH 28930 - s = pd.Series([1, None], dtype=dtype) + s = pd.Series([1, None], dtype=Int_dtype) result = s == "a" expected = pd.Series([False, False]) self.assert_series_equal(result, expected) - @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) - @pytest.mark.parametrize("op", ["__lt__", "__le__"]) - def test_compare_lt(self, dtype, op): + def test_compare_unequal_to_int(self, Int_dtype, compare_operators_no_eq_ne): # GH 28930 - s = pd.Series([0, 0], dtype=dtype) - method = getattr(s, op) - result = method(1) + s = pd.Series([1, 2, 3], dtype=Int_dtype) - expected = pd.Series([True, True]) + method = getattr(s, compare_operators_no_eq_ne) + result = method(2) - self.assert_series_equal(result, expected) - - @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) - @pytest.mark.parametrize("op", ["__gt__", "__ge__"]) - def test_compare_gt(self, dtype, op): - # GH 28930 - s = pd.Series([0, 0], dtype=dtype) - method = getattr(s, op) - result = method(-1) - - expected = pd.Series([True, True]) + method = getattr(np.array(s), compare_operators_no_eq_ne) + expected = pd.Series(method(2)) self.assert_series_equal(result, expected) From 18553b70241911f07054e402e9417120e596f54e Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Wed, 16 Oct 2019 18:14:35 -0500 Subject: [PATCH 12/16] Modify test names --- pandas/tests/extension/test_integer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/extension/test_integer.py b/pandas/tests/extension/test_integer.py index 9fd97b3b5a13d..64363c5dd7a7a 100644 --- a/pandas/tests/extension/test_integer.py +++ b/pandas/tests/extension/test_integer.py @@ -168,7 +168,7 @@ def check_opname(self, s, op_name, other, exc=None): def _compare_other(self, s, data, op_name, other): self.check_opname(s, op_name, other) - def test_compare_unequal_to_string(self, Int_dtype): + def test_compare_to_string(self, Int_dtype): # GH 28930 s = pd.Series([1, None], dtype=Int_dtype) result = s == "a" @@ -176,7 +176,7 @@ def test_compare_unequal_to_string(self, Int_dtype): self.assert_series_equal(result, expected) - def test_compare_unequal_to_int(self, Int_dtype, compare_operators_no_eq_ne): + def test_compare_to_int(self, Int_dtype, compare_operators_no_eq_ne): # GH 28930 s = pd.Series([1, 2, 3], dtype=Int_dtype) From 2e7ea136974d155d7828d40342e311a7d8affef6 Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Wed, 16 Oct 2019 18:16:10 -0500 Subject: [PATCH 13/16] Check all comparisons --- pandas/tests/extension/test_integer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/tests/extension/test_integer.py b/pandas/tests/extension/test_integer.py index 64363c5dd7a7a..f7623d92cb41e 100644 --- a/pandas/tests/extension/test_integer.py +++ b/pandas/tests/extension/test_integer.py @@ -176,14 +176,14 @@ def test_compare_to_string(self, Int_dtype): self.assert_series_equal(result, expected) - def test_compare_to_int(self, Int_dtype, compare_operators_no_eq_ne): + def test_compare_to_int(self, Int_dtype, all_compare_operators): # GH 28930 s = pd.Series([1, 2, 3], dtype=Int_dtype) - method = getattr(s, compare_operators_no_eq_ne) + method = getattr(s, all_compare_operators) result = method(2) - method = getattr(np.array(s), compare_operators_no_eq_ne) + method = getattr(np.array(s), all_compare_operators) expected = pd.Series(method(2)) self.assert_series_equal(result, expected) From 9e14cb905e0bcef434b6746f983aab014fea41e9 Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Wed, 16 Oct 2019 18:20:33 -0500 Subject: [PATCH 14/16] Update test --- pandas/tests/extension/test_integer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pandas/tests/extension/test_integer.py b/pandas/tests/extension/test_integer.py index f7623d92cb41e..e730607bf1298 100644 --- a/pandas/tests/extension/test_integer.py +++ b/pandas/tests/extension/test_integer.py @@ -178,13 +178,14 @@ def test_compare_to_string(self, Int_dtype): def test_compare_to_int(self, Int_dtype, all_compare_operators): # GH 28930 - s = pd.Series([1, 2, 3], dtype=Int_dtype) + s1 = pd.Series([1, 2, 3], dtype=Int_dtype) + s2 = pd.Series([1, 2, 3], dtype="int") - method = getattr(s, all_compare_operators) + method = getattr(s1, all_compare_operators) result = method(2) - method = getattr(np.array(s), all_compare_operators) - expected = pd.Series(method(2)) + method = getattr(s2, all_compare_operators) + expected = method(2) self.assert_series_equal(result, expected) From 59460615ade107123817d02ffde002a5c1d1dc1d Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Thu, 24 Oct 2019 16:18:16 -0400 Subject: [PATCH 15/16] Change fixture name --- pandas/conftest.py | 2 +- pandas/tests/extension/test_integer.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index 8e10bfd883b75..e7eab100c35e8 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -641,7 +641,7 @@ def any_int_dtype(request): @pytest.fixture(params=ALL_EA_INT_DTYPES) -def Int_dtype(request): +def any_nullable_int_dtype(request): """ Parameterized fixture for any nullable integer dtype. diff --git a/pandas/tests/extension/test_integer.py b/pandas/tests/extension/test_integer.py index e730607bf1298..f94dbfcc3ec6c 100644 --- a/pandas/tests/extension/test_integer.py +++ b/pandas/tests/extension/test_integer.py @@ -168,17 +168,17 @@ def check_opname(self, s, op_name, other, exc=None): def _compare_other(self, s, data, op_name, other): self.check_opname(s, op_name, other) - def test_compare_to_string(self, Int_dtype): + def test_compare_to_string(self, any_nullable_int_dtype): # GH 28930 - s = pd.Series([1, None], dtype=Int_dtype) + s = pd.Series([1, None], dtype=any_nullable_int_dtype) result = s == "a" expected = pd.Series([False, False]) self.assert_series_equal(result, expected) - def test_compare_to_int(self, Int_dtype, all_compare_operators): + def test_compare_to_int(self, any_nullable_int_dtype, all_compare_operators): # GH 28930 - s1 = pd.Series([1, 2, 3], dtype=Int_dtype) + s1 = pd.Series([1, 2, 3], dtype=any_nullable_int_dtype) s2 = pd.Series([1, 2, 3], dtype="int") method = getattr(s1, all_compare_operators) From 1316c20c6059338ed800df52dead738fb0dac6b2 Mon Sep 17 00:00:00 2001 From: Daniel Saxton <> Date: Sun, 8 Dec 2019 20:52:03 -0600 Subject: [PATCH 16/16] Use f-string --- pandas/core/arrays/integer.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index a1db1e37c3034..393eafa7ac10a 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -653,9 +653,7 @@ def cmp_method(self, other): with warnings.catch_warnings(): warnings.filterwarnings("ignore", "elementwise", FutureWarning) with np.errstate(all="ignore"): - method = getattr( - self._data, "__{op_name}__".format(op_name=op_name) - ) + method = getattr(self._data, f"__{op_name}__") result = method(other) if result is NotImplemented: