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,
56 fname=None,
57 method: str | None = None,
58 max_fname_arg_count=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,
68 kwargs,
69 fname=None,
70 max_fname_arg_count=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, 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, 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(skipna: bool | ndarray | None, args) -> tuple[bool, Any]:
106 if isinstance(skipna, ndarray) or skipna is None:
107 args = (skipna, *args)
108 skipna = True
109
110 return skipna, args
111
112
113def validate_argmin_with_skipna(skipna: bool | ndarray | None, args, kwargs) -> bool:
114 """
115 If 'Series.argmin' is called via the 'numpy' library, the third parameter
116 in its signature is 'out', which takes either an ndarray or 'None', so
117 check if the 'skipna' parameter is either an instance of ndarray or is
118 None, since 'skipna' itself should be a boolean
119 """
120 skipna, args = process_skipna(skipna, args)
121 validate_argmin(args, kwargs)
122 return skipna
123
124
125def validate_argmax_with_skipna(skipna: bool | ndarray | None, args, kwargs) -> bool:
126 """
127 If 'Series.argmax' is called via the 'numpy' library, the third parameter
128 in its signature is 'out', which takes either an ndarray or 'None', so
129 check if the 'skipna' parameter is either an instance of ndarray or is
130 None, since 'skipna' itself should be a boolean
131 """
132 skipna, args = process_skipna(skipna, args)
133 validate_argmax(args, kwargs)
134 return skipna
135
136
137ARGSORT_DEFAULTS: dict[str, int | str | None] = {}
138ARGSORT_DEFAULTS["axis"] = -1
139ARGSORT_DEFAULTS["kind"] = "quicksort"
140ARGSORT_DEFAULTS["order"] = None
141ARGSORT_DEFAULTS["kind"] = None
142ARGSORT_DEFAULTS["stable"] = None
143
144
145validate_argsort = CompatValidator(
146 ARGSORT_DEFAULTS, fname="argsort", max_fname_arg_count=0, method="both"
147)
148
149# two different signatures of argsort, this second validation for when the
150# `kind` param is supported
151ARGSORT_DEFAULTS_KIND: dict[str, int | None] = {}
152ARGSORT_DEFAULTS_KIND["axis"] = -1
153ARGSORT_DEFAULTS_KIND["order"] = None
154ARGSORT_DEFAULTS_KIND["stable"] = None
155validate_argsort_kind = CompatValidator(
156 ARGSORT_DEFAULTS_KIND, fname="argsort", max_fname_arg_count=0, method="both"
157)
158
159
160def validate_argsort_with_ascending(ascending: bool | int | None, args, kwargs) -> bool:
161 """
162 If 'Categorical.argsort' is called via the 'numpy' library, the first
163 parameter in its signature is 'axis', which takes either an integer or
164 'None', so check if the 'ascending' parameter has either integer type or is
165 None, since 'ascending' itself should be a boolean
166 """
167 if is_integer(ascending) or ascending is None:
168 args = (ascending, *args)
169 ascending = True
170
171 validate_argsort_kind(args, kwargs, max_fname_arg_count=3)
172 ascending = cast(bool, ascending)
173 return ascending
174
175
176CLIP_DEFAULTS: dict[str, Any] = {"out": None}
177validate_clip = CompatValidator(
178 CLIP_DEFAULTS, fname="clip", method="both", max_fname_arg_count=3
179)
180
181
182@overload
183def validate_clip_with_axis(axis: ndarray, args, kwargs) -> None: ...
184
185
186@overload
187def validate_clip_with_axis(axis: AxisNoneT, args, kwargs) -> AxisNoneT: ...
188
189
190def validate_clip_with_axis(
191 axis: ndarray | AxisNoneT, args, kwargs
192) -> AxisNoneT | None:
193 """
194 If 'NDFrame.clip' is called via the numpy library, the third parameter in
195 its signature is 'out', which can takes an ndarray, so check if the 'axis'
196 parameter is an instance of ndarray, since 'axis' itself should either be
197 an integer or None
198 """
199 if isinstance(axis, ndarray):
200 args = (axis, *args)
201 # error: Incompatible types in assignment (expression has type "None",
202 # variable has type "Union[ndarray[Any, Any], str, int]")
203 axis = None # type: ignore[assignment]
204
205 validate_clip(args, kwargs)
206 # error: Incompatible return value type (got "Union[ndarray[Any, Any],
207 # str, int]", expected "Union[str, int, None]")
208 return axis # type: ignore[return-value]
209
210
211CUM_FUNC_DEFAULTS: dict[str, Any] = {}
212CUM_FUNC_DEFAULTS["dtype"] = None
213CUM_FUNC_DEFAULTS["out"] = None
214validate_cum_func = CompatValidator(
215 CUM_FUNC_DEFAULTS, method="both", max_fname_arg_count=1
216)
217validate_cumsum = CompatValidator(
218 CUM_FUNC_DEFAULTS, fname="cumsum", method="both", max_fname_arg_count=1
219)
220
221
222def validate_cum_func_with_skipna(skipna: bool, args, kwargs, name) -> 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(name: str, args, kwargs, allowed=None) -> None:
324 """
325 'args' and 'kwargs' should be empty, except for allowed kwargs because all
326 of their necessary parameters are explicitly listed in the function
327 signature
328 """
329 if allowed is None:
330 allowed = []
331
332 kwargs = set(kwargs) - set(allowed)
333
334 if len(args) + len(kwargs) > 0:
335 raise UnsupportedFunctionCall(
336 "numpy operations are not valid with groupby. "
337 f"Use .groupby(...).{name}() instead"
338 )
339
340
341def validate_minmax_axis(axis: AxisInt | None, ndim: int = 1) -> None:
342 """
343 Ensure that the axis argument passed to min, max, argmin, or argmax is zero
344 or None, as otherwise it will be incorrectly ignored.
345
346 Parameters
347 ----------
348 axis : int or None
349 ndim : int, default 1
350
351 Raises
352 ------
353 ValueError
354 """
355 if axis is None:
356 return
357 if axis >= ndim or (axis < 0 and ndim + axis < 0):
358 raise ValueError(f"`axis` must be fewer than the number of dimensions ({ndim})")
359
360
361_validation_funcs = {
362 "median": validate_median,
363 "mean": validate_mean,
364 "min": validate_min,
365 "max": validate_max,
366 "sum": validate_sum,
367 "prod": validate_prod,
368}
369
370
371def validate_func(fname, args, kwargs) -> None:
372 if fname not in _validation_funcs:
373 return validate_stat_func(args, kwargs, fname=fname)
374
375 validation_func = _validation_funcs[fname]
376 return validation_func(args, kwargs)