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

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

135 statements  

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]