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