1"""
2Module that contains many useful utilities
3for validating data or function arguments
4"""
5
6from __future__ import annotations
7
8from collections.abc import (
9 Iterable,
10 Sequence,
11)
12from typing import (
13 TYPE_CHECKING,
14 Any,
15 TypeVar,
16 overload,
17)
18
19import numpy as np
20
21from pandas._libs import lib
22from pandas._libs.missing import NA
23
24from pandas.core.dtypes.common import (
25 is_bool,
26 is_integer,
27)
28
29if TYPE_CHECKING:
30 from pandas._typing import DtypeBackend
31
32BoolishT = TypeVar("BoolishT", bool, int)
33BoolishNoneT = TypeVar("BoolishNoneT", bool, int, None)
34
35
36def _check_arg_length(
37 fname: str | None,
38 args: tuple[Any, ...],
39 max_fname_arg_count: int,
40 compat_args: dict[str, Any],
41) -> None:
42 """
43 Checks whether 'args' has length of at most 'compat_args'. Raises
44 a TypeError if that is not the case, similar to in Python when a
45 function is called with too many arguments.
46 """
47 if max_fname_arg_count < 0:
48 raise ValueError("'max_fname_arg_count' must be non-negative")
49
50 if len(args) > len(compat_args):
51 max_arg_count = len(compat_args) + max_fname_arg_count
52 actual_arg_count = len(args) + max_fname_arg_count
53 argument = "argument" if max_arg_count == 1 else "arguments"
54
55 raise TypeError(
56 f"{fname}() takes at most {max_arg_count} {argument} "
57 f"({actual_arg_count} given)"
58 )
59
60
61def _check_for_default_values(
62 fname: str | None,
63 arg_val_dict: dict[str, Any],
64 compat_args: dict[str, Any],
65) -> None:
66 """
67 Check that the keys in `arg_val_dict` are mapped to their
68 default values as specified in `compat_args`.
69
70 Note that this function is to be called only when it has been
71 checked that arg_val_dict.keys() is a subset of compat_args
72 """
73 for key, v1 in arg_val_dict.items():
74 v2 = compat_args[key]
75 # try checking equality directly with '=' operator,
76 # as comparison may have been overridden for the left
77 # hand object
78 try:
79 # check for None-ness otherwise we could end up
80 # comparing a numpy array vs None
81 if (v1 is not None and v2 is None) or (v1 is None and v2 is not None):
82 match = False
83 else:
84 match = v1 == v2
85
86 if not is_bool(match):
87 raise ValueError("'match' is not a boolean")
88
89 # could not compare them directly, so try comparison
90 # using the 'is' operator
91 except ValueError:
92 match = v1 is v2
93
94 if not match:
95 raise ValueError(
96 f"the '{key}' parameter is not supported in "
97 f"the pandas implementation of {fname}()"
98 )
99
100
101def validate_args(
102 fname: str | None,
103 args: tuple[Any, ...],
104 max_fname_arg_count: int,
105 compat_args: dict[str, Any],
106) -> None:
107 """
108 Checks whether the length of the `*args` argument passed into a function
109 has at most `len(compat_args)` arguments and whether or not all of these
110 elements in `args` are set to their default values.
111
112 Parameters
113 ----------
114 fname : str
115 The name of the function being passed the `*args` parameter
116 args : tuple
117 The `*args` parameter passed into a function
118 max_fname_arg_count : int
119 The maximum number of arguments that the function `fname`
120 can accept, excluding those in `args`. Used for displaying
121 appropriate error messages. Must be non-negative.
122 compat_args : dict
123 A dictionary of keys and their associated default values.
124 In order to accommodate buggy behaviour in some versions of `numpy`,
125 where a signature displayed keyword arguments but then passed those
126 arguments **positionally** internally when calling downstream
127 implementations, a dict ensures that the original
128 order of the keyword arguments is enforced.
129
130 Raises
131 ------
132 TypeError
133 If `args` contains more values than there are `compat_args`
134 ValueError
135 If `args` contains values that do not correspond to those
136 of the default values specified in `compat_args`
137 """
138 _check_arg_length(fname, args, max_fname_arg_count, compat_args)
139
140 # We do this so that we can provide a more informative
141 # error message about the parameters that we are not
142 # supporting in the pandas implementation of 'fname'
143 kwargs = dict(zip(compat_args, args, strict=False))
144 _check_for_default_values(fname, kwargs, compat_args)
145
146
147def _check_for_invalid_keys(
148 fname: str | None,
149 kwargs: dict[str, Any],
150 compat_args: dict[str, Any],
151) -> None:
152 """
153 Checks whether 'kwargs' contains any keys that are not
154 in 'compat_args' and raises a TypeError if there is one.
155 """
156 # set(dict) --> set of the dictionary's keys
157 diff = set(kwargs) - set(compat_args)
158
159 if diff:
160 bad_arg = next(iter(diff))
161 raise TypeError(f"{fname}() got an unexpected keyword argument '{bad_arg}'")
162
163
164def validate_kwargs(
165 fname: str | None,
166 kwargs: dict[str, Any],
167 compat_args: dict[str, Any],
168) -> None:
169 """
170 Checks whether parameters passed to the **kwargs argument in a
171 function `fname` are valid parameters as specified in `*compat_args`
172 and whether or not they are set to their default values.
173
174 Parameters
175 ----------
176 fname : str
177 The name of the function being passed the `**kwargs` parameter
178 kwargs : dict
179 The `**kwargs` parameter passed into `fname`
180 compat_args: dict
181 A dictionary of keys that `kwargs` is allowed to have and their
182 associated default values
183
184 Raises
185 ------
186 TypeError if `kwargs` contains keys not in `compat_args`
187 ValueError if `kwargs` contains keys in `compat_args` that do not
188 map to the default values specified in `compat_args`
189 """
190 kwds = kwargs.copy()
191 _check_for_invalid_keys(fname, kwargs, compat_args)
192 _check_for_default_values(fname, kwds, compat_args)
193
194
195def validate_args_and_kwargs(
196 fname: str | None,
197 args: tuple[Any, ...],
198 kwargs: dict[str, Any],
199 max_fname_arg_count: int,
200 compat_args: dict[str, Any],
201) -> None:
202 """
203 Checks whether parameters passed to the *args and **kwargs argument in a
204 function `fname` are valid parameters as specified in `*compat_args`
205 and whether or not they are set to their default values.
206
207 Parameters
208 ----------
209 fname: str
210 The name of the function being passed the `**kwargs` parameter
211 args: tuple
212 The `*args` parameter passed into a function
213 kwargs: dict
214 The `**kwargs` parameter passed into `fname`
215 max_fname_arg_count: int
216 The minimum number of arguments that the function `fname`
217 requires, excluding those in `args`. Used for displaying
218 appropriate error messages. Must be non-negative.
219 compat_args: dict
220 A dictionary of keys that `kwargs` is allowed to
221 have and their associated default values.
222
223 Raises
224 ------
225 TypeError if `args` contains more values than there are
226 `compat_args` OR `kwargs` contains keys not in `compat_args`
227 ValueError if `args` contains values not at the default value (`None`)
228 `kwargs` contains keys in `compat_args` that do not map to the default
229 value as specified in `compat_args`
230
231 See Also
232 --------
233 validate_args : Purely args validation.
234 validate_kwargs : Purely kwargs validation.
235
236 """
237 # Check that the total number of arguments passed in (i.e.
238 # args and kwargs) does not exceed the length of compat_args
239 _check_arg_length(
240 fname, args + tuple(kwargs.values()), max_fname_arg_count, compat_args
241 )
242
243 # Check there is no overlap with the positional and keyword
244 # arguments, similar to what is done in actual Python functions
245 args_dict = dict(zip(compat_args, args, strict=False))
246
247 for key in args_dict:
248 if key in kwargs:
249 raise TypeError(
250 f"{fname}() got multiple values for keyword argument '{key}'"
251 )
252
253 kwargs.update(args_dict)
254 validate_kwargs(fname, kwargs, compat_args)
255
256
257def validate_bool_kwarg(
258 value: BoolishNoneT,
259 arg_name: str,
260 none_allowed: bool = True,
261 int_allowed: bool = False,
262) -> BoolishNoneT:
263 """
264 Ensure that argument passed in arg_name can be interpreted as boolean.
265
266 Parameters
267 ----------
268 value : bool
269 Value to be validated.
270 arg_name : str
271 Name of the argument. To be reflected in the error message.
272 none_allowed : bool, default True
273 Whether to consider None to be a valid boolean.
274 int_allowed : bool, default False
275 Whether to consider integer value to be a valid boolean.
276
277 Returns
278 -------
279 value
280 The same value as input.
281
282 Raises
283 ------
284 ValueError
285 If the value is not a valid boolean.
286 """
287 good_value = is_bool(value)
288 if none_allowed:
289 good_value = good_value or (value is None)
290
291 if int_allowed:
292 good_value = good_value or isinstance(value, int)
293
294 if not good_value:
295 raise ValueError(
296 f'For argument "{arg_name}" expected type bool, received '
297 f"type {type(value).__name__}."
298 )
299 return value
300
301
302def validate_na_arg(value: object, name: str) -> None:
303 """
304 Validate na arguments.
305
306 Parameters
307 ----------
308 value : object
309 Value to validate.
310 name : str
311 Name of the argument, used to raise an informative error message.
312
313 Raises
314 ------
315 ValueError
316 When ``value`` is determined to be invalid.
317 """
318 if (
319 value is lib.no_default
320 or isinstance(value, bool)
321 or value is None
322 or value is NA
323 or (lib.is_float(value) and np.isnan(value))
324 ):
325 return
326 raise ValueError(f"{name} must be None, pd.NA, np.nan, True, or False; got {value}")
327
328
329def validate_percentile(q: float | Iterable[float]) -> np.ndarray:
330 """
331 Validate percentiles (used by describe and quantile).
332
333 This function checks if the given float or iterable of floats is a valid percentile
334 otherwise raises a ValueError.
335
336 Parameters
337 ----------
338 q: float or iterable of floats
339 A single percentile or an iterable of percentiles.
340
341 Returns
342 -------
343 ndarray
344 An ndarray of the percentiles if valid.
345
346 Raises
347 ------
348 ValueError if percentiles are not in given interval([0, 1]).
349 """
350 q_arr = np.asarray(q)
351 # Don't change this to an f-string. The string formatting
352 # is too expensive for cases where we don't need it.
353 msg = "percentiles should all be in the interval [0, 1]"
354 if q_arr.ndim == 0:
355 if not 0 <= q_arr <= 1:
356 raise ValueError(msg)
357 elif not all(0 <= qs <= 1 for qs in q_arr):
358 raise ValueError(msg)
359 return q_arr
360
361
362@overload
363def validate_ascending(ascending: BoolishT) -> BoolishT: ...
364
365
366@overload
367def validate_ascending(ascending: Sequence[BoolishT]) -> list[BoolishT]: ...
368
369
370def validate_ascending(
371 ascending: bool | int | Sequence[BoolishT],
372) -> bool | int | list[BoolishT]:
373 """Validate ``ascending`` kwargs for ``sort_index`` method."""
374 kwargs = {"none_allowed": False, "int_allowed": True}
375 if not isinstance(ascending, Sequence):
376 return validate_bool_kwarg(ascending, "ascending", **kwargs)
377
378 return [validate_bool_kwarg(item, "ascending", **kwargs) for item in ascending]
379
380
381def validate_endpoints(closed: str | None) -> tuple[bool, bool]:
382 """
383 Check that the `closed` argument is among [None, "left", "right"]
384
385 Parameters
386 ----------
387 closed : {None, "left", "right"}
388
389 Returns
390 -------
391 left_closed : bool
392 right_closed : bool
393
394 Raises
395 ------
396 ValueError : if argument is not among valid values
397 """
398 left_closed = False
399 right_closed = False
400
401 if closed is None:
402 left_closed = True
403 right_closed = True
404 elif closed == "left":
405 left_closed = True
406 elif closed == "right":
407 right_closed = True
408 else:
409 raise ValueError("Closed has to be either 'left', 'right' or None")
410
411 return left_closed, right_closed
412
413
414def validate_inclusive(inclusive: str | None) -> tuple[bool, bool]:
415 """
416 Check that the `inclusive` argument is among {"both", "neither", "left", "right"}.
417
418 Parameters
419 ----------
420 inclusive : {"both", "neither", "left", "right"}
421
422 Returns
423 -------
424 left_right_inclusive : tuple[bool, bool]
425
426 Raises
427 ------
428 ValueError : if argument is not among valid values
429 """
430 left_right_inclusive: tuple[bool, bool] | None = None
431
432 if isinstance(inclusive, str):
433 left_right_inclusive = {
434 "both": (True, True),
435 "left": (True, False),
436 "right": (False, True),
437 "neither": (False, False),
438 }.get(inclusive)
439
440 if left_right_inclusive is None:
441 raise ValueError(
442 "Inclusive has to be either 'both', 'neither', 'left' or 'right'"
443 )
444
445 return left_right_inclusive
446
447
448def validate_insert_loc(loc: int, length: int) -> int:
449 """
450 Check that we have an integer between -length and length, inclusive.
451
452 Standardize negative loc to within [0, length].
453
454 The exceptions we raise on failure match np.insert.
455 """
456 if not is_integer(loc):
457 raise TypeError(f"loc must be an integer between -{length} and {length}")
458
459 if loc < 0:
460 loc += length
461 if not 0 <= loc <= length:
462 raise IndexError(f"loc must be an integer between -{length} and {length}")
463 return loc # pyright: ignore[reportReturnType]
464
465
466def check_dtype_backend(dtype_backend: DtypeBackend | lib.NoDefault) -> None:
467 if dtype_backend is not lib.no_default:
468 if dtype_backend not in ["numpy_nullable", "pyarrow"]:
469 raise ValueError(
470 f"dtype_backend {dtype_backend} is invalid, only 'numpy_nullable' and "
471 f"'pyarrow' are allowed.",
472 )