]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
calculate warnings stacklevels dynamically
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 7 May 2021 17:01:40 +0000 (13:01 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 10 May 2021 19:19:17 +0000 (15:19 -0400)
A new approach has been applied to the warnings system in SQLAlchemy to
accurately predict the appropriate stack level for each warning
dynamically. This allows evaluating the source of SQLAlchemy-generated
warnings and deprecation warnings to be more straightforward as the warning
will indicate the source line within end-user code, rather than from an
arbitrary level within SQLAlchemy's own source code.

Fixes: #6241
Change-Id: I9ecf3b3ea77424d15e8d4c0aa47350602c0568d7

doc/build/changelog/unreleased_14/6241.rst [new file with mode: 0644]
lib/sqlalchemy/orm/instrumentation.py
lib/sqlalchemy/util/deprecations.py
lib/sqlalchemy/util/langhelpers.py

diff --git a/doc/build/changelog/unreleased_14/6241.rst b/doc/build/changelog/unreleased_14/6241.rst
new file mode 100644 (file)
index 0000000..a30816b
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: feature, general
+    :tickets: 6241
+
+    A new approach has been applied to the warnings system in SQLAlchemy to
+    accurately predict the appropriate stack level for each warning
+    dynamically. This allows evaluating the source of SQLAlchemy-generated
+    warnings and deprecation warnings to be more straightforward as the warning
+    will indicate the source line within end-user code, rather than from an
+    arbitrary level within SQLAlchemy's own source code.
index c970bee22ac1be9ddf830096481dd14975cc5ddb..1edcab72a83f738d25040de709ea27ba34b3fee1 100644 (file)
@@ -636,6 +636,7 @@ def __init__(%(apply_pos)s):
         func_kw_defaults = getattr(original_init, "__kwdefaults__", None)
 
     env = locals().copy()
+    env["__name__"] = __name__
     exec(func_text, env)
     __init__ = env["__init__"]
     __init__.__doc__ = original_init.__doc__
index 5d55a3ae611d5312802e220c3c9744781d86dcf7..19c55aa338437d74fb882cee03887df726fd5464 100644 (file)
@@ -10,10 +10,10 @@ functionality."""
 
 import os
 import re
-import warnings
 
 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
@@ -38,7 +38,7 @@ def _warn_with_version(msg, version, type_, stacklevel):
     warn = type_(msg)
     warn.deprecated_since = version
 
-    warnings.warn(warn, stacklevel=stacklevel + 1)
+    _warnings_warn(warn, stacklevel=stacklevel + 1)
 
 
 def warn_deprecated(msg, version, stacklevel=3):
index b31f316fee3090f69b17ba33a29b8e2103e00275..c5ca76f016549582768d34c22d705eaa2800a827 100644 (file)
@@ -193,7 +193,7 @@ def %(name)s(%(args)s):
 """
             % metadata
         )
-        env.update({targ_name: target, fn_name: fn})
+        env.update({targ_name: target, fn_name: fn, "__name__": fn.__module__})
 
         decorated = _exec_code_in_env(code, env, fn.__name__)
         decorated.__defaults__ = getattr(fn, "__func__", fn).__defaults__
@@ -269,7 +269,11 @@ def %(name)s(%(args)s):
 """
         % metadata
     )
-    env = {"cls": callable_, "symbol": symbol}
+    env = {
+        "cls": callable_,
+        "symbol": symbol,
+        "__name__": callable_.__module__,
+    }
     exec(code, env)
     decorated = env[location_name]
 
@@ -626,7 +630,7 @@ def create_proxy_methods(
         def instrument(name, clslevel=False):
             fn = getattr(target_cls, name)
             spec = compat.inspect_getfullargspec(fn)
-            env = {}
+            env = {"__name__": fn.__module__}
 
             spec = _update_argspec_defaults_into_env(spec, env)
             caller_argspec = format_argspec_plus(spec, grouped=False)
@@ -1614,7 +1618,7 @@ def warn(msg, code=None):
     if code:
         msg = "%s %s" % (msg, exc.SQLAlchemyError(msg, code=code)._code_str())
 
-    warnings.warn(msg, exc.SAWarning, stacklevel=2)
+    _warnings_warn(msg, exc.SAWarning)
 
 
 def warn_limited(msg, args):
@@ -1624,7 +1628,36 @@ def warn_limited(msg, args):
     """
     if args:
         msg = _hash_limit_string(msg, 10, args)
-    warnings.warn(msg, exc.SAWarning, stacklevel=2)
+    _warnings_warn(msg, exc.SAWarning)
+
+
+def _warnings_warn(message, category=None, stacklevel=2):
+
+    # adjust the given stacklevel to be outside of SQLAlchemy
+    try:
+        frame = sys._getframe(stacklevel)
+    except ValueError:
+        # being called from less than 3 (or given) stacklevels, weird,
+        # but don't crash
+        stacklevel = 0
+    except:
+        # _getframe() doesn't work, weird interpreter issue, weird,
+        # ok, but don't crash
+        stacklevel = 0
+    else:
+        # using __name__ here requires that we have __name__ in the
+        # __globals__ of the decorated string functions we make also.
+        # we generate this using {"__name__": fn.__module__}
+        while frame is not None and re.match(
+            r"^(?:sqlalchemy\.|alembic\.)", frame.f_globals.get("__name__", "")
+        ):
+            frame = frame.f_back
+            stacklevel += 1
+
+    if category is not None:
+        warnings.warn(message, category, stacklevel=stacklevel + 1)
+    else:
+        warnings.warn(message, stacklevel=stacklevel + 1)
 
 
 def only_once(fn, retry_on_exception):