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