From 9b3b4c754d57ed6d1d1c45e4bae2d65db1214860 Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 10 Jun 2020 10:47:29 -0700 Subject: [PATCH 1/4] CLN: disallow tuple in to_offset --- pandas/_libs/tslibs/offsets.pyx | 13 +++++-------- pandas/tests/indexes/datetimes/test_constructors.py | 6 ++---- pandas/tests/indexes/period/test_constructors.py | 7 ++----- pandas/tests/indexes/period/test_period.py | 6 ------ pandas/tests/scalar/period/test_period.py | 5 +++-- pandas/tests/tslibs/test_to_offset.py | 9 +++++++-- 6 files changed, 19 insertions(+), 27 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 4069d192d9e88..c809c33bb6139 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -3583,10 +3583,10 @@ cpdef to_offset(freq): >>> to_offset("1D1H") <25 * Hours> - >>> to_offset(("W", 2)) + >>> to_offset("2W") <2 * Weeks: weekday=6> - >>> to_offset((2, "B")) + >>> to_offset("2B") <2 * BusinessDays> >>> to_offset(pd.Timedelta(days=1)) @@ -3602,12 +3602,9 @@ cpdef to_offset(freq): return freq if isinstance(freq, tuple): - name = freq[0] - stride = freq[1] - if isinstance(stride, str): - name, stride = stride, name - name, _ = base_and_stride(name) - delta = _get_offset(name) * stride + raise TypeError( + f"to_offset does not support tuples {freq}, pass as a string instead" + ) elif isinstance(freq, timedelta): return delta_to_tick(freq) diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py index b15549839de03..33da2eb12bea7 100644 --- a/pandas/tests/indexes/datetimes/test_constructors.py +++ b/pandas/tests/indexes/datetimes/test_constructors.py @@ -946,10 +946,8 @@ def test_datetimeindex_constructor_misc(self): assert idx[0] == sdate + 0 * offsets.BDay() assert idx.freq == "B" - idx = date_range(end=edate, freq=("D", 5), periods=20) - assert len(idx) == 20 - assert idx[-1] == edate - assert idx.freq == "5D" + with pytest.raises(TypeError, match="pass as a string instead"): + date_range(end=edate, freq=("D", 5), periods=20) idx1 = date_range(start=sdate, end=edate, freq="W-SUN") idx2 = date_range(start=sdate, end=edate, freq=offsets.Week(weekday=6)) diff --git a/pandas/tests/indexes/period/test_constructors.py b/pandas/tests/indexes/period/test_constructors.py index 4ec7ef64e2272..2cf4e920d7b9e 100644 --- a/pandas/tests/indexes/period/test_constructors.py +++ b/pandas/tests/indexes/period/test_constructors.py @@ -463,11 +463,8 @@ def test_constructor(self): assert (i1 == i2).all() assert i1.freq == i2.freq - end_intv = Period("2006-12-31", ("w", 1)) - i2 = period_range(end=end_intv, periods=10) - assert len(i1) == len(i2) - assert (i1 == i2).all() - assert i1.freq == i2.freq + with pytest.raises(TypeError, match="pass as a string instead"): + Period("2006-12-31", ("w", 1)) end_intv = Period("2005-05-01", "B") i1 = period_range(start=start, end=end_intv) diff --git a/pandas/tests/indexes/period/test_period.py b/pandas/tests/indexes/period/test_period.py index d247d6571f5d0..47617802be11c 100644 --- a/pandas/tests/indexes/period/test_period.py +++ b/pandas/tests/indexes/period/test_period.py @@ -172,12 +172,6 @@ def test_period_index_length(self): assert (i1 == i2).all() assert i1.freq == i2.freq - end_intv = Period("2006-12-31", ("w", 1)) - i2 = period_range(end=end_intv, periods=10) - assert len(i1) == len(i2) - assert (i1 == i2).all() - assert i1.freq == i2.freq - msg = "start and end must have same freq" with pytest.raises(ValueError, match=msg): period_range(start=start, end=end_intv) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index 702899f163e06..576b32629cdfa 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -48,8 +48,9 @@ def test_construction(self): i1 = Period("1982", freq="min") i2 = Period("1982", freq="MIN") assert i1 == i2 - i2 = Period("1982", freq=("Min", 1)) - assert i1 == i2 + + with pytest.raises(TypeError, match="pass as a string instead"): + Period("1982", freq=("Min", 1)) i1 = Period(year=2005, month=3, day=1, freq="D") i2 = Period("3/1/2005", freq="D") diff --git a/pandas/tests/tslibs/test_to_offset.py b/pandas/tests/tslibs/test_to_offset.py index 04be0e445a3b2..93e5e2c801c09 100644 --- a/pandas/tests/tslibs/test_to_offset.py +++ b/pandas/tests/tslibs/test_to_offset.py @@ -10,7 +10,6 @@ [ (to_offset("10us"), offsets.Micro(10)), (offsets.Hour(), offsets.Hour()), - ((5, "T"), offsets.Minute(5)), ("2h30min", offsets.Minute(150)), ("2h 30min", offsets.Minute(150)), ("2h30min15s", offsets.Second(150 * 60 + 15)), @@ -89,10 +88,16 @@ def test_to_offset_invalid(freqstr): def test_to_offset_no_evaluate(): - with pytest.raises(ValueError, match="Could not evaluate"): + msg = str(("", "")) + with pytest.raises(TypeError, match=msg): to_offset(("", "")) +def test_to_offset_tuple_unsupported(): + with pytest.raises(TypeError, match="pass as a string instead"): + to_offset((5, "T")) + + @pytest.mark.parametrize( "freqstr,expected", [ From 4c970cfee2cc663defd17911f2f607f8b82081c9 Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 10 Jun 2020 16:45:22 -0700 Subject: [PATCH 2/4] whatsnew --- doc/source/whatsnew/v1.1.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 92f7c0f6b59a3..c270b06ff2614 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -818,6 +818,7 @@ Datetimelike - Bug in :meth:`DatetimeIndex.intersection` and :meth:`TimedeltaIndex.intersection` with results not having the correct ``name`` attribute (:issue:`33904`) - Bug in :meth:`DatetimeArray.__setitem__`, :meth:`TimedeltaArray.__setitem__`, :meth:`PeriodArray.__setitem__` incorrectly allowing values with ``int64`` dtype to be silently cast (:issue:`33717`) - Bug in subtracting :class:`TimedeltaIndex` from :class:`Period` incorrectly raising ``TypeError`` in some cases where it should succeed and ``IncompatibleFrequency`` in some cases where it should raise ``TypeError`` (:issue:`33883`) +- The ``freq`` keyword in :class:`Period`, :func:`date_range`, :func:`period_range`, :func:`pd.tseries.frequencies.to_offset` no longer allows tuples, pass as string instead (:issue:`34703`) Timedelta ^^^^^^^^^ From 23964b69fd4581dc2176abc8a98036f1e267cf86 Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 14 Jun 2020 16:18:38 -0700 Subject: [PATCH 3/4] REF: collect tests --- pandas/tests/indexes/datetimes/test_constructors.py | 9 ++++++--- pandas/tests/indexes/period/test_constructors.py | 7 ++++--- pandas/tests/scalar/period/test_period.py | 7 ++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py index 33da2eb12bea7..c150e7901c86a 100644 --- a/pandas/tests/indexes/datetimes/test_constructors.py +++ b/pandas/tests/indexes/datetimes/test_constructors.py @@ -946,9 +946,6 @@ def test_datetimeindex_constructor_misc(self): assert idx[0] == sdate + 0 * offsets.BDay() assert idx.freq == "B" - with pytest.raises(TypeError, match="pass as a string instead"): - date_range(end=edate, freq=("D", 5), periods=20) - idx1 = date_range(start=sdate, end=edate, freq="W-SUN") idx2 = date_range(start=sdate, end=edate, freq=offsets.Week(weekday=6)) assert len(idx1) == len(idx2) @@ -977,6 +974,12 @@ def test_pass_datetimeindex_to_index(self): tm.assert_numpy_array_equal(idx.values, expected.values) + def test_date_range_tuple_freq_raises(self): + # GH#34703 + edate = datetime(2000, 1, 1) + with pytest.raises(TypeError, match="pass as a string instead"): + date_range(end=edate, freq=("D", 5), periods=20) + def test_timestamp_constructor_invalid_fold_raise(): # Test for #25057 diff --git a/pandas/tests/indexes/period/test_constructors.py b/pandas/tests/indexes/period/test_constructors.py index 2cf4e920d7b9e..f85f37e4127c3 100644 --- a/pandas/tests/indexes/period/test_constructors.py +++ b/pandas/tests/indexes/period/test_constructors.py @@ -463,9 +463,6 @@ def test_constructor(self): assert (i1 == i2).all() assert i1.freq == i2.freq - with pytest.raises(TypeError, match="pass as a string instead"): - Period("2006-12-31", ("w", 1)) - end_intv = Period("2005-05-01", "B") i1 = period_range(start=start, end=end_intv) @@ -487,6 +484,10 @@ def test_constructor(self): with pytest.raises(IncompatibleFrequency, match=msg): PeriodIndex(vals) + # tuple freq disallowed GH#34703 + with pytest.raises(TypeError, match="pass as a string instead"): + Period("2006-12-31", ("w", 1)) + @pytest.mark.parametrize( "freq", ["M", "Q", "A", "D", "B", "T", "S", "L", "U", "N", "H"] ) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index 576b32629cdfa..dcef0615121c1 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -49,9 +49,6 @@ def test_construction(self): i2 = Period("1982", freq="MIN") assert i1 == i2 - with pytest.raises(TypeError, match="pass as a string instead"): - Period("1982", freq=("Min", 1)) - i1 = Period(year=2005, month=3, day=1, freq="D") i2 = Period("3/1/2005", freq="D") assert i1 == i2 @@ -81,6 +78,10 @@ def test_construction(self): with pytest.raises(ValueError, match=msg): Period("2007-1-1", freq="X") + # GH#34703 tuple freq disallowed + with pytest.raises(TypeError, match="pass as a string instead"): + Period("1982", freq=("Min", 1)) + def test_construction_bday(self): # Biz day construction, roll forward if non-weekday From 51b655112c56f91d615f53597c8c42bcae1ccb04 Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 14 Jun 2020 16:32:53 -0700 Subject: [PATCH 4/4] post-rebase fixup --- pandas/_libs/tslibs/offsets.pyx | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 8971bf813d1b8..c6ab9188089e7 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -3482,36 +3482,6 @@ INVALID_FREQ_ERR_MSG = "Invalid frequency: {0}" _offset_map = {} -cdef _base_and_stride(str freqstr): - """ - Return base freq and stride info from string representation - - Returns - ------- - base : str - stride : int - - Examples - -------- - _base_and_stride('5Min') -> 'Min', 5 - """ - groups = opattern.match(freqstr) - - if not groups: - raise ValueError(f"Could not evaluate {freqstr}") - - stride = groups.group(1) - - if len(stride): - stride = int(stride) - else: - stride = 1 - - base = groups.group(2) - - return base, stride - - # TODO: better name? def _get_offset(name: str) -> BaseOffset: """