Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/util/deprecations.py: 79%

182 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

1# util/deprecations.py 

2# Copyright (C) 2005-2023 the SQLAlchemy authors and contributors 

3# <see AUTHORS file> 

4# 

5# This module is part of SQLAlchemy and is released under 

6# the MIT License: https://www.opensource.org/licenses/mit-license.php 

7 

8"""Helpers related to deprecation of functions, methods, classes, other 

9functionality.""" 

10 

11import os 

12import re 

13import sys 

14 

15from . import compat 

16from .langhelpers import _hash_limit_string 

17from .langhelpers import _warnings_warn 

18from .langhelpers import decorator 

19from .langhelpers import inject_docstring_text 

20from .langhelpers import inject_param_text 

21from .. import exc 

22 

23 

24SQLALCHEMY_WARN_20 = False 

25 

26SILENCE_UBER_WARNING = False 

27 

28if os.getenv("SQLALCHEMY_WARN_20", "false").lower() in ("true", "yes", "1"): 

29 SQLALCHEMY_WARN_20 = True 

30 

31if compat.py2k: 

32 SILENCE_UBER_WARNING = True 

33elif os.getenv("SQLALCHEMY_SILENCE_UBER_WARNING", "false").lower() in ( 

34 "true", 

35 "yes", 

36 "1", 

37): 

38 SILENCE_UBER_WARNING = True 

39 

40 

41def _warn_with_version(msg, version, type_, stacklevel, code=None): 

42 if ( 

43 issubclass(type_, exc.Base20DeprecationWarning) 

44 and not SQLALCHEMY_WARN_20 

45 ): 

46 if not SILENCE_UBER_WARNING: 

47 _emit_uber_warning(type_, stacklevel) 

48 

49 return 

50 

51 warn = type_(msg, code=code) 

52 warn.deprecated_since = version 

53 

54 _warnings_warn(warn, stacklevel=stacklevel + 1) 

55 

56 

57def _emit_uber_warning(type_, stacklevel): 

58 global SILENCE_UBER_WARNING 

59 

60 if SILENCE_UBER_WARNING: 

61 return 

62 

63 SILENCE_UBER_WARNING = True 

64 

65 file_ = sys.stderr 

66 

67 # source: https://github.com/pytest-dev/pytest/blob/326ae0cd88f5e954c8effc2b0c986832e9caff11/src/_pytest/_io/terminalwriter.py#L35-L37 # noqa: E501 

68 use_color = ( 

69 hasattr(file_, "isatty") 

70 and file_.isatty() 

71 and os.environ.get("TERM") != "dumb" 

72 ) 

73 

74 msg = ( 

75 "%(red)sDeprecated API features detected! " 

76 "These feature(s) are not compatible with SQLAlchemy 2.0. " 

77 "%(green)sTo prevent incompatible upgrades prior to updating " 

78 "applications, ensure requirements files are " 

79 'pinned to "sqlalchemy<2.0". ' 

80 "%(cyan)sSet environment variable SQLALCHEMY_WARN_20=1 to show all " 

81 "deprecation warnings. Set environment variable " 

82 "SQLALCHEMY_SILENCE_UBER_WARNING=1 to silence this message.%(nocolor)s" 

83 ) 

84 

85 if use_color: 

86 msg = msg % { 

87 "red": "\x1b[31m", 

88 "cyan": "\x1b[36m", 

89 "green": "\x1b[32m", 

90 "magenta": "\x1b[35m", 

91 "nocolor": "\x1b[0m", 

92 } 

93 else: 

94 msg = msg % { 

95 "red": "", 

96 "cyan": "", 

97 "green": "", 

98 "magenta": "", 

99 "nocolor": "", 

100 } 

101 

102 # note this is a exc.Base20DeprecationWarning subclass, which 

103 # will implicitly add the link to the SQLAlchemy 2.0 page in the message 

104 warn = type_(msg) 

105 _warnings_warn(warn, stacklevel=stacklevel + 1) 

106 

107 

108def warn_deprecated(msg, version, stacklevel=3, code=None): 

109 _warn_with_version( 

110 msg, version, exc.SADeprecationWarning, stacklevel, code=code 

111 ) 

112 

113 

114def warn_deprecated_limited(msg, args, version, stacklevel=3, code=None): 

115 """Issue a deprecation warning with a parameterized string, 

116 limiting the number of registrations. 

117 

118 """ 

119 if args: 

120 msg = _hash_limit_string(msg, 10, args) 

121 _warn_with_version( 

122 msg, version, exc.SADeprecationWarning, stacklevel, code=code 

123 ) 

124 

125 

126def warn_deprecated_20(msg, stacklevel=3, code=None): 

127 

128 _warn_with_version( 

129 msg, 

130 exc.RemovedIn20Warning.deprecated_since, 

131 exc.RemovedIn20Warning, 

132 stacklevel, 

133 code=code, 

134 ) 

135 

136 

137def deprecated_cls(version, message, constructor="__init__"): 

138 header = ".. deprecated:: %s %s" % (version, (message or "")) 

139 

140 def decorate(cls): 

141 return _decorate_cls_with_warning( 

142 cls, 

143 constructor, 

144 exc.SADeprecationWarning, 

145 message % dict(func=constructor), 

146 version, 

147 header, 

148 ) 

149 

150 return decorate 

151 

152 

153def deprecated_20_cls( 

154 clsname, alternative=None, constructor="__init__", becomes_legacy=False 

155): 

156 message = ( 

157 ".. deprecated:: 1.4 The %s class is considered legacy as of the " 

158 "1.x series of SQLAlchemy and %s in 2.0." 

159 % ( 

160 clsname, 

161 "will be removed" 

162 if not becomes_legacy 

163 else "becomes a legacy construct", 

164 ) 

165 ) 

166 

167 if alternative: 

168 message += " " + alternative 

169 

170 if becomes_legacy: 

171 warning_cls = exc.LegacyAPIWarning 

172 else: 

173 warning_cls = exc.RemovedIn20Warning 

174 

175 def decorate(cls): 

176 return _decorate_cls_with_warning( 

177 cls, 

178 constructor, 

179 warning_cls, 

180 message, 

181 warning_cls.deprecated_since, 

182 message, 

183 ) 

184 

185 return decorate 

186 

187 

188def deprecated( 

189 version, 

190 message=None, 

191 add_deprecation_to_docstring=True, 

192 warning=None, 

193 enable_warnings=True, 

194): 

195 """Decorates a function and issues a deprecation warning on use. 

196 

197 :param version: 

198 Issue version in the warning. 

199 

200 :param message: 

201 If provided, issue message in the warning. A sensible default 

202 is used if not provided. 

203 

204 :param add_deprecation_to_docstring: 

205 Default True. If False, the wrapped function's __doc__ is left 

206 as-is. If True, the 'message' is prepended to the docs if 

207 provided, or sensible default if message is omitted. 

208 

209 """ 

210 

211 # nothing is deprecated "since" 2.0 at this time. All "removed in 2.0" 

212 # should emit the RemovedIn20Warning, but messaging should be expressed 

213 # in terms of "deprecated since 1.4". 

214 

215 if version == "2.0": 

216 if warning is None: 

217 warning = exc.RemovedIn20Warning 

218 version = "1.4" 

219 if add_deprecation_to_docstring: 

220 header = ".. deprecated:: %s %s" % ( 

221 version, 

222 (message or ""), 

223 ) 

224 else: 

225 header = None 

226 

227 if message is None: 

228 message = "Call to deprecated function %(func)s" 

229 

230 if warning is None: 

231 warning = exc.SADeprecationWarning 

232 

233 if warning is not exc.RemovedIn20Warning: 

234 message += " (deprecated since: %s)" % version 

235 

236 def decorate(fn): 

237 return _decorate_with_warning( 

238 fn, 

239 warning, 

240 message % dict(func=fn.__name__), 

241 version, 

242 header, 

243 enable_warnings=enable_warnings, 

244 ) 

245 

246 return decorate 

247 

248 

249def moved_20(message, **kw): 

250 return deprecated( 

251 "2.0", message=message, warning=exc.MovedIn20Warning, **kw 

252 ) 

253 

254 

255def deprecated_20(api_name, alternative=None, becomes_legacy=False, **kw): 

256 type_reg = re.match("^:(attr|func|meth):", api_name) 

257 if type_reg: 

258 type_ = {"attr": "attribute", "func": "function", "meth": "method"}[ 

259 type_reg.group(1) 

260 ] 

261 else: 

262 type_ = "construct" 

263 message = ( 

264 "The %s %s is considered legacy as of the " 

265 "1.x series of SQLAlchemy and %s in 2.0." 

266 % ( 

267 api_name, 

268 type_, 

269 "will be removed" 

270 if not becomes_legacy 

271 else "becomes a legacy construct", 

272 ) 

273 ) 

274 

275 if ":attr:" in api_name: 

276 attribute_ok = kw.pop("warn_on_attribute_access", False) 

277 if not attribute_ok: 

278 assert kw.get("enable_warnings") is False, ( 

279 "attribute %s will emit a warning on read access. " 

280 "If you *really* want this, " 

281 "add warn_on_attribute_access=True. Otherwise please add " 

282 "enable_warnings=False." % api_name 

283 ) 

284 

285 if alternative: 

286 message += " " + alternative 

287 

288 if becomes_legacy: 

289 warning_cls = exc.LegacyAPIWarning 

290 else: 

291 warning_cls = exc.RemovedIn20Warning 

292 

293 return deprecated("2.0", message=message, warning=warning_cls, **kw) 

294 

295 

296def deprecated_params(**specs): 

297 """Decorates a function to warn on use of certain parameters. 

298 

299 e.g. :: 

300 

301 @deprecated_params( 

302 weak_identity_map=( 

303 "0.7", 

304 "the :paramref:`.Session.weak_identity_map parameter " 

305 "is deprecated." 

306 ) 

307 

308 ) 

309 

310 """ 

311 

312 messages = {} 

313 versions = {} 

314 version_warnings = {} 

315 

316 for param, (version, message) in specs.items(): 

317 versions[param] = version 

318 messages[param] = _sanitize_restructured_text(message) 

319 version_warnings[param] = ( 

320 exc.RemovedIn20Warning 

321 if version == "2.0" 

322 else exc.SADeprecationWarning 

323 ) 

324 

325 def decorate(fn): 

326 spec = compat.inspect_getfullargspec(fn) 

327 

328 if spec.defaults is not None: 

329 defaults = dict( 

330 zip( 

331 spec.args[(len(spec.args) - len(spec.defaults)) :], 

332 spec.defaults, 

333 ) 

334 ) 

335 check_defaults = set(defaults).intersection(messages) 

336 check_kw = set(messages).difference(defaults) 

337 else: 

338 check_defaults = () 

339 check_kw = set(messages) 

340 

341 check_any_kw = spec.varkw 

342 

343 @decorator 

344 def warned(fn, *args, **kwargs): 

345 for m in check_defaults: 

346 if (defaults[m] is None and kwargs[m] is not None) or ( 

347 defaults[m] is not None and kwargs[m] != defaults[m] 

348 ): 

349 _warn_with_version( 

350 messages[m], 

351 versions[m], 

352 version_warnings[m], 

353 stacklevel=3, 

354 ) 

355 

356 if check_any_kw in messages and set(kwargs).difference( 

357 check_defaults 

358 ): 

359 

360 _warn_with_version( 

361 messages[check_any_kw], 

362 versions[check_any_kw], 

363 version_warnings[check_any_kw], 

364 stacklevel=3, 

365 ) 

366 

367 for m in check_kw: 

368 if m in kwargs: 

369 _warn_with_version( 

370 messages[m], 

371 versions[m], 

372 version_warnings[m], 

373 stacklevel=3, 

374 ) 

375 return fn(*args, **kwargs) 

376 

377 doc = fn.__doc__ is not None and fn.__doc__ or "" 

378 if doc: 

379 doc = inject_param_text( 

380 doc, 

381 { 

382 param: ".. deprecated:: %s %s" 

383 % ("1.4" if version == "2.0" else version, (message or "")) 

384 for param, (version, message) in specs.items() 

385 }, 

386 ) 

387 decorated = warned(fn) 

388 decorated.__doc__ = doc 

389 return decorated 

390 

391 return decorate 

392 

393 

394def _sanitize_restructured_text(text): 

395 def repl(m): 

396 type_, name = m.group(1, 2) 

397 if type_ in ("func", "meth"): 

398 name += "()" 

399 return name 

400 

401 text = re.sub(r":ref:`(.+) <.*>`", lambda m: '"%s"' % m.group(1), text) 

402 return re.sub(r"\:(\w+)\:`~?(?:_\w+)?\.?(.+?)`", repl, text) 

403 

404 

405def _decorate_cls_with_warning( 

406 cls, constructor, wtype, message, version, docstring_header=None 

407): 

408 doc = cls.__doc__ is not None and cls.__doc__ or "" 

409 if docstring_header is not None: 

410 

411 if constructor is not None: 

412 docstring_header %= dict(func=constructor) 

413 

414 if issubclass(wtype, exc.Base20DeprecationWarning): 

415 docstring_header += ( 

416 " (Background on SQLAlchemy 2.0 at: " 

417 ":ref:`migration_20_toplevel`)" 

418 ) 

419 doc = inject_docstring_text(doc, docstring_header, 1) 

420 

421 if type(cls) is type: 

422 clsdict = dict(cls.__dict__) 

423 clsdict["__doc__"] = doc 

424 clsdict.pop("__dict__", None) 

425 clsdict.pop("__weakref__", None) 

426 cls = type(cls.__name__, cls.__bases__, clsdict) 

427 if constructor is not None: 

428 constructor_fn = clsdict[constructor] 

429 

430 else: 

431 cls.__doc__ = doc 

432 if constructor is not None: 

433 constructor_fn = getattr(cls, constructor) 

434 

435 if constructor is not None: 

436 setattr( 

437 cls, 

438 constructor, 

439 _decorate_with_warning( 

440 constructor_fn, wtype, message, version, None 

441 ), 

442 ) 

443 return cls 

444 

445 

446def _decorate_with_warning( 

447 func, wtype, message, version, docstring_header=None, enable_warnings=True 

448): 

449 """Wrap a function with a warnings.warn and augmented docstring.""" 

450 

451 message = _sanitize_restructured_text(message) 

452 

453 if issubclass(wtype, exc.Base20DeprecationWarning): 

454 doc_only = ( 

455 " (Background on SQLAlchemy 2.0 at: " 

456 ":ref:`migration_20_toplevel`)" 

457 ) 

458 else: 

459 doc_only = "" 

460 

461 @decorator 

462 def warned(fn, *args, **kwargs): 

463 skip_warning = not enable_warnings or kwargs.pop( 

464 "_sa_skip_warning", False 

465 ) 

466 if not skip_warning: 

467 _warn_with_version(message, version, wtype, stacklevel=3) 

468 return fn(*args, **kwargs) 

469 

470 doc = func.__doc__ is not None and func.__doc__ or "" 

471 if docstring_header is not None: 

472 docstring_header %= dict(func=func.__name__) 

473 

474 docstring_header += doc_only 

475 

476 doc = inject_docstring_text(doc, docstring_header, 1) 

477 

478 decorated = warned(func) 

479 decorated.__doc__ = doc 

480 decorated._sa_warn = lambda: _warn_with_version( 

481 message, version, wtype, stacklevel=3 

482 ) 

483 return decorated