From: Stephen Rosen Date: Fri, 7 May 2021 19:45:47 +0000 (-0400) Subject: Improve cascade backrefs warning and add `code` to deprecation warnings X-Git-Tag: rel_1_4_15~2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=1a7d8005ccf4a48a3008caaf66b309be93287774;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Improve cascade backrefs warning and add `code` to deprecation warnings 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 --- diff --git a/doc/build/changelog/migration_14.rst b/doc/build/changelog/migration_14.rst index 1addfd15b8..ef0ca9b01a 100644 --- a/doc/build/changelog/migration_14.rst +++ b/doc/build/changelog/migration_14.rst @@ -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) diff --git a/doc/build/errors.rst b/doc/build/errors.rst index 3bd7b7a5b8..f2538b7053 100644 --- a/doc/build/errors.rst +++ b/doc/build/errors.rst @@ -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 ============================ diff --git a/doc/build/orm/cascades.rst b/doc/build/orm/cascades.rst index 51aba5b9c8..ead4f75e9b 100644 --- a/doc/build/orm/cascades.rst +++ b/doc/build/orm/cascades.rst @@ -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: diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index a0a86826d1..4501976e23 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -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.""" diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py index 1fb1c10acf..77bbb47510 100644 --- a/lib/sqlalchemy/orm/unitofwork.py +++ b/lib/sqlalchemy/orm/unitofwork.py @@ -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", ) diff --git a/lib/sqlalchemy/util/deprecations.py b/lib/sqlalchemy/util/deprecations.py index 19c55aa338..425a5f044f 100644 --- a/lib/sqlalchemy/util/deprecations.py +++ b/lib/sqlalchemy/util/deprecations.py @@ -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, ) diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index c5ca76f016..7796d41065 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -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): diff --git a/test/orm/test_cascade.py b/test/orm/test_cascade.py index 879ae2993a..7a1ac35576 100644 --- a/test/orm/test_cascade.py +++ b/test/orm/test_cascade.py @@ -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)