diff --git a/doc/source/whatsnew/v0.24.2.rst b/doc/source/whatsnew/v0.24.2.rst index 8e59c2300e7ca..f528c058d2868 100644 --- a/doc/source/whatsnew/v0.24.2.rst +++ b/doc/source/whatsnew/v0.24.2.rst @@ -26,6 +26,7 @@ Fixed Regressions - Fixed regression in :meth:`DataFrame.duplicated()`, where empty dataframe was not returning a boolean dtyped Series. (:issue:`25184`) - Fixed regression in :meth:`Series.min` and :meth:`Series.max` where ``numeric_only=True`` was ignored when the ``Series`` contained ```Categorical`` data (:issue:`25299`) +- Fixed regression in ``IntervalDtype`` construction where passing an incorrect string with 'Interval' as a prefix could result in a ``RecursionError``. (:issue:`25338`) .. _whatsnew_0242.enhancements: diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index f84471c3b04e8..b73f55329e25b 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -932,13 +932,18 @@ def construct_from_string(cls, string): attempt to construct this type from a string, raise a TypeError if its not possible """ - if (isinstance(string, compat.string_types) and - (string.startswith('interval') or - string.startswith('Interval'))): - return cls(string) + if not isinstance(string, compat.string_types): + msg = "a string needs to be passed, got type {typ}" + raise TypeError(msg.format(typ=type(string))) + + if (string.lower() == 'interval' or + cls._match.search(string) is not None): + return cls(string) - msg = "a string needs to be passed, got type {typ}" - raise TypeError(msg.format(typ=type(string))) + msg = ('Incorrectly formatted string passed to constructor. ' + 'Valid formats include Interval or Interval[dtype] ' + 'where dtype is numeric, datetime, or timedelta') + raise TypeError(msg) @property def type(self): @@ -979,7 +984,7 @@ def is_dtype(cls, dtype): return True else: return False - except ValueError: + except (ValueError, TypeError): return False else: return False diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index 0fe0a845f5129..71eaf504bdc46 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -501,10 +501,11 @@ def test_construction_not_supported(self, subtype): with pytest.raises(TypeError, match=msg): IntervalDtype(subtype) - def test_construction_errors(self): + @pytest.mark.parametrize('subtype', ['xx', 'IntervalA', 'Interval[foo]']) + def test_construction_errors(self, subtype): msg = 'could not construct IntervalDtype' with pytest.raises(TypeError, match=msg): - IntervalDtype('xx') + IntervalDtype(subtype) def test_construction_from_string(self): result = IntervalDtype('interval[int64]') @@ -513,7 +514,7 @@ def test_construction_from_string(self): assert is_dtype_equal(self.dtype, result) @pytest.mark.parametrize('string', [ - 'foo', 'foo[int64]', 0, 3.14, ('a', 'b'), None]) + 0, 3.14, ('a', 'b'), None]) def test_construction_from_string_errors(self, string): # these are invalid entirely msg = 'a string needs to be passed, got type' @@ -522,10 +523,12 @@ def test_construction_from_string_errors(self, string): IntervalDtype.construct_from_string(string) @pytest.mark.parametrize('string', [ - 'interval[foo]']) + 'foo', 'foo[int64]', 'IntervalA']) def test_construction_from_string_error_subtype(self, string): # this is an invalid subtype - msg = 'could not construct IntervalDtype' + msg = ("Incorrectly formatted string passed to constructor. " + r"Valid formats include Interval or Interval\[dtype\] " + "where dtype is numeric, datetime, or timedelta") with pytest.raises(TypeError, match=msg): IntervalDtype.construct_from_string(string) @@ -549,6 +552,7 @@ def test_is_dtype(self): assert not IntervalDtype.is_dtype('U') assert not IntervalDtype.is_dtype('S') assert not IntervalDtype.is_dtype('foo') + assert not IntervalDtype.is_dtype('IntervalA') assert not IntervalDtype.is_dtype(np.object_) assert not IntervalDtype.is_dtype(np.int64) assert not IntervalDtype.is_dtype(np.float64) diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index 4d3c9926fc5ae..b2aac441db195 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -563,6 +563,13 @@ def test_comp_ops_df_compat(self): with pytest.raises(ValueError, match=msg): left.to_frame() < right.to_frame() + def test_compare_series_interval_keyword(self): + # GH 25338 + s = Series(['IntervalA', 'IntervalB', 'IntervalC']) + result = s == 'IntervalA' + expected = Series([True, False, False]) + assert_series_equal(result, expected) + class TestSeriesFlexComparisonOps(object):