You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							417 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
	
	
							417 lines
						
					
					
						
							12 KiB
						
					
					
				# util/deprecations.py
 | 
						|
# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors
 | 
						|
# <see AUTHORS file>
 | 
						|
#
 | 
						|
# This module is part of SQLAlchemy and is released under
 | 
						|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
 | 
						|
 | 
						|
"""Helpers related to deprecation of functions, methods, classes, other
 | 
						|
functionality."""
 | 
						|
 | 
						|
import os
 | 
						|
import re
 | 
						|
 | 
						|
from . import compat
 | 
						|
from .langhelpers import _hash_limit_string
 | 
						|
from .langhelpers import _warnings_warn
 | 
						|
from .langhelpers import decorator
 | 
						|
from .langhelpers import inject_docstring_text
 | 
						|
from .langhelpers import inject_param_text
 | 
						|
from .. import exc
 | 
						|
 | 
						|
 | 
						|
SQLALCHEMY_WARN_20 = False
 | 
						|
 | 
						|
if os.getenv("SQLALCHEMY_WARN_20", "false").lower() in ("true", "yes", "1"):
 | 
						|
    SQLALCHEMY_WARN_20 = True
 | 
						|
 | 
						|
 | 
						|
def _warn_with_version(msg, version, type_, stacklevel, code=None):
 | 
						|
    if (
 | 
						|
        issubclass(type_, exc.Base20DeprecationWarning)
 | 
						|
        and not SQLALCHEMY_WARN_20
 | 
						|
    ):
 | 
						|
        return
 | 
						|
 | 
						|
    warn = type_(msg, code=code)
 | 
						|
    warn.deprecated_since = version
 | 
						|
 | 
						|
    _warnings_warn(warn, stacklevel=stacklevel + 1)
 | 
						|
 | 
						|
 | 
						|
def warn_deprecated(msg, version, stacklevel=3, code=None):
 | 
						|
    _warn_with_version(
 | 
						|
        msg, version, exc.SADeprecationWarning, stacklevel, code=code
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def warn_deprecated_limited(msg, args, version, stacklevel=3, code=None):
 | 
						|
    """Issue a deprecation warning with a parameterized string,
 | 
						|
    limiting the number of registrations.
 | 
						|
 | 
						|
    """
 | 
						|
    if args:
 | 
						|
        msg = _hash_limit_string(msg, 10, args)
 | 
						|
    _warn_with_version(
 | 
						|
        msg, version, exc.SADeprecationWarning, stacklevel, code=code
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def warn_deprecated_20(msg, stacklevel=3, code=None):
 | 
						|
 | 
						|
    _warn_with_version(
 | 
						|
        msg,
 | 
						|
        exc.RemovedIn20Warning.deprecated_since,
 | 
						|
        exc.RemovedIn20Warning,
 | 
						|
        stacklevel,
 | 
						|
        code=code,
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def deprecated_cls(version, message, constructor="__init__"):
 | 
						|
    header = ".. deprecated:: %s %s" % (version, (message or ""))
 | 
						|
 | 
						|
    def decorate(cls):
 | 
						|
        return _decorate_cls_with_warning(
 | 
						|
            cls,
 | 
						|
            constructor,
 | 
						|
            exc.SADeprecationWarning,
 | 
						|
            message % dict(func=constructor),
 | 
						|
            version,
 | 
						|
            header,
 | 
						|
        )
 | 
						|
 | 
						|
    return decorate
 | 
						|
 | 
						|
 | 
						|
def deprecated_20_cls(
 | 
						|
    clsname, alternative=None, constructor="__init__", becomes_legacy=False
 | 
						|
):
 | 
						|
    message = (
 | 
						|
        ".. deprecated:: 1.4 The %s class is considered legacy as of the "
 | 
						|
        "1.x series of SQLAlchemy and %s in 2.0."
 | 
						|
        % (
 | 
						|
            clsname,
 | 
						|
            "will be removed"
 | 
						|
            if not becomes_legacy
 | 
						|
            else "becomes a legacy construct",
 | 
						|
        )
 | 
						|
    )
 | 
						|
 | 
						|
    if alternative:
 | 
						|
        message += " " + alternative
 | 
						|
 | 
						|
    if becomes_legacy:
 | 
						|
        warning_cls = exc.LegacyAPIWarning
 | 
						|
    else:
 | 
						|
        warning_cls = exc.RemovedIn20Warning
 | 
						|
 | 
						|
    def decorate(cls):
 | 
						|
        return _decorate_cls_with_warning(
 | 
						|
            cls,
 | 
						|
            constructor,
 | 
						|
            warning_cls,
 | 
						|
            message,
 | 
						|
            warning_cls.deprecated_since,
 | 
						|
            message,
 | 
						|
        )
 | 
						|
 | 
						|
    return decorate
 | 
						|
 | 
						|
 | 
						|
def deprecated(
 | 
						|
    version,
 | 
						|
    message=None,
 | 
						|
    add_deprecation_to_docstring=True,
 | 
						|
    warning=None,
 | 
						|
    enable_warnings=True,
 | 
						|
):
 | 
						|
    """Decorates a function and issues a deprecation warning on use.
 | 
						|
 | 
						|
    :param version:
 | 
						|
      Issue version in the warning.
 | 
						|
 | 
						|
    :param message:
 | 
						|
      If provided, issue message in the warning.  A sensible default
 | 
						|
      is used if not provided.
 | 
						|
 | 
						|
    :param add_deprecation_to_docstring:
 | 
						|
      Default True.  If False, the wrapped function's __doc__ is left
 | 
						|
      as-is.  If True, the 'message' is prepended to the docs if
 | 
						|
      provided, or sensible default if message is omitted.
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    # nothing is deprecated "since" 2.0 at this time.  All "removed in 2.0"
 | 
						|
    # should emit the RemovedIn20Warning, but messaging should be expressed
 | 
						|
    # in terms of "deprecated since 1.4".
 | 
						|
 | 
						|
    if version == "2.0":
 | 
						|
        if warning is None:
 | 
						|
            warning = exc.RemovedIn20Warning
 | 
						|
        version = "1.4"
 | 
						|
    if add_deprecation_to_docstring:
 | 
						|
        header = ".. deprecated:: %s %s" % (
 | 
						|
            version,
 | 
						|
            (message or ""),
 | 
						|
        )
 | 
						|
    else:
 | 
						|
        header = None
 | 
						|
 | 
						|
    if message is None:
 | 
						|
        message = "Call to deprecated function %(func)s"
 | 
						|
 | 
						|
    if warning is None:
 | 
						|
        warning = exc.SADeprecationWarning
 | 
						|
 | 
						|
    if warning is not exc.RemovedIn20Warning:
 | 
						|
        message += " (deprecated since: %s)" % version
 | 
						|
 | 
						|
    def decorate(fn):
 | 
						|
        return _decorate_with_warning(
 | 
						|
            fn,
 | 
						|
            warning,
 | 
						|
            message % dict(func=fn.__name__),
 | 
						|
            version,
 | 
						|
            header,
 | 
						|
            enable_warnings=enable_warnings,
 | 
						|
        )
 | 
						|
 | 
						|
    return decorate
 | 
						|
 | 
						|
 | 
						|
def moved_20(message, **kw):
 | 
						|
    return deprecated(
 | 
						|
        "2.0", message=message, warning=exc.MovedIn20Warning, **kw
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def deprecated_20(api_name, alternative=None, becomes_legacy=False, **kw):
 | 
						|
    type_reg = re.match("^:(attr|func|meth):", api_name)
 | 
						|
    if type_reg:
 | 
						|
        type_ = {"attr": "attribute", "func": "function", "meth": "method"}[
 | 
						|
            type_reg.group(1)
 | 
						|
        ]
 | 
						|
    else:
 | 
						|
        type_ = "construct"
 | 
						|
    message = (
 | 
						|
        "The %s %s is considered legacy as of the "
 | 
						|
        "1.x series of SQLAlchemy and %s in 2.0."
 | 
						|
        % (
 | 
						|
            api_name,
 | 
						|
            type_,
 | 
						|
            "will be removed"
 | 
						|
            if not becomes_legacy
 | 
						|
            else "becomes a legacy construct",
 | 
						|
        )
 | 
						|
    )
 | 
						|
 | 
						|
    if ":attr:" in api_name:
 | 
						|
        attribute_ok = kw.pop("warn_on_attribute_access", False)
 | 
						|
        if not attribute_ok:
 | 
						|
            assert kw.get("enable_warnings") is False, (
 | 
						|
                "attribute %s will emit a warning on read access.  "
 | 
						|
                "If you *really* want this, "
 | 
						|
                "add warn_on_attribute_access=True.  Otherwise please add "
 | 
						|
                "enable_warnings=False." % api_name
 | 
						|
            )
 | 
						|
 | 
						|
    if alternative:
 | 
						|
        message += " " + alternative
 | 
						|
 | 
						|
    if becomes_legacy:
 | 
						|
        warning_cls = exc.LegacyAPIWarning
 | 
						|
    else:
 | 
						|
        warning_cls = exc.RemovedIn20Warning
 | 
						|
 | 
						|
    return deprecated("2.0", message=message, warning=warning_cls, **kw)
 | 
						|
 | 
						|
 | 
						|
def deprecated_params(**specs):
 | 
						|
    """Decorates a function to warn on use of certain parameters.
 | 
						|
 | 
						|
    e.g. ::
 | 
						|
 | 
						|
        @deprecated_params(
 | 
						|
            weak_identity_map=(
 | 
						|
                "0.7",
 | 
						|
                "the :paramref:`.Session.weak_identity_map parameter "
 | 
						|
                "is deprecated."
 | 
						|
            )
 | 
						|
 | 
						|
        )
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    messages = {}
 | 
						|
    versions = {}
 | 
						|
    version_warnings = {}
 | 
						|
 | 
						|
    for param, (version, message) in specs.items():
 | 
						|
        versions[param] = version
 | 
						|
        messages[param] = _sanitize_restructured_text(message)
 | 
						|
        version_warnings[param] = (
 | 
						|
            exc.RemovedIn20Warning
 | 
						|
            if version == "2.0"
 | 
						|
            else exc.SADeprecationWarning
 | 
						|
        )
 | 
						|
 | 
						|
    def decorate(fn):
 | 
						|
        spec = compat.inspect_getfullargspec(fn)
 | 
						|
 | 
						|
        if spec.defaults is not None:
 | 
						|
            defaults = dict(
 | 
						|
                zip(
 | 
						|
                    spec.args[(len(spec.args) - len(spec.defaults)) :],
 | 
						|
                    spec.defaults,
 | 
						|
                )
 | 
						|
            )
 | 
						|
            check_defaults = set(defaults).intersection(messages)
 | 
						|
            check_kw = set(messages).difference(defaults)
 | 
						|
        else:
 | 
						|
            check_defaults = ()
 | 
						|
            check_kw = set(messages)
 | 
						|
 | 
						|
        check_any_kw = spec.varkw
 | 
						|
 | 
						|
        @decorator
 | 
						|
        def warned(fn, *args, **kwargs):
 | 
						|
            for m in check_defaults:
 | 
						|
                if (defaults[m] is None and kwargs[m] is not None) or (
 | 
						|
                    defaults[m] is not None and kwargs[m] != defaults[m]
 | 
						|
                ):
 | 
						|
                    _warn_with_version(
 | 
						|
                        messages[m],
 | 
						|
                        versions[m],
 | 
						|
                        version_warnings[m],
 | 
						|
                        stacklevel=3,
 | 
						|
                    )
 | 
						|
 | 
						|
            if check_any_kw in messages and set(kwargs).difference(
 | 
						|
                check_defaults
 | 
						|
            ):
 | 
						|
 | 
						|
                _warn_with_version(
 | 
						|
                    messages[check_any_kw],
 | 
						|
                    versions[check_any_kw],
 | 
						|
                    version_warnings[check_any_kw],
 | 
						|
                    stacklevel=3,
 | 
						|
                )
 | 
						|
 | 
						|
            for m in check_kw:
 | 
						|
                if m in kwargs:
 | 
						|
                    _warn_with_version(
 | 
						|
                        messages[m],
 | 
						|
                        versions[m],
 | 
						|
                        version_warnings[m],
 | 
						|
                        stacklevel=3,
 | 
						|
                    )
 | 
						|
            return fn(*args, **kwargs)
 | 
						|
 | 
						|
        doc = fn.__doc__ is not None and fn.__doc__ or ""
 | 
						|
        if doc:
 | 
						|
            doc = inject_param_text(
 | 
						|
                doc,
 | 
						|
                {
 | 
						|
                    param: ".. deprecated:: %s %s"
 | 
						|
                    % ("1.4" if version == "2.0" else version, (message or ""))
 | 
						|
                    for param, (version, message) in specs.items()
 | 
						|
                },
 | 
						|
            )
 | 
						|
        decorated = warned(fn)
 | 
						|
        decorated.__doc__ = doc
 | 
						|
        return decorated
 | 
						|
 | 
						|
    return decorate
 | 
						|
 | 
						|
 | 
						|
def _sanitize_restructured_text(text):
 | 
						|
    def repl(m):
 | 
						|
        type_, name = m.group(1, 2)
 | 
						|
        if type_ in ("func", "meth"):
 | 
						|
            name += "()"
 | 
						|
        return name
 | 
						|
 | 
						|
    text = re.sub(r":ref:`(.+) <.*>`", lambda m: '"%s"' % m.group(1), text)
 | 
						|
    return re.sub(r"\:(\w+)\:`~?(?:_\w+)?\.?(.+?)`", repl, text)
 | 
						|
 | 
						|
 | 
						|
def _decorate_cls_with_warning(
 | 
						|
    cls, constructor, wtype, message, version, docstring_header=None
 | 
						|
):
 | 
						|
    doc = cls.__doc__ is not None and cls.__doc__ or ""
 | 
						|
    if docstring_header is not None:
 | 
						|
 | 
						|
        if constructor is not None:
 | 
						|
            docstring_header %= dict(func=constructor)
 | 
						|
 | 
						|
        if issubclass(wtype, exc.Base20DeprecationWarning):
 | 
						|
            docstring_header += (
 | 
						|
                " (Background on SQLAlchemy 2.0 at: "
 | 
						|
                ":ref:`migration_20_toplevel`)"
 | 
						|
            )
 | 
						|
        doc = inject_docstring_text(doc, docstring_header, 1)
 | 
						|
 | 
						|
        if type(cls) is type:
 | 
						|
            clsdict = dict(cls.__dict__)
 | 
						|
            clsdict["__doc__"] = doc
 | 
						|
            clsdict.pop("__dict__", None)
 | 
						|
            cls = type(cls.__name__, cls.__bases__, clsdict)
 | 
						|
            if constructor is not None:
 | 
						|
                constructor_fn = clsdict[constructor]
 | 
						|
 | 
						|
        else:
 | 
						|
            cls.__doc__ = doc
 | 
						|
            if constructor is not None:
 | 
						|
                constructor_fn = getattr(cls, constructor)
 | 
						|
 | 
						|
        if constructor is not None:
 | 
						|
            setattr(
 | 
						|
                cls,
 | 
						|
                constructor,
 | 
						|
                _decorate_with_warning(
 | 
						|
                    constructor_fn, wtype, message, version, None
 | 
						|
                ),
 | 
						|
            )
 | 
						|
    return cls
 | 
						|
 | 
						|
 | 
						|
def _decorate_with_warning(
 | 
						|
    func, wtype, message, version, docstring_header=None, enable_warnings=True
 | 
						|
):
 | 
						|
    """Wrap a function with a warnings.warn and augmented docstring."""
 | 
						|
 | 
						|
    message = _sanitize_restructured_text(message)
 | 
						|
 | 
						|
    if issubclass(wtype, exc.Base20DeprecationWarning):
 | 
						|
        doc_only = (
 | 
						|
            " (Background on SQLAlchemy 2.0 at: "
 | 
						|
            ":ref:`migration_20_toplevel`)"
 | 
						|
        )
 | 
						|
    else:
 | 
						|
        doc_only = ""
 | 
						|
 | 
						|
    @decorator
 | 
						|
    def warned(fn, *args, **kwargs):
 | 
						|
        skip_warning = not enable_warnings or kwargs.pop(
 | 
						|
            "_sa_skip_warning", False
 | 
						|
        )
 | 
						|
        if not skip_warning:
 | 
						|
            _warn_with_version(message, version, wtype, stacklevel=3)
 | 
						|
        return fn(*args, **kwargs)
 | 
						|
 | 
						|
    doc = func.__doc__ is not None and func.__doc__ or ""
 | 
						|
    if docstring_header is not None:
 | 
						|
        docstring_header %= dict(func=func.__name__)
 | 
						|
 | 
						|
        docstring_header += doc_only
 | 
						|
 | 
						|
        doc = inject_docstring_text(doc, docstring_header, 1)
 | 
						|
 | 
						|
    decorated = warned(func)
 | 
						|
    decorated.__doc__ = doc
 | 
						|
    decorated._sa_warn = lambda: _warn_with_version(
 | 
						|
        message, version, wtype, stacklevel=3
 | 
						|
    )
 | 
						|
    return decorated
 |