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

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

161 statements  

1# util/deprecations.py 

2# Copyright (C) 2005-2024 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# mypy: allow-untyped-defs, allow-untyped-calls 

8 

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

10functionality.""" 

11 

12from __future__ import annotations 

13 

14import re 

15from typing import Any 

16from typing import Callable 

17from typing import Dict 

18from typing import Match 

19from typing import Optional 

20from typing import Sequence 

21from typing import Set 

22from typing import Tuple 

23from typing import Type 

24from typing import TypeVar 

25from typing import Union 

26 

27from . import compat 

28from .langhelpers import _hash_limit_string 

29from .langhelpers import _warnings_warn 

30from .langhelpers import decorator 

31from .langhelpers import inject_docstring_text 

32from .langhelpers import inject_param_text 

33from .. import exc 

34 

35_T = TypeVar("_T", bound=Any) 

36 

37 

38# https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators 

39_F = TypeVar("_F", bound="Callable[..., Any]") 

40 

41 

42def _warn_with_version( 

43 msg: str, 

44 version: str, 

45 type_: Type[exc.SADeprecationWarning], 

46 stacklevel: int, 

47 code: Optional[str] = None, 

48) -> None: 

49 warn = type_(msg, code=code) 

50 warn.deprecated_since = version 

51 

52 _warnings_warn(warn, stacklevel=stacklevel + 1) 

53 

54 

55def warn_deprecated( 

56 msg: str, version: str, stacklevel: int = 3, code: Optional[str] = None 

57) -> None: 

58 _warn_with_version( 

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

60 ) 

61 

62 

63def warn_deprecated_limited( 

64 msg: str, 

65 args: Sequence[Any], 

66 version: str, 

67 stacklevel: int = 3, 

68 code: Optional[str] = None, 

69) -> None: 

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

71 limiting the number of registrations. 

72 

73 """ 

74 if args: 

75 msg = _hash_limit_string(msg, 10, args) 

76 _warn_with_version( 

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

78 ) 

79 

80 

81def deprecated_cls( 

82 version: str, message: str, constructor: Optional[str] = "__init__" 

83) -> Callable[[Type[_T]], Type[_T]]: 

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

85 

86 def decorate(cls: Type[_T]) -> Type[_T]: 

87 return _decorate_cls_with_warning( 

88 cls, 

89 constructor, 

90 exc.SADeprecationWarning, 

91 message % dict(func=constructor), 

92 version, 

93 header, 

94 ) 

95 

96 return decorate 

97 

98 

99def deprecated( 

100 version: str, 

101 message: Optional[str] = None, 

102 add_deprecation_to_docstring: bool = True, 

103 warning: Optional[Type[exc.SADeprecationWarning]] = None, 

104 enable_warnings: bool = True, 

105) -> Callable[[_F], _F]: 

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

107 

108 :param version: 

109 Issue version in the warning. 

110 

111 :param message: 

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

113 is used if not provided. 

114 

115 :param add_deprecation_to_docstring: 

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

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

118 provided, or sensible default if message is omitted. 

119 

120 """ 

121 

122 if add_deprecation_to_docstring: 

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

124 version, 

125 (message or ""), 

126 ) 

127 else: 

128 header = None 

129 

130 if message is None: 

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

132 

133 if warning is None: 

134 warning = exc.SADeprecationWarning 

135 

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

137 

138 def decorate(fn: _F) -> _F: 

139 assert message is not None 

140 assert warning is not None 

141 return _decorate_with_warning( 

142 fn, 

143 warning, 

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

145 version, 

146 header, 

147 enable_warnings=enable_warnings, 

148 ) 

149 

150 return decorate 

151 

152 

153def moved_20( 

154 message: str, **kw: Any 

155) -> Callable[[Callable[..., _T]], Callable[..., _T]]: 

156 return deprecated( 

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

158 ) 

159 

160 

161def became_legacy_20( 

162 api_name: str, alternative: Optional[str] = None, **kw: Any 

163) -> Callable[[_F], _F]: 

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

165 if type_reg: 

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

167 type_reg.group(1) 

168 ] 

169 else: 

170 type_ = "construct" 

171 message = ( 

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

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

174 % ( 

175 api_name, 

176 type_, 

177 "becomes a legacy construct", 

178 ) 

179 ) 

180 

181 if ":attr:" in api_name: 

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

183 if not attribute_ok: 

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

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

186 "If you *really* want this, " 

187 "add warn_on_attribute_access=True. Otherwise please add " 

188 "enable_warnings=False." % api_name 

189 ) 

190 

191 if alternative: 

192 message += " " + alternative 

193 

194 warning_cls = exc.LegacyAPIWarning 

195 

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

197 

198 

199def deprecated_params(**specs: Tuple[str, str]) -> Callable[[_F], _F]: 

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

201 

202 e.g. :: 

203 

204 @deprecated_params( 

205 weak_identity_map=( 

206 "0.7", 

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

208 "is deprecated." 

209 ) 

210 

211 ) 

212 

213 """ 

214 

215 messages: Dict[str, str] = {} 

216 versions: Dict[str, str] = {} 

217 version_warnings: Dict[str, Type[exc.SADeprecationWarning]] = {} 

218 

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

220 versions[param] = version 

221 messages[param] = _sanitize_restructured_text(message) 

222 version_warnings[param] = exc.SADeprecationWarning 

223 

224 def decorate(fn: _F) -> _F: 

225 spec = compat.inspect_getfullargspec(fn) 

226 

227 check_defaults: Union[Set[str], Tuple[()]] 

228 if spec.defaults is not None: 

229 defaults = dict( 

230 zip( 

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

232 spec.defaults, 

233 ) 

234 ) 

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

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

237 elif spec.kwonlydefaults is not None: 

238 defaults = spec.kwonlydefaults 

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

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

241 else: 

242 check_defaults = () 

243 check_kw = set(messages) 

244 

245 check_any_kw = spec.varkw 

246 

247 # latest mypy has opinions here, not sure if they implemented 

248 # Concatenate or something 

249 @decorator 

250 def warned(fn: _F, *args: Any, **kwargs: Any) -> _F: 

251 for m in check_defaults: 

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

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

254 ): 

255 _warn_with_version( 

256 messages[m], 

257 versions[m], 

258 version_warnings[m], 

259 stacklevel=3, 

260 ) 

261 

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

263 check_defaults 

264 ): 

265 assert check_any_kw is not None 

266 _warn_with_version( 

267 messages[check_any_kw], 

268 versions[check_any_kw], 

269 version_warnings[check_any_kw], 

270 stacklevel=3, 

271 ) 

272 

273 for m in check_kw: 

274 if m in kwargs: 

275 _warn_with_version( 

276 messages[m], 

277 versions[m], 

278 version_warnings[m], 

279 stacklevel=3, 

280 ) 

281 return fn(*args, **kwargs) # type: ignore[no-any-return] 

282 

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

284 if doc: 

285 doc = inject_param_text( 

286 doc, 

287 { 

288 param: ".. deprecated:: %s %s" 

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

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

291 }, 

292 ) 

293 decorated = warned(fn) 

294 decorated.__doc__ = doc 

295 return decorated 

296 

297 return decorate 

298 

299 

300def _sanitize_restructured_text(text: str) -> str: 

301 def repl(m: Match[str]) -> str: 

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

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

304 name += "()" 

305 return name 

306 

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

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

309 

310 

311def _decorate_cls_with_warning( 

312 cls: Type[_T], 

313 constructor: Optional[str], 

314 wtype: Type[exc.SADeprecationWarning], 

315 message: str, 

316 version: str, 

317 docstring_header: Optional[str] = None, 

318) -> Type[_T]: 

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

320 if docstring_header is not None: 

321 if constructor is not None: 

322 docstring_header %= dict(func=constructor) 

323 

324 if issubclass(wtype, exc.Base20DeprecationWarning): 

325 docstring_header += ( 

326 " (Background on SQLAlchemy 2.0 at: " 

327 ":ref:`migration_20_toplevel`)" 

328 ) 

329 doc = inject_docstring_text(doc, docstring_header, 1) 

330 

331 constructor_fn = None 

332 if type(cls) is type: 

333 clsdict = dict(cls.__dict__) 

334 clsdict["__doc__"] = doc 

335 clsdict.pop("__dict__", None) 

336 clsdict.pop("__weakref__", None) 

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

338 if constructor is not None: 

339 constructor_fn = clsdict[constructor] 

340 

341 else: 

342 cls.__doc__ = doc 

343 if constructor is not None: 

344 constructor_fn = getattr(cls, constructor) 

345 

346 if constructor is not None: 

347 assert constructor_fn is not None 

348 assert wtype is not None 

349 setattr( 

350 cls, 

351 constructor, 

352 _decorate_with_warning( 

353 constructor_fn, wtype, message, version, None 

354 ), 

355 ) 

356 return cls 

357 

358 

359def _decorate_with_warning( 

360 func: _F, 

361 wtype: Type[exc.SADeprecationWarning], 

362 message: str, 

363 version: str, 

364 docstring_header: Optional[str] = None, 

365 enable_warnings: bool = True, 

366) -> _F: 

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

368 

369 message = _sanitize_restructured_text(message) 

370 

371 if issubclass(wtype, exc.Base20DeprecationWarning): 

372 doc_only = ( 

373 " (Background on SQLAlchemy 2.0 at: " 

374 ":ref:`migration_20_toplevel`)" 

375 ) 

376 else: 

377 doc_only = "" 

378 

379 @decorator 

380 def warned(fn: _F, *args: Any, **kwargs: Any) -> _F: 

381 skip_warning = not enable_warnings or kwargs.pop( 

382 "_sa_skip_warning", False 

383 ) 

384 if not skip_warning: 

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

386 return fn(*args, **kwargs) # type: ignore[no-any-return] 

387 

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

389 if docstring_header is not None: 

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

391 

392 docstring_header += doc_only 

393 

394 doc = inject_docstring_text(doc, docstring_header, 1) 

395 

396 decorated = warned(func) 

397 decorated.__doc__ = doc 

398 decorated._sa_warn = lambda: _warn_with_version( # type: ignore 

399 message, version, wtype, stacklevel=3 

400 ) 

401 return decorated