diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index 2ed2c21ba5584..e4c462a71921e 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -242,7 +242,7 @@ Strings Interval ^^^^^^^^ -- +- Construction of :class:`Interval` is restricted to numeric, :class:`Timestamp` and :class:`Timedelta` endpoints (:issue:`23013`) - - diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index e86b692e9915e..74fbd69708de8 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -20,8 +20,10 @@ cnp.import_array() cimport pandas._libs.util as util from pandas._libs.hashtable cimport Int64Vector, Int64VectorData +from pandas._libs.tslibs.util cimport is_integer_object, is_float_object from pandas._libs.tslibs import Timestamp +from pandas._libs.tslibs.timedeltas import Timedelta from pandas._libs.tslibs.timezones cimport tz_compare @@ -250,6 +252,10 @@ cdef class Interval(IntervalMixin): def __init__(self, left, right, str closed='right'): # note: it is faster to just do these checks than to use a special # constructor (__cinit__/__new__) to avoid them + + self._validate_endpoint(left) + self._validate_endpoint(right) + if closed not in _VALID_CLOSED: msg = "invalid option for 'closed': {closed}".format(closed=closed) raise ValueError(msg) @@ -266,6 +272,14 @@ cdef class Interval(IntervalMixin): self.right = right self.closed = closed + def _validate_endpoint(self, endpoint): + # GH 23013 + if not (is_integer_object(endpoint) or is_float_object(endpoint) or + isinstance(endpoint, (Timestamp, Timedelta))): + msg = ("Only numeric, Timestamp and Timedelta endpoints " + "are allowed when constructing an Interval.") + raise ValueError(msg) + def __hash__(self): return hash((self.left, self.right, self.closed)) diff --git a/pandas/tests/indexes/interval/test_construction.py b/pandas/tests/indexes/interval/test_construction.py index 483978b40fee0..a1e31455fe912 100644 --- a/pandas/tests/indexes/interval/test_construction.py +++ b/pandas/tests/indexes/interval/test_construction.py @@ -315,6 +315,12 @@ def test_generic_errors(self, constructor): """ pass + def test_constructor_string(self): + # GH23013 + # When forming the interval from breaks, + # the interval of strings is already forbidden. + pass + def test_constructor_errors(self, constructor): # mismatched closed within intervals with no constructor override ivs = [Interval(0, 1, closed='right'), Interval(2, 3, closed='left')] diff --git a/pandas/tests/scalar/interval/test_interval.py b/pandas/tests/scalar/interval/test_interval.py index 432f44725e2ba..56153aa4bf413 100644 --- a/pandas/tests/scalar/interval/test_interval.py +++ b/pandas/tests/scalar/interval/test_interval.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from pandas import Interval, Timedelta, Timestamp +from pandas import Interval, Period, Timedelta, Timestamp import pandas.core.common as com @@ -100,13 +100,14 @@ def test_length_timestamp(self, tz, left, right, expected): ('a', 'z'), (('a', 'b'), ('c', 'd')), (list('AB'), list('ab')), - (Interval(0, 1), Interval(1, 2))]) - def test_length_errors(self, left, right): - # GH 18789 - iv = Interval(left, right) - msg = 'cannot compute length between .* and .*' - with pytest.raises(TypeError, match=msg): - iv.length + (Interval(0, 1), Interval(1, 2)), + (Period('2018Q1', freq='Q'), Period('2018Q1', freq='Q')) + ]) + def test_construct_errors(self, left, right): + # GH 23013 + msg = "Only numeric, Timestamp and Timedelta endpoints are allowed" + with pytest.raises(ValueError, match=msg): + Interval(left, right) def test_math_add(self, closed): interval = Interval(0, 1, closed=closed)