]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Improve cascade backrefs warning and add `code` to deprecation warnings
authorStephen Rosen <sirosen@globus.org>
Fri, 7 May 2021 19:45:47 +0000 (15:45 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 10 May 2021 23:02:35 +0000 (19:02 -0400)
This adds a new description to errors.rst and adds support
for any SQLAlchemy warning to refer to an errors.rst code.

Fixes: #6148
Closes: #6250
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/6250
Pull-request-sha: dbcaeb54e31517fe88f6f8c515f1024002675f13

Change-Id: I4303c62ac9b1f13f67a34f825687014f1771c98c

doc/build/changelog/migration_14.rst
doc/build/errors.rst
doc/build/orm/cascades.rst
lib/sqlalchemy/exc.py
lib/sqlalchemy/orm/unitofwork.py
lib/sqlalchemy/util/deprecations.py
lib/sqlalchemy/util/langhelpers.py
test/orm/test_cascade.py

index 1addfd15b8f6d5a2322f7ba888b4176b74d35cde..ef0ca9b01a661d2dddd1ab232d40013181807b13 100644 (file)
@@ -2185,16 +2185,18 @@ The above behavior was an unintended side effect of backref behavior, in that
 since ``a1.user`` implies ``u1.addresses.append(a1)``, ``a1`` would get
 cascaded into the :class:`_orm.Session`.  This remains the default behavior
 throughout 1.4.     At some point, a new flag :paramref:`_orm.relationship.cascade_backrefs`
-was added to disable to above behavior, as it can be surprising and also gets in
-the way of some operations where the object would be placed in the :class:`_orm.Session`
-too early and get prematurely flushed.
+was added to disable to above behavior, along with :paramref:`_orm.backref.cascade_backrefs`
+to set this when the relationship is specified by ``relationship.backref``, as it can be
+surprising and also gets in the way of some operations where the object would be placed in
+the :class:`_orm.Session` too early and get prematurely flushed.
 
 In 2.0, the default behavior will be that "cascade_backrefs" is False, and
 additionally there will be no "True" behavior as this is not generally a desirable
 behavior.    When 2.0 deprecation warnings are enabled, a warning will be emitted
 when a "backref cascade" actually takes place.    To get the new behavior, either
-set :paramref:`_orm.relationship.cascade_backrefs` to ``False`` on the target
-relationship, as is already supported in 1.3 and earlier, or alternatively make
+set :paramref:`_orm.relationship.cascade_backrefs` and
+:paramref:`_orm.backref.cascade_backrefs` to ``False`` on any target
+relationships, as is already supported in 1.3 and earlier, or alternatively make
 use of the :paramref:`_orm.Session.future` flag to :term:`2.0-style` mode::
 
     Session = sessionmaker(engine, future=True)
index 3bd7b7a5b80502e4cc2e7614a27bad345f0ca747..f2538b70532505b4867c2ab755f9e9d7d2a0c192 100644 (file)
@@ -172,6 +172,38 @@ In SQLAlchemy 1.4, this :term:`2.0 style` behavior is enabled when the
 :paramref:`_orm.Session.future` flag is set on :class:`_orm.sessionmaker`
 or :class:`_orm.Session`.
 
+.. _error_s9r1:
+
+Object is being merged into a Session along the backref cascade
+---------------------------------------------------------------
+
+This message refers to the "backref cascade" behavior of SQLAlchemy,
+which is described at :ref:`backref_cascade`.   This refers to the action of
+an object being added into a :class:`_orm.Session` as a result of another
+object that's already present in that session being associated with it.
+As this behavior has been shown to be more confusing than helpful,
+the :paramref:`_orm.relationship.cascade_backrefs` and
+:paramref:`_orm.backref.cascade_backrefs` parameters were added, which can
+be set to ``False`` to disable it, and in SQLAlchemy 2.0 the "cascade backrefs"
+behavior will be disabled completely.
+
+To set :paramref:`_orm.relationship.cascade_backrefs` to ``False`` on a
+backref that is currently configured using the
+:paramref:`_orm.relationship.backref` string parameter, the backref must
+be declared using the :func:`_orm.backref` function first so that the
+:paramref:`_orm.backref.cascade_backrefs` parameter may be passed.
+
+Alternatively, the entire "cascade backrefs" behavior can be turned off
+across the board by using the :class:`_orm.Session` in "future" mode,
+by passing ``True`` for the :paramref:`_orm.Session.future` parameter.
+
+.. seealso::
+
+    :ref:`backref_cascade` - complete description of the cascade backrefs
+    behavior
+
+    :ref:`change_5150` - background on the change for SQLAlchemy 2.0.
+
 Connections and Transactions
 ============================
 
index 51aba5b9c8d66be172ab2bcd5b8cf0b1bfc1e5a6..ead4f75e9bfb0b094d10d94505dda78bb22579ad 100644 (file)
@@ -610,7 +610,19 @@ option may be helpful for situations where an object needs to be kept out of a
 session until it's construction is completed, but still needs to be given
 associations to objects which are already persistent in the target session.
 
+When relationships are created by the :paramref:`_orm.relationship.backref`
+parameter on :func:`_orm.relationship`, the :paramref:`_orm.cascade_backrefs`
+parameter may be set to ``False`` on the backref side by using the
+:func:`_orm.backref` function instead of a string. For example, the above relationship
+could be declared::
 
+    mapper_registry.map_imperatively(Order, order_table, properties={
+        'items' : relationship(
+            Item, backref=backref('order', cascade_backrefs=False), cascade_backrefs=False
+        )
+    })
+
+This sets the ``cascade_backrefs=False`` behavior on both relationships.
 
 .. _session_deleting_from_collections:
 
index a0a86826d17d182c33f1ca6b7989612dcebd4ba0..4501976e23b34456b6909467604279aca8fe0b57 100644 (file)
@@ -19,8 +19,8 @@ from .util import compat
 _version_token = None
 
 
-class SQLAlchemyError(Exception):
-    """Generic error class."""
+class HasDescriptionCode(object):
+    """helper which adds 'code' as an attribute and '_code_str' as a method"""
 
     code = None
 
@@ -28,7 +28,7 @@ class SQLAlchemyError(Exception):
         code = kw.pop("code", None)
         if code is not None:
             self.code = code
-        super(SQLAlchemyError, self).__init__(*arg, **kw)
+        super(HasDescriptionCode, self).__init__(*arg, **kw)
 
     def _code_str(self):
         if not self.code:
@@ -43,6 +43,10 @@ class SQLAlchemyError(Exception):
                 )
             )
 
+
+class SQLAlchemyError(HasDescriptionCode, Exception):
+    """Generic error class."""
+
     def _message(self, as_unicode=compat.py3k):
         # rules:
         #
@@ -650,12 +654,18 @@ class NotSupportedError(DatabaseError):
 # Warnings
 
 
-class SADeprecationWarning(DeprecationWarning):
+class SADeprecationWarning(HasDescriptionCode, DeprecationWarning):
     """Issued for usage of deprecated APIs."""
 
     deprecated_since = None
     "Indicates the version that started raising this deprecation warning"
 
+    def __str__(self):
+        message = super(SADeprecationWarning, self).__str__()
+        if self.code:
+            message = "%s %s" % (message, self._code_str())
+        return message
+
 
 class RemovedIn20Warning(SADeprecationWarning):
     """Issued for usage of APIs specifically deprecated in SQLAlchemy 2.0.
@@ -671,6 +681,12 @@ class RemovedIn20Warning(SADeprecationWarning):
     deprecated_since = "1.4"
     "Indicates the version that started raising this deprecation warning"
 
+    def __str__(self):
+        return (
+            super(RemovedIn20Warning, self).__str__()
+            + " (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)"
+        )
+
 
 class MovedIn20Warning(RemovedIn20Warning):
     """Subtype of RemovedIn20Warning to indicate an API that moved only."""
@@ -686,5 +702,5 @@ class SAPendingDeprecationWarning(PendingDeprecationWarning):
     "Indicates the version that started raising this deprecation warning"
 
 
-class SAWarning(RuntimeWarning):
+class SAWarning(HasDescriptionCode, RuntimeWarning):
     """Issued at runtime."""
index 1fb1c10acf3896808a01a1c776dc8e0685814981..77bbb47510148b6aa20c6f7109a195bd8dc24574 100644 (file)
@@ -26,8 +26,10 @@ def _warn_for_cascade_backrefs(state, prop):
         '"%s" object is being merged into a Session along the backref '
         'cascade path for relationship "%s"; in SQLAlchemy 2.0, this '
         "reverse cascade will not take place.  Set cascade_backrefs to "
-        "False for the 2.0 behavior; or to set globally for the whole "
-        "Session, set the future=True flag" % (state.class_.__name__, prop)
+        "False in either the relationship() or backref() function for "
+        "the 2.0 behavior; or to set globally for the whole "
+        "Session, set the future=True flag" % (state.class_.__name__, prop),
+        code="s9r1",
     )
 
 
index 19c55aa338437d74fb882cee03887df726fd5464..425a5f044f3f78486423a9ec341a97c99eb911a1 100644 (file)
@@ -26,42 +26,42 @@ if os.getenv("SQLALCHEMY_WARN_20", "false").lower() in ("true", "yes", "1"):
     SQLALCHEMY_WARN_20 = True
 
 
-def _warn_with_version(msg, version, type_, stacklevel):
-    is_20 = issubclass(type_, exc.RemovedIn20Warning)
-
-    if is_20 and not SQLALCHEMY_WARN_20:
+def _warn_with_version(msg, version, type_, stacklevel, code=None):
+    if issubclass(type_, exc.RemovedIn20Warning) and not SQLALCHEMY_WARN_20:
         return
 
-    if is_20:
-        msg += " (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)"
-
-    warn = type_(msg)
+    warn = type_(msg, code=code)
     warn.deprecated_since = version
 
     _warnings_warn(warn, stacklevel=stacklevel + 1)
 
 
-def warn_deprecated(msg, version, stacklevel=3):
-    _warn_with_version(msg, version, exc.SADeprecationWarning, stacklevel)
+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):
+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)
+    _warn_with_version(
+        msg, version, exc.SADeprecationWarning, stacklevel, code=code
+    )
 
 
-def warn_deprecated_20(msg, stacklevel=3):
+def warn_deprecated_20(msg, stacklevel=3, code=None):
 
     _warn_with_version(
         msg,
         exc.RemovedIn20Warning.deprecated_since,
         exc.RemovedIn20Warning,
         stacklevel,
+        code=code,
     )
 
 
index c5ca76f016549582768d34c22d705eaa2800a827..7796d4106581fa25cd331ad5dd6c4008762c57cb 100644 (file)
@@ -1616,9 +1616,9 @@ def warn(msg, code=None):
 
     """
     if code:
-        msg = "%s %s" % (msg, exc.SQLAlchemyError(msg, code=code)._code_str())
-
-    _warnings_warn(msg, exc.SAWarning)
+        _warnings_warn(exc.SAWarning(msg, code=code))
+    else:
+        _warnings_warn(msg, exc.SAWarning)
 
 
 def warn_limited(msg, args):
index 879ae2993aab423d822e3cb1193ac16c5d4e60e8..7a1ac35576004b647990030cc5d3d5c1c0978f37 100644 (file)
@@ -1394,6 +1394,10 @@ class NoSaveCascadeFlushTest(_fixtures.FixtureTest):
             with testing.expect_deprecated(
                 '"Address" object is being merged into a Session along '
                 'the backref cascade path for relationship "User.addresses"'
+                # link added to this specific warning
+                r".*Background on this error at: http://sqlalche.me/e/14/s9r1"
+                # link added to all RemovedIn20Warnings
+                r".*Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9"
             ):
                 a1.user = u1
             sess.add(a1)