1from __future__ import annotations
2
3from functools import wraps
4import inspect
5from textwrap import dedent
6from typing import (
7 TYPE_CHECKING,
8 Any,
9 cast,
10)
11import warnings
12
13from pandas._libs.properties import cache_readonly
14from pandas._typing import (
15 F,
16 T,
17)
18from pandas.util._exceptions import find_stack_level
19
20if TYPE_CHECKING:
21 from collections.abc import (
22 Callable,
23 Mapping,
24 )
25
26 from pandas.errors import PandasChangeWarning
27
28
29def deprecate(
30 klass: type[Warning],
31 name: str,
32 alternative: Callable[..., Any],
33 version: str,
34 alt_name: str | None = None,
35 stacklevel: int = 2,
36 msg: str | None = None,
37) -> Callable[[F], F]:
38 """
39 Return a new function that emits a deprecation warning on use.
40
41 To use this method for a deprecated function, another function
42 `alternative` with the same signature must exist. The deprecated
43 function will emit a deprecation warning, and in the docstring
44 it will contain the deprecation directive with the provided version
45 so it can be detected for future removal.
46
47 Parameters
48 ----------
49 klass : Warning
50 The warning class to use.
51 name : str
52 Name of function to deprecate.
53 alternative : func
54 Function to use instead.
55 version : str
56 Version of pandas in which the method has been deprecated.
57 alt_name : str, optional
58 Name to use in preference of alternative.__name__.
59 stacklevel : int, default 2
60 msg : str
61 The message to display in the warning.
62 Default is '{name} is deprecated. Use {alt_name} instead.'
63 """
64 alt_name = alt_name or alternative.__name__
65 warning_msg = msg or f"{name} is deprecated, use {alt_name} instead."
66
67 @wraps(alternative)
68 def wrapper(*args, **kwargs) -> Callable[..., Any]:
69 warnings.warn(warning_msg, klass, stacklevel=stacklevel)
70 return alternative(*args, **kwargs)
71
72 # adding deprecated directive to the docstring
73 msg = msg or f"Use `{alt_name}` instead."
74 doc_error_msg = (
75 "deprecate needs a correctly formatted docstring in "
76 "the target function (should have a one liner short "
77 "summary, and opening quotes should be in their own "
78 f"line). Found:\n{alternative.__doc__}"
79 )
80
81 # when python is running in optimized mode (i.e. `-OO`), docstrings are
82 # removed, so we check that a docstring with correct formatting is used
83 # but we allow empty docstrings
84 if alternative.__doc__:
85 if alternative.__doc__.count("\n") < 3:
86 raise AssertionError(doc_error_msg)
87 empty1, summary, empty2, doc_string = alternative.__doc__.split("\n", 3)
88 if empty1 or (empty2 and not summary):
89 raise AssertionError(doc_error_msg)
90 wrapper.__doc__ = dedent(
91 f"""
92 {summary.strip()}
93
94 .. deprecated:: {version}
95 {msg}
96
97 {dedent(doc_string)}"""
98 )
99 # error: Incompatible return value type (got "Callable[[VarArg(Any), KwArg(Any)],
100 # Callable[...,Any]]", expected "Callable[[F], F]")
101 return wrapper # type: ignore[return-value]
102
103
104def deprecate_kwarg(
105 klass: type[Warning],
106 old_arg_name: str,
107 new_arg_name: str | None,
108 mapping: Mapping[Any, Any] | Callable[[Any], Any] | None = None,
109 stacklevel: int = 2,
110) -> Callable[[F], F]:
111 """
112 Decorator to deprecate a keyword argument of a function.
113
114 Parameters
115 ----------
116 klass : Warning
117 The warning class to use.
118 old_arg_name : str
119 Name of argument in function to deprecate.
120 new_arg_name : str or None
121 Name of preferred argument in function. Use None to raise warning that
122 ``old_arg_name`` keyword is deprecated.
123 mapping : dict or callable
124 If mapping is present, use it to translate old arguments to
125 new arguments. A callable must do its own value checking;
126 values not found in a dict will be forwarded unchanged.
127 stacklevel : int, default 2
128
129 Examples
130 --------
131 The following deprecates 'cols', using 'columns' instead
132
133 >>> @deprecate_kwarg(FutureWarning, old_arg_name="cols", new_arg_name="columns")
134 ... def f(columns=""):
135 ... print(columns)
136 >>> f(columns="should work ok")
137 should work ok
138
139 >>> f(cols="should raise warning") # doctest: +SKIP
140 FutureWarning: cols is deprecated, use columns instead
141 warnings.warn(msg, FutureWarning)
142 should raise warning
143
144 >>> f(cols="should error", columns="can't pass do both") # doctest: +SKIP
145 TypeError: Can only specify 'cols' or 'columns', not both
146
147 >>> @deprecate_kwarg(FutureWarning, "old", "new", {"yes": True, "no": False})
148 ... def f(new=False):
149 ... print("yes!" if new else "no!")
150 >>> f(old="yes") # doctest: +SKIP
151 FutureWarning: old='yes' is deprecated, use new=True instead
152 warnings.warn(msg, FutureWarning)
153 yes!
154
155 To raise a warning that a keyword will be removed entirely in the future
156
157 >>> @deprecate_kwarg(FutureWarning, old_arg_name="cols", new_arg_name=None)
158 ... def f(cols="", another_param=""):
159 ... print(cols)
160 >>> f(cols="should raise warning") # doctest: +SKIP
161 FutureWarning: the 'cols' keyword is deprecated and will be removed in a
162 future version. Please take steps to stop the use of 'cols'
163 should raise warning
164 >>> f(another_param="should not raise warning") # doctest: +SKIP
165 should not raise warning
166
167 >>> f(cols="should raise warning", another_param="") # doctest: +SKIP
168 FutureWarning: the 'cols' keyword is deprecated and will be removed in a
169 future version. Please take steps to stop the use of 'cols'
170 should raise warning
171 """
172 if mapping is not None and not hasattr(mapping, "get") and not callable(mapping):
173 raise TypeError(
174 "mapping from old to new argument values must be dict or callable!"
175 )
176
177 def _deprecate_kwarg(func: F) -> F:
178 @wraps(func)
179 def wrapper(*args, **kwargs) -> Callable[..., Any]:
180 __tracebackhide__ = True
181
182 old_arg_value = kwargs.pop(old_arg_name, None)
183
184 if old_arg_value is not None:
185 if new_arg_name is None:
186 msg = (
187 f"the {old_arg_name!r} keyword is deprecated and "
188 "will be removed in a future version. Please take "
189 f"steps to stop the use of {old_arg_name!r}"
190 )
191 warnings.warn(msg, klass, stacklevel=stacklevel)
192 kwargs[old_arg_name] = old_arg_value
193 return func(*args, **kwargs)
194
195 elif mapping is not None:
196 if callable(mapping):
197 new_arg_value = mapping(old_arg_value)
198 else:
199 new_arg_value = mapping.get(old_arg_value, old_arg_value)
200 msg = (
201 f"the {old_arg_name}={old_arg_value!r} keyword is "
202 "deprecated, use "
203 f"{new_arg_name}={new_arg_value!r} instead."
204 )
205 else:
206 new_arg_value = old_arg_value
207 msg = (
208 f"the {old_arg_name!r} keyword is deprecated, "
209 f"use {new_arg_name!r} instead."
210 )
211
212 warnings.warn(msg, klass, stacklevel=stacklevel)
213 if kwargs.get(new_arg_name) is not None:
214 msg = (
215 f"Can only specify {old_arg_name!r} "
216 f"or {new_arg_name!r}, not both."
217 )
218 raise TypeError(msg)
219 kwargs[new_arg_name] = new_arg_value
220 return func(*args, **kwargs)
221
222 return cast(F, wrapper)
223
224 return _deprecate_kwarg
225
226
227def _format_argument_list(allow_args: list[str]) -> str:
228 """
229 Convert the allow_args argument (either string or integer) of
230 `deprecate_nonkeyword_arguments` function to a string describing
231 it to be inserted into warning message.
232
233 Parameters
234 ----------
235 allowed_args : list, tuple or int
236 The `allowed_args` argument for `deprecate_nonkeyword_arguments`,
237 but None value is not allowed.
238
239 Returns
240 -------
241 str
242 The substring describing the argument list in best way to be
243 inserted to the warning message.
244
245 Examples
246 --------
247 `format_argument_list([])` -> ''
248 `format_argument_list(['a'])` -> "except for the arguments 'a'"
249 `format_argument_list(['a', 'b'])` -> "except for the arguments 'a' and 'b'"
250 `format_argument_list(['a', 'b', 'c'])` ->
251 "except for the arguments 'a', 'b' and 'c'"
252 """
253 if "self" in allow_args:
254 allow_args.remove("self")
255 if not allow_args:
256 return ""
257 elif len(allow_args) == 1:
258 return f" except for the argument '{allow_args[0]}'"
259 else:
260 last = allow_args[-1]
261 args = ", ".join(["'" + x + "'" for x in allow_args[:-1]])
262 return f" except for the arguments {args} and '{last}'"
263
264
265def future_version_msg(version: str | None) -> str:
266 """Specify which version of pandas the deprecation will take place in."""
267 if version is None:
268 return "In a future version of pandas"
269 else:
270 return f"Starting with pandas version {version}"
271
272
273def deprecate_nonkeyword_arguments(
274 klass: type[PandasChangeWarning],
275 allowed_args: list[str] | None = None,
276 name: str | None = None,
277) -> Callable[[F], F]:
278 """
279 Decorator to deprecate a use of non-keyword arguments of a function.
280
281 Parameters
282 ----------
283 klass : Warning
284 The warning class to use.
285 allowed_args : list, optional
286 In case of list, it must be the list of names of some
287 first arguments of the decorated functions that are
288 OK to be given as positional arguments. In case of None value,
289 defaults to list of all arguments not having the
290 default value.
291 name : str, optional
292 The specific name of the function to show in the warning
293 message. If None, then the Qualified name of the function
294 is used.
295 """
296
297 def decorate(func):
298 old_sig = inspect.signature(func)
299
300 if allowed_args is not None:
301 allow_args = allowed_args
302 else:
303 allow_args = [
304 p.name
305 for p in old_sig.parameters.values()
306 if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
307 and p.default is p.empty
308 ]
309
310 new_params = [
311 p.replace(kind=p.KEYWORD_ONLY)
312 if (
313 p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
314 and p.name not in allow_args
315 )
316 else p
317 for p in old_sig.parameters.values()
318 ]
319 new_params.sort(key=lambda p: p.kind)
320 new_sig = old_sig.replace(parameters=new_params)
321
322 num_allow_args = len(allow_args)
323 msg = (
324 f"{future_version_msg(klass.version())} all arguments of "
325 f"{name or func.__qualname__}{{arguments}} will be keyword-only."
326 )
327
328 @wraps(func)
329 def wrapper(*args, **kwargs):
330 if len(args) > num_allow_args:
331 warnings.warn(
332 msg.format(arguments=_format_argument_list(allow_args)),
333 klass,
334 stacklevel=find_stack_level(),
335 )
336 return func(*args, **kwargs)
337
338 # error: "Callable[[VarArg(Any), KwArg(Any)], Any]" has no
339 # attribute "__signature__"
340 wrapper.__signature__ = new_sig # type: ignore[attr-defined]
341 return wrapper
342
343 return decorate
344
345
346def doc(*docstrings: None | str | Callable, **params: object) -> Callable[[F], F]:
347 """
348 A decorator to take docstring templates, concatenate them and perform string
349 substitution on them.
350
351 This decorator will add a variable "_docstring_components" to the wrapped
352 callable to keep track the original docstring template for potential usage.
353 If it should be consider as a template, it will be saved as a string.
354 Otherwise, it will be saved as callable, and later user __doc__ and dedent
355 to get docstring.
356
357 Parameters
358 ----------
359 *docstrings : None, str, or callable
360 The string / docstring / docstring template to be appended in order
361 after default docstring under callable.
362 **params
363 The string which would be used to format docstring template.
364 """
365
366 def decorator(decorated: F) -> F:
367 # collecting docstring and docstring templates
368 docstring_components: list[str | Callable] = []
369 if decorated.__doc__:
370 docstring_components.append(dedent(decorated.__doc__))
371
372 for docstring in docstrings:
373 if docstring is None:
374 continue
375 if hasattr(docstring, "_docstring_components"):
376 docstring_components.extend(
377 docstring._docstring_components # pyright: ignore[reportAttributeAccessIssue]
378 )
379 elif isinstance(docstring, str) or docstring.__doc__:
380 docstring_components.append(docstring)
381
382 params_applied = [
383 component.format(**params)
384 if isinstance(component, str) and len(params) > 0
385 else component
386 for component in docstring_components
387 ]
388
389 decorated.__doc__ = "".join(
390 [
391 component
392 if isinstance(component, str)
393 else dedent(component.__doc__ or "")
394 for component in params_applied
395 ]
396 )
397
398 # error: "F" has no attribute "_docstring_components"
399 decorated._docstring_components = ( # type: ignore[attr-defined]
400 docstring_components
401 )
402 return decorated
403
404 return decorator
405
406
407# Substitution and Appender are derived from matplotlib.docstring (1.1.0)
408# module https://matplotlib.org/users/license.html
409
410
411class Substitution:
412 """
413 A decorator to take a function's docstring and perform string
414 substitution on it.
415
416 This decorator should be robust even if func.__doc__ is None
417 (for example, if -OO was passed to the interpreter)
418
419 Usage: construct a docstring.Substitution with a sequence or
420 dictionary suitable for performing substitution; then
421 decorate a suitable function with the constructed object. e.g.
422
423 sub_author_name = Substitution(author='Jason')
424
425 @sub_author_name
426 def some_function(x):
427 "%(author)s wrote this function"
428
429 # note that some_function.__doc__ is now "Jason wrote this function"
430
431 One can also use positional arguments.
432
433 sub_first_last_names = Substitution('Edgar Allen', 'Poe')
434
435 @sub_first_last_names
436 def some_function(x):
437 "%s %s wrote the Raven"
438 """
439
440 def __init__(self, *args, **kwargs) -> None:
441 if args and kwargs:
442 raise AssertionError("Only positional or keyword args are allowed")
443
444 self.params = args or kwargs
445
446 def __call__(self, func: F) -> F:
447 func.__doc__ = func.__doc__ and func.__doc__ % self.params
448 return func
449
450 def update(self, *args, **kwargs) -> None:
451 """
452 Update self.params with supplied args.
453 """
454 if isinstance(self.params, dict):
455 self.params.update(*args, **kwargs)
456
457
458class Appender:
459 """
460 A function decorator that will append an addendum to the docstring
461 of the target function.
462
463 This decorator should be robust even if func.__doc__ is None
464 (for example, if -OO was passed to the interpreter).
465
466 Usage: construct a docstring.Appender with a string to be joined to
467 the original docstring. An optional 'join' parameter may be supplied
468 which will be used to join the docstring and addendum. e.g.
469
470 add_copyright = Appender("Copyright (c) 2009", join='\n')
471
472 @add_copyright
473 def my_dog(has='fleas'):
474 "This docstring will have a copyright below"
475 pass
476 """
477
478 addendum: str | None
479
480 def __init__(self, addendum: str | None, join: str = "", indents: int = 0) -> None:
481 if indents > 0:
482 self.addendum = indent(addendum, indents=indents)
483 else:
484 self.addendum = addendum
485 self.join = join
486
487 def __call__(self, func: T) -> T:
488 func.__doc__ = func.__doc__ if func.__doc__ else ""
489 self.addendum = self.addendum if self.addendum else ""
490 docitems = [func.__doc__, self.addendum]
491 func.__doc__ = dedent(self.join.join(docitems))
492 return func
493
494
495def indent(text: str | None, indents: int = 1) -> str:
496 if not text or not isinstance(text, str):
497 return ""
498 jointext = "".join(["\n"] + [" "] * indents)
499 return jointext.join(text.split("\n"))
500
501
502__all__ = [
503 "Appender",
504 "Substitution",
505 "cache_readonly",
506 "deprecate",
507 "deprecate_kwarg",
508 "deprecate_nonkeyword_arguments",
509 "doc",
510 "future_version_msg",
511]
512
513
514def set_module(module) -> Callable[[F], F]:
515 """Private decorator for overriding __module__ on a function or class.
516
517 Example usage::
518
519 @set_module("pandas")
520 def example():
521 pass
522
523
524 assert example.__module__ == "pandas"
525 """
526
527 def decorator(func: F) -> F:
528 if module is not None:
529 if isinstance(func, type):
530 # Store the original module for classes to ensure linkcode_resolve
531 # can resolve the true source location after re-exporting
532 try:
533 func._module_source = func.__module__ # type: ignore[attr-defined]
534 except AttributeError:
535 pass
536
537 func.__module__ = module
538 return cast("F", func) # type: ignore[redundant-cast]
539
540 return decorator