Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-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

137 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 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]