Skip to content

Commit 7277952

Browse files
authored
Refactor/validate utils (pandas-dev#43551)
1 parent 9f90bd4 commit 7277952

File tree

7 files changed

+125
-54
lines changed

7 files changed

+125
-54
lines changed

pandas/core/arrays/datetimelike.py

-33
Original file line numberDiff line numberDiff line change
@@ -1823,39 +1823,6 @@ def validate_periods(periods):
18231823
return periods
18241824

18251825

1826-
def validate_endpoints(closed):
1827-
"""
1828-
Check that the `closed` argument is among [None, "left", "right"]
1829-
1830-
Parameters
1831-
----------
1832-
closed : {None, "left", "right"}
1833-
1834-
Returns
1835-
-------
1836-
left_closed : bool
1837-
right_closed : bool
1838-
1839-
Raises
1840-
------
1841-
ValueError : if argument is not among valid values
1842-
"""
1843-
left_closed = False
1844-
right_closed = False
1845-
1846-
if closed is None:
1847-
left_closed = True
1848-
right_closed = True
1849-
elif closed == "left":
1850-
left_closed = True
1851-
elif closed == "right":
1852-
right_closed = True
1853-
else:
1854-
raise ValueError("Closed has to be either 'left', 'right' or None")
1855-
1856-
return left_closed, right_closed
1857-
1858-
18591826
def validate_inferred_freq(freq, inferred_freq, freq_infer):
18601827
"""
18611828
If the user passes a freq and another freq is inferred from passed data,

pandas/core/arrays/datetimes.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
)
4040
from pandas._typing import npt
4141
from pandas.errors import PerformanceWarning
42+
from pandas.util._validators import validate_endpoints
4243

4344
from pandas.core.dtypes.cast import astype_dt64_to_dt64tz
4445
from pandas.core.dtypes.common import (
@@ -416,7 +417,7 @@ def _generate_range(
416417
if start is NaT or end is NaT:
417418
raise ValueError("Neither `start` nor `end` can be NaT")
418419

419-
left_closed, right_closed = dtl.validate_endpoints(closed)
420+
left_closed, right_closed = validate_endpoints(closed)
420421
start, end, _normalized = _maybe_normalize_endpoints(start, end, normalize)
421422
tz = _infer_tz_from_endpoints(start, end, tz)
422423

pandas/core/arrays/timedeltas.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
NpDtype,
3737
)
3838
from pandas.compat.numpy import function as nv
39+
from pandas.util._validators import validate_endpoints
3940

4041
from pandas.core.dtypes.cast import astype_td64_unit_conversion
4142
from pandas.core.dtypes.common import (
@@ -312,7 +313,7 @@ def _generate_range(cls, start, end, periods, freq, closed=None):
312313
if end is not None:
313314
end = Timedelta(end)
314315

315-
left_closed, right_closed = dtl.validate_endpoints(closed)
316+
left_closed, right_closed = validate_endpoints(closed)
316317

317318
if freq is not None:
318319
index = generate_regular_range(start, end, periods, freq)

pandas/core/generic.py

+13-15
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
validate_ascending,
7373
validate_bool_kwarg,
7474
validate_fillna_kwargs,
75+
validate_inclusive,
7576
)
7677

7778
from pandas.core.dtypes.common import (
@@ -7694,16 +7695,18 @@ def between_time(
76947695
if not isinstance(index, DatetimeIndex):
76957696
raise TypeError("Index must be DatetimeIndex")
76967697

7697-
if (include_start != lib.no_default or include_end != lib.no_default) and (
7698-
inclusive is not None
7699-
):
7698+
old_include_arg_used = (include_start != lib.no_default) or (
7699+
include_end != lib.no_default
7700+
)
7701+
7702+
if old_include_arg_used and inclusive is not None:
77007703
raise ValueError(
77017704
"Deprecated arguments `include_start` and `include_end` "
77027705
"cannot be passed if `inclusive` has been given."
77037706
)
77047707
# If any of the deprecated arguments ('include_start', 'include_end')
77057708
# have been passed
7706-
elif (include_start != lib.no_default) or (include_end != lib.no_default):
7709+
elif old_include_arg_used:
77077710
warnings.warn(
77087711
"`include_start` and `include_end` are deprecated in "
77097712
"favour of `inclusive`.",
@@ -7720,20 +7723,15 @@ def between_time(
77207723
(False, False): "neither",
77217724
}
77227725
inclusive = inc_dict[(left, right)]
7723-
else: # On arg removal inclusive can default to "both"
7724-
if inclusive is None:
7725-
inclusive = "both"
7726-
elif inclusive not in ["both", "neither", "left", "right"]:
7727-
raise ValueError(
7728-
f"Inclusive has to be either string of 'both', "
7729-
f"'left', 'right', or 'neither'. Got {inclusive}."
7730-
)
7731-
7726+
elif inclusive is None:
7727+
# On arg removal inclusive can default to "both"
7728+
inclusive = "both"
7729+
left_inclusive, right_inclusive = validate_inclusive(inclusive)
77327730
indexer = index.indexer_between_time(
77337731
start_time,
77347732
end_time,
7735-
include_start=inclusive in ["both", "left"],
7736-
include_end=inclusive in ["both", "right"],
7733+
include_start=left_inclusive,
7734+
include_end=right_inclusive,
77377735
)
77387736
return self._take_with_is_copy(indexer, axis=axis)
77397737

pandas/tests/frame/methods/test_between_time.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,7 @@ def test_between_time_incorr_arg_inclusive(self):
236236
stime = time(0, 0)
237237
etime = time(1, 0)
238238
inclusive = "bad_string"
239-
msg = (
240-
"Inclusive has to be either string of 'both', 'left', 'right', "
241-
"or 'neither'. Got bad_string."
242-
)
239+
msg = "Inclusive has to be either 'both', 'neither', 'left' or 'right'"
243240
with pytest.raises(ValueError, match=msg):
244241
ts.between_time(stime, etime, inclusive=inclusive)
245242

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import numpy as np
2+
import pytest
3+
4+
from pandas.util._validators import validate_inclusive
5+
6+
import pandas as pd
7+
8+
9+
@pytest.mark.parametrize(
10+
"invalid_inclusive",
11+
(
12+
"ccc",
13+
2,
14+
object(),
15+
None,
16+
np.nan,
17+
pd.NA,
18+
pd.DataFrame(),
19+
),
20+
)
21+
def test_invalid_inclusive(invalid_inclusive):
22+
with pytest.raises(
23+
ValueError,
24+
match="Inclusive has to be either 'both', 'neither', 'left' or 'right'",
25+
):
26+
validate_inclusive(invalid_inclusive)
27+
28+
29+
@pytest.mark.parametrize(
30+
"valid_inclusive, expected_tuple",
31+
(
32+
("left", (True, False)),
33+
("right", (False, True)),
34+
("both", (True, True)),
35+
("neither", (False, False)),
36+
),
37+
)
38+
def test_valid_inclusive(valid_inclusive, expected_tuple):
39+
resultant_tuple = validate_inclusive(valid_inclusive)
40+
assert expected_tuple == resultant_tuple

pandas/util/_validators.py

+67
Original file line numberDiff line numberDiff line change
@@ -427,3 +427,70 @@ def validate_ascending(
427427
return validate_bool_kwarg(ascending, "ascending", **kwargs)
428428

429429
return [validate_bool_kwarg(item, "ascending", **kwargs) for item in ascending]
430+
431+
432+
def validate_endpoints(closed: str | None) -> tuple[bool, bool]:
433+
"""
434+
Check that the `closed` argument is among [None, "left", "right"]
435+
436+
Parameters
437+
----------
438+
closed : {None, "left", "right"}
439+
440+
Returns
441+
-------
442+
left_closed : bool
443+
right_closed : bool
444+
445+
Raises
446+
------
447+
ValueError : if argument is not among valid values
448+
"""
449+
left_closed = False
450+
right_closed = False
451+
452+
if closed is None:
453+
left_closed = True
454+
right_closed = True
455+
elif closed == "left":
456+
left_closed = True
457+
elif closed == "right":
458+
right_closed = True
459+
else:
460+
raise ValueError("Closed has to be either 'left', 'right' or None")
461+
462+
return left_closed, right_closed
463+
464+
465+
def validate_inclusive(inclusive: str | None) -> tuple[bool, bool]:
466+
"""
467+
Check that the `inclusive` argument is among {"both", "neither", "left", "right"}.
468+
469+
Parameters
470+
----------
471+
inclusive : {"both", "neither", "left", "right"}
472+
473+
Returns
474+
-------
475+
left_right_inclusive : tuple[bool, bool]
476+
477+
Raises
478+
------
479+
ValueError : if argument is not among valid values
480+
"""
481+
left_right_inclusive: tuple[bool, bool] | None = None
482+
483+
if isinstance(inclusive, str):
484+
left_right_inclusive = {
485+
"both": (True, True),
486+
"left": (True, False),
487+
"right": (False, True),
488+
"neither": (False, False),
489+
}.get(inclusive)
490+
491+
if left_right_inclusive is None:
492+
raise ValueError(
493+
"Inclusive has to be either 'both', 'neither', 'left' or 'right'"
494+
)
495+
496+
return left_right_inclusive

0 commit comments

Comments
 (0)