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