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