1"""
2For compatibility with numpy libraries, pandas functions or methods have to
3accept '*args' and '**kwargs' parameters to accommodate numpy arguments that
4are not actually used or respected in the pandas implementation.
5
6To ensure that users do not abuse these parameters, validation is performed in
7'validators.py' to make sure that any extra parameters passed correspond ONLY
8to those in the numpy signature. Part of that validation includes whether or
9not the user attempted to pass in non-default values for these extraneous
10parameters. As we want to discourage users from relying on these parameters
11when calling the pandas implementation, we want them only to pass in the
12default values for these parameters.
13
14This module provides a set of commonly used default arguments for functions and
15methods that are spread throughout the codebase. This module will make it
16easier to adjust to future upstream changes in the analogous numpy signatures.
17"""
18
19from __future__ import annotations
20
21from typing import (
22 TYPE_CHECKING,
23 Any,
24 TypeVar,
25 cast,
26 overload,
27)
28
29import numpy as np
30from numpy import ndarray
31
32from pandas._libs.lib import (
33 is_bool,
34 is_integer,
35)
36from pandas.errors import UnsupportedFunctionCall
37from pandas.util._validators import (
38 validate_args,
39 validate_args_and_kwargs,
40 validate_kwargs,
41)
42
43if TYPE_CHECKING:
44 from pandas._typing import (
45 Axis,
46 AxisInt,
47 )
48
49 AxisNoneT = TypeVar("AxisNoneT", Axis, None)
50
51
52class CompatValidator:
53 def __init__(
54 self,
55 defaults: dict[str, Any],
56 fname: str | None = None,
57 method: str | None = None,
58 max_fname_arg_count: int | None = None,
59 ) -> None:
60 self.fname = fname
61 self.method = method
62 self.defaults = defaults
63 self.max_fname_arg_count = max_fname_arg_count
64
65 def __call__(
66 self,
67 args: tuple[Any, ...],
68 kwargs: dict[str, Any],
69 fname: str | None = None,
70 max_fname_arg_count: int | None = None,
71 method: str | None = None,
72 ) -> None:
73 if not args and not kwargs:
74 return None
75
76 fname = self.fname if fname is None else fname
77 max_fname_arg_count = (
78 self.max_fname_arg_count
79 if max_fname_arg_count is None
80 else max_fname_arg_count
81 )
82 method = self.method if method is None else method
83
84 if method == "args":
85 validate_args(fname, args, cast("int", max_fname_arg_count), self.defaults)
86 elif method == "kwargs":
87 validate_kwargs(fname, kwargs, self.defaults)
88 elif method == "both":
89 validate_args_and_kwargs(
90 fname, args, kwargs, cast("int", max_fname_arg_count), self.defaults
91 )
92 else:
93 raise ValueError(f"invalid validation method '{method}'")
94
95
96ARGMINMAX_DEFAULTS = {"out": None}
97validate_argmin = CompatValidator(
98 ARGMINMAX_DEFAULTS, fname="argmin", method="both", max_fname_arg_count=1
99)
100validate_argmax = CompatValidator(
101 ARGMINMAX_DEFAULTS, fname="argmax", method="both", max_fname_arg_count=1
102)
103
104
105def process_skipna(
106 skipna: bool | ndarray | None, args: tuple[Any, ...]
107) -> tuple[bool, tuple[Any, ...]]:
108 if isinstance(skipna, ndarray) or skipna is None:
109 args = (skipna, *args)
110 skipna = True
111
112 return skipna, args
113
114
115def validate_argmax_with_skipna(
116 skipna: bool | ndarray | None, args: tuple[Any, ...], kwargs: dict[str, Any]
117) -> bool:
118 """
119 If 'Series.argmax' is called via the 'numpy' library, the third parameter
120 in its signature is 'out', which takes either an ndarray or 'None', so
121 check if the 'skipna' parameter is either an instance of ndarray or is
122 None, since 'skipna' itself should be a boolean
123 """
124 skipna, args = process_skipna(skipna, args)
125 validate_argmax(args, kwargs)
126 return skipna
127
128
129ARGSORT_DEFAULTS: dict[str, int | str | None] = {}
130ARGSORT_DEFAULTS["axis"] = -1
131ARGSORT_DEFAULTS["kind"] = "quicksort"
132ARGSORT_DEFAULTS["order"] = None
133ARGSORT_DEFAULTS["kind"] = None
134ARGSORT_DEFAULTS["stable"] = None
135
136
137validate_argsort = CompatValidator(
138 ARGSORT_DEFAULTS, fname="argsort", max_fname_arg_count=0, method="both"
139)
140
141# two different signatures of argsort, this second validation for when the
142# `kind` param is supported
143ARGSORT_DEFAULTS_KIND: dict[str, int | None] = {}
144ARGSORT_DEFAULTS_KIND["axis"] = -1
145ARGSORT_DEFAULTS_KIND["order"] = None
146ARGSORT_DEFAULTS_KIND["stable"] = None
147validate_argsort_kind = CompatValidator(
148 ARGSORT_DEFAULTS_KIND, fname="argsort", max_fname_arg_count=0, method="both"
149)
150
151
152def validate_argsort_with_ascending(
153 ascending: bool | int | None, args: tuple[Any, ...], kwargs: dict[str, Any]
154) -> bool:
155 """
156 If 'Categorical.argsort' is called via the 'numpy' library, the first
157 parameter in its signature is 'axis', which takes either an integer or
158 'None', so check if the 'ascending' parameter has either integer type or is
159 None, since 'ascending' itself should be a boolean
160 """
161 if is_integer(ascending) or ascending is None:
162 args = (ascending, *args)
163 ascending = True
164
165 validate_argsort_kind(args, kwargs, max_fname_arg_count=3)
166 ascending = cast("bool", ascending)
167 return ascending
168
169
170CLIP_DEFAULTS: dict[str, Any] = {"out": None}
171validate_clip = CompatValidator(
172 CLIP_DEFAULTS, fname="clip", method="both", max_fname_arg_count=3
173)
174
175
176@overload
177def validate_clip_with_axis(
178 axis: ndarray, args: tuple[Any, ...], kwargs: dict[str, Any]
179) -> None: ...
180
181
182@overload
183def validate_clip_with_axis(
184 axis: AxisNoneT, args: tuple[Any, ...], kwargs: dict[str, Any]
185) -> AxisNoneT: ...
186
187
188def validate_clip_with_axis(
189 axis: ndarray | AxisNoneT, args: tuple[Any, ...], kwargs: dict[str, Any]
190) -> AxisNoneT | None:
191 """
192 If 'NDFrame.clip' is called via the numpy library, the third parameter in
193 its signature is 'out', which can takes an ndarray, so check if the 'axis'
194 parameter is an instance of ndarray, since 'axis' itself should either be
195 an integer or None
196 """
197 if isinstance(axis, ndarray):
198 args = (axis, *args)
199 # error: Incompatible types in assignment (expression has type "None",
200 # variable has type "Union[ndarray[Any, Any], str, int]")
201 axis = None # type: ignore[assignment]
202
203 validate_clip(args, kwargs)
204 # error: Incompatible return value type (got "Union[ndarray[Any, Any],
205 # str, int]", expected "Union[str, int, None]")
206 return axis # type: ignore[return-value]
207
208
209CUM_FUNC_DEFAULTS: dict[str, Any] = {}
210CUM_FUNC_DEFAULTS["dtype"] = None
211CUM_FUNC_DEFAULTS["out"] = None
212validate_cum_func = CompatValidator(
213 CUM_FUNC_DEFAULTS, method="both", max_fname_arg_count=1
214)
215validate_cumsum = CompatValidator(
216 CUM_FUNC_DEFAULTS, fname="cumsum", method="both", max_fname_arg_count=1
217)
218
219
220def validate_cum_func_with_skipna(
221 skipna: bool, args: tuple[Any, ...], kwargs: dict[str, Any], name: str
222) -> bool:
223 """
224 If this function is called via the 'numpy' library, the third parameter in
225 its signature is 'dtype', which takes either a 'numpy' dtype or 'None', so
226 check if the 'skipna' parameter is a boolean or not
227 """
228 if not is_bool(skipna):
229 args = (skipna, *args)
230 skipna = True
231 elif isinstance(skipna, np.bool_):
232 skipna = bool(skipna)
233
234 validate_cum_func(args, kwargs, fname=name)
235 return skipna
236
237
238ALLANY_DEFAULTS: dict[str, bool | None] = {}
239ALLANY_DEFAULTS["dtype"] = None
240ALLANY_DEFAULTS["out"] = None
241ALLANY_DEFAULTS["keepdims"] = False
242ALLANY_DEFAULTS["axis"] = None
243validate_all = CompatValidator(
244 ALLANY_DEFAULTS, fname="all", method="both", max_fname_arg_count=1
245)
246validate_any = CompatValidator(
247 ALLANY_DEFAULTS, fname="any", method="both", max_fname_arg_count=1
248)
249
250LOGICAL_FUNC_DEFAULTS = {"out": None, "keepdims": False}
251validate_logical_func = CompatValidator(LOGICAL_FUNC_DEFAULTS, method="kwargs")
252
253MINMAX_DEFAULTS = {"axis": None, "dtype": None, "out": None, "keepdims": False}
254validate_min = CompatValidator(
255 MINMAX_DEFAULTS, fname="min", method="both", max_fname_arg_count=1
256)
257validate_max = CompatValidator(
258 MINMAX_DEFAULTS, fname="max", method="both", max_fname_arg_count=1
259)
260
261
262REPEAT_DEFAULTS: dict[str, Any] = {"axis": None}
263validate_repeat = CompatValidator(
264 REPEAT_DEFAULTS, fname="repeat", method="both", max_fname_arg_count=1
265)
266
267ROUND_DEFAULTS: dict[str, Any] = {"out": None}
268validate_round = CompatValidator(
269 ROUND_DEFAULTS, fname="round", method="both", max_fname_arg_count=1
270)
271
272STAT_FUNC_DEFAULTS: dict[str, Any | None] = {}
273STAT_FUNC_DEFAULTS["dtype"] = None
274STAT_FUNC_DEFAULTS["out"] = None
275
276SUM_DEFAULTS = STAT_FUNC_DEFAULTS.copy()
277SUM_DEFAULTS["axis"] = None
278SUM_DEFAULTS["keepdims"] = False
279SUM_DEFAULTS["initial"] = None
280
281PROD_DEFAULTS = SUM_DEFAULTS.copy()
282
283MEAN_DEFAULTS = SUM_DEFAULTS.copy()
284
285MEDIAN_DEFAULTS = STAT_FUNC_DEFAULTS.copy()
286MEDIAN_DEFAULTS["overwrite_input"] = False
287MEDIAN_DEFAULTS["keepdims"] = False
288
289STAT_FUNC_DEFAULTS["keepdims"] = False
290
291validate_stat_func = CompatValidator(STAT_FUNC_DEFAULTS, method="kwargs")
292validate_sum = CompatValidator(
293 SUM_DEFAULTS, fname="sum", method="both", max_fname_arg_count=1
294)
295validate_prod = CompatValidator(
296 PROD_DEFAULTS, fname="prod", method="both", max_fname_arg_count=1
297)
298validate_mean = CompatValidator(
299 MEAN_DEFAULTS, fname="mean", method="both", max_fname_arg_count=1
300)
301validate_median = CompatValidator(
302 MEDIAN_DEFAULTS, fname="median", method="both", max_fname_arg_count=1
303)
304
305STAT_DDOF_FUNC_DEFAULTS: dict[str, bool | None] = {}
306STAT_DDOF_FUNC_DEFAULTS["dtype"] = None
307STAT_DDOF_FUNC_DEFAULTS["out"] = None
308STAT_DDOF_FUNC_DEFAULTS["keepdims"] = False
309validate_stat_ddof_func = CompatValidator(STAT_DDOF_FUNC_DEFAULTS, method="kwargs")
310
311TAKE_DEFAULTS: dict[str, str | None] = {}
312TAKE_DEFAULTS["out"] = None
313TAKE_DEFAULTS["mode"] = "raise"
314validate_take = CompatValidator(TAKE_DEFAULTS, fname="take", method="kwargs")
315
316
317TRANSPOSE_DEFAULTS = {"axes": None}
318validate_transpose = CompatValidator(
319 TRANSPOSE_DEFAULTS, fname="transpose", method="both", max_fname_arg_count=0
320)
321
322
323def validate_groupby_func(
324 name: str,
325 args: tuple[Any, ...],
326 kwargs: dict[str, Any],
327 allowed: list[str] | None = None,
328) -> None:
329 """
330 'args' and 'kwargs' should be empty, except for allowed kwargs because all
331 of their necessary parameters are explicitly listed in the function
332 signature
333 """
334 if allowed is None:
335 allowed = []
336
337 extra_kwargs = set(kwargs) - set(allowed)
338
339 if len(args) + len(extra_kwargs) > 0:
340 raise UnsupportedFunctionCall(
341 "numpy operations are not valid with groupby. "
342 f"Use .groupby(...).{name}() instead"
343 )
344
345
346def validate_minmax_axis(axis: AxisInt | None, ndim: int = 1) -> None:
347 """
348 Ensure that the axis argument passed to min, max, argmin, or argmax is zero
349 or None, as otherwise it will be incorrectly ignored.
350
351 Parameters
352 ----------
353 axis : int or None
354 ndim : int, default 1
355
356 Raises
357 ------
358 ValueError
359 """
360 if axis is None:
361 return
362 if axis >= ndim or (axis < 0 and ndim + axis < 0):
363 raise ValueError(f"`axis` must be fewer than the number of dimensions ({ndim})")
364
365
366_validation_funcs = {
367 "median": validate_median,
368 "mean": validate_mean,
369 "min": validate_min,
370 "max": validate_max,
371 "sum": validate_sum,
372 "prod": validate_prod,
373}
374
375
376def validate_func(fname: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> None:
377 if fname not in _validation_funcs:
378 return validate_stat_func(args, kwargs, fname=fname)
379
380 validation_func = _validation_funcs[fname]
381 return validation_func(args, kwargs)