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
« 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
8"""Helpers related to deprecation of functions, methods, classes, other
9functionality."""
11import os
12import re
13import sys
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
24SQLALCHEMY_WARN_20 = False
26SILENCE_UBER_WARNING = False
28if os.getenv("SQLALCHEMY_WARN_20", "false").lower() in ("true", "yes", "1"):
29 SQLALCHEMY_WARN_20 = True
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
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)
49 return
51 warn = type_(msg, code=code)
52 warn.deprecated_since = version
54 _warnings_warn(warn, stacklevel=stacklevel + 1)
57def _emit_uber_warning(type_, stacklevel):
58 global SILENCE_UBER_WARNING
60 if SILENCE_UBER_WARNING:
61 return
63 SILENCE_UBER_WARNING = True
65 file_ = sys.stderr
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 )
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 )
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 }
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)
108def warn_deprecated(msg, version, stacklevel=3, code=None):
109 _warn_with_version(
110 msg, version, exc.SADeprecationWarning, stacklevel, code=code
111 )
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.
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 )
126def warn_deprecated_20(msg, stacklevel=3, code=None):
128 _warn_with_version(
129 msg,
130 exc.RemovedIn20Warning.deprecated_since,
131 exc.RemovedIn20Warning,
132 stacklevel,
133 code=code,
134 )
137def deprecated_cls(version, message, constructor="__init__"):
138 header = ".. deprecated:: %s %s" % (version, (message or ""))
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 )
150 return decorate
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 )
167 if alternative:
168 message += " " + alternative
170 if becomes_legacy:
171 warning_cls = exc.LegacyAPIWarning
172 else:
173 warning_cls = exc.RemovedIn20Warning
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 )
185 return decorate
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.
197 :param version:
198 Issue version in the warning.
200 :param message:
201 If provided, issue message in the warning. A sensible default
202 is used if not provided.
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.
209 """
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".
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
227 if message is None:
228 message = "Call to deprecated function %(func)s"
230 if warning is None:
231 warning = exc.SADeprecationWarning
233 if warning is not exc.RemovedIn20Warning:
234 message += " (deprecated since: %s)" % version
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 )
246 return decorate
249def moved_20(message, **kw):
250 return deprecated(
251 "2.0", message=message, warning=exc.MovedIn20Warning, **kw
252 )
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 )
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 )
285 if alternative:
286 message += " " + alternative
288 if becomes_legacy:
289 warning_cls = exc.LegacyAPIWarning
290 else:
291 warning_cls = exc.RemovedIn20Warning
293 return deprecated("2.0", message=message, warning=warning_cls, **kw)
296def deprecated_params(**specs):
297 """Decorates a function to warn on use of certain parameters.
299 e.g. ::
301 @deprecated_params(
302 weak_identity_map=(
303 "0.7",
304 "the :paramref:`.Session.weak_identity_map parameter "
305 "is deprecated."
306 )
308 )
310 """
312 messages = {}
313 versions = {}
314 version_warnings = {}
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 )
325 def decorate(fn):
326 spec = compat.inspect_getfullargspec(fn)
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)
341 check_any_kw = spec.varkw
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 )
356 if check_any_kw in messages and set(kwargs).difference(
357 check_defaults
358 ):
360 _warn_with_version(
361 messages[check_any_kw],
362 versions[check_any_kw],
363 version_warnings[check_any_kw],
364 stacklevel=3,
365 )
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)
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
391 return decorate
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
401 text = re.sub(r":ref:`(.+) <.*>`", lambda m: '"%s"' % m.group(1), text)
402 return re.sub(r"\:(\w+)\:`~?(?:_\w+)?\.?(.+?)`", repl, text)
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:
411 if constructor is not None:
412 docstring_header %= dict(func=constructor)
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)
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]
430 else:
431 cls.__doc__ = doc
432 if constructor is not None:
433 constructor_fn = getattr(cls, constructor)
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
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."""
451 message = _sanitize_restructured_text(message)
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 = ""
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)
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__)
474 docstring_header += doc_only
476 doc = inject_docstring_text(doc, docstring_header, 1)
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