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

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

149 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._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