Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/util/_decorators.py: 40%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

136 statements  

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