From: Federico Caselli Date: Fri, 24 Jan 2025 22:00:06 +0000 (+0100) Subject: The ``noload`` loader option is now deprecated. X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=32427ad333ce44851c8c750b0872c89cd8c104cb;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git The ``noload`` loader option is now deprecated. Fixes: #11045 Change-Id: If77517926eda71f92cd92b2d22a69a5ee172274e --- diff --git a/doc/build/changelog/unreleased_21/11045.rst b/doc/build/changelog/unreleased_21/11045.rst new file mode 100644 index 0000000000..8788d33d79 --- /dev/null +++ b/doc/build/changelog/unreleased_21/11045.rst @@ -0,0 +1,8 @@ +.. change:: + :tags: orm + :tickets: 11045 + + The :func:`_orm.noload` relationship loader option and related + ``lazy='noload'`` setting is deprecated and will be removed in a future + release. This option was originally intended for custom loader patterns + that are no longer applicable in modern SQLAlchemy. diff --git a/lib/sqlalchemy/orm/_orm_constructors.py b/lib/sqlalchemy/orm/_orm_constructors.py index 9e42a834fa..b2acc93b43 100644 --- a/lib/sqlalchemy/orm/_orm_constructors.py +++ b/lib/sqlalchemy/orm/_orm_constructors.py @@ -1426,11 +1426,6 @@ def relationship( issues a JOIN to the immediate parent object, specifying primary key identifiers using an IN clause. - * ``noload`` - no loading should occur at any time. The related - collection will remain empty. The ``noload`` strategy is not - recommended for general use. For a general use "never load" - approach, see :ref:`write_only_relationship` - * ``raise`` - lazy loading is disallowed; accessing the attribute, if its value were not already loaded via eager loading, will raise an :exc:`~sqlalchemy.exc.InvalidRequestError`. @@ -1493,6 +1488,13 @@ def relationship( :ref:`write_only_relationship` - more generally useful approach for large collections that should not fully load into memory + * ``noload`` - no loading should occur at any time. The related + collection will remain empty. + + .. deprecated:: 2.1 The ``noload`` loader strategy is deprecated and + will be removed in a future release. This option produces incorrect + results by returning ``None`` for related items. + * True - a synonym for 'select' * False - a synonym for 'joined' diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 8a530399dc..8a5d1af961 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -638,6 +638,13 @@ class _NoLoader(_AbstractRelationshipLoader): __slots__ = () + @util.deprecated( + "2.1", + "The ``noload`` loader strategy is deprecated and will be removed " + "in a future release. This option " + "produces incorrect results by returning ``None`` for related " + "items.", + ) def init_class_attribute(self, mapper): self.is_class_level = True diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index 4ecbfd64c1..5d21237198 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -478,6 +478,13 @@ class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption): ) return loader + @util.deprecated( + "2.1", + "The :func:`_orm.noload` option is deprecated and will be removed " + "in a future release. This option " + "produces incorrect results by returning ``None`` for related " + "items.", + ) def noload(self, attr: _AttrType) -> Self: """Indicate that the given relationship attribute should remain unloaded. @@ -485,17 +492,9 @@ class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption): The relationship attribute will return ``None`` when accessed without producing any loading effect. - This function is part of the :class:`_orm.Load` interface and supports - both method-chained and standalone operation. - :func:`_orm.noload` applies to :func:`_orm.relationship` attributes only. - .. legacy:: The :func:`_orm.noload` option is **legacy**. As it - forces collections to be empty, which invariably leads to - non-intuitive and difficult to predict results. There are no - legitimate uses for this option in modern SQLAlchemy. - .. seealso:: :ref:`loading_toplevel` diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py index 8364c15f8f..effe50d481 100644 --- a/lib/sqlalchemy/testing/assertions.py +++ b/lib/sqlalchemy/testing/assertions.py @@ -89,6 +89,12 @@ def expect_deprecated(*messages, **kw): ) +def expect_noload_deprecation(): + return expect_deprecated( + r"The (?:``noload`` loader strategy|noload\(\) option) is deprecated." + ) + + def expect_deprecated_20(*messages, **kw): return _expect_warnings_sqla_only( sa_exc.Base20DeprecationWarning, messages, **kw diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 19c1cc21e3..f7879d55c0 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -1846,6 +1846,10 @@ def _warnings_warn( category: Optional[Type[Warning]] = None, stacklevel: int = 2, ) -> None: + + if category is None and isinstance(message, Warning): + category = type(message) + # adjust the given stacklevel to be outside of SQLAlchemy try: frame = sys._getframe(stacklevel) diff --git a/test/orm/inheritance/test_assorted_poly.py b/test/orm/inheritance/test_assorted_poly.py index ab06dbaea3..2b15b74251 100644 --- a/test/orm/inheritance/test_assorted_poly.py +++ b/test/orm/inheritance/test_assorted_poly.py @@ -41,6 +41,7 @@ from sqlalchemy.testing import config from sqlalchemy.testing import eq_ from sqlalchemy.testing import expect_warnings from sqlalchemy.testing import fixtures +from sqlalchemy.testing.assertions import expect_noload_deprecation from sqlalchemy.testing.entities import ComparableEntity from sqlalchemy.testing.fixtures import fixture_session from sqlalchemy.testing.provision import normalize_sequence @@ -2355,7 +2356,8 @@ class CorrelateExceptWPolyAdaptTest( def test_poly_query_on_correlate(self): Common, Superclass = self._fixture(False) - poly = with_polymorphic(Superclass, "*") + with expect_noload_deprecation(): + poly = with_polymorphic(Superclass, "*") s = fixture_session() q = ( @@ -2384,7 +2386,8 @@ class CorrelateExceptWPolyAdaptTest( def test_poly_query_on_correlate_except(self): Common, Superclass = self._fixture(True) - poly = with_polymorphic(Superclass, "*") + with expect_noload_deprecation(): + poly = with_polymorphic(Superclass, "*") s = fixture_session() q = ( diff --git a/test/orm/test_ac_relationships.py b/test/orm/test_ac_relationships.py index 603e71d249..34f4e37ee4 100644 --- a/test/orm/test_ac_relationships.py +++ b/test/orm/test_ac_relationships.py @@ -17,6 +17,7 @@ from sqlalchemy.orm import Session from sqlalchemy.testing import eq_ from sqlalchemy.testing import expect_warnings from sqlalchemy.testing import fixtures +from sqlalchemy.testing.assertions import expect_noload_deprecation from sqlalchemy.testing.assertions import expect_raises_message from sqlalchemy.testing.assertsql import CompiledSQL from sqlalchemy.testing.entities import ComparableEntity @@ -142,7 +143,8 @@ class AliasedClassRelationshipTest( for b in a1.partitioned_bs: eq_(b.cs, []) - self.assert_sql_count(testing.db, go, 2) + with expect_noload_deprecation(): + self.assert_sql_count(testing.db, go, 2) @testing.combinations("ac_attribute", "ac_attr_w_of_type") def test_selectinload_w_joinedload_after(self, calling_style): diff --git a/test/orm/test_default_strategies.py b/test/orm/test_default_strategies.py index 178b03fe6f..c1989d1f69 100644 --- a/test/orm/test_default_strategies.py +++ b/test/orm/test_default_strategies.py @@ -18,7 +18,7 @@ from sqlalchemy.testing.fixtures import fixture_session from test.orm import _fixtures -class DefaultStrategyOptionsTest(_fixtures.FixtureTest): +class DefaultStrategyOptionsTestFixtures(_fixtures.FixtureTest): def _assert_fully_loaded(self, users): # verify everything loaded, with no additional sql needed def go(): @@ -193,6 +193,9 @@ class DefaultStrategyOptionsTest(_fixtures.FixtureTest): return fixture_session() + +class DefaultStrategyOptionsTest(DefaultStrategyOptionsTestFixtures): + def test_downgrade_baseline(self): """Mapper strategy defaults load as expected (compare to rest of DefaultStrategyOptionsTest downgrade tests).""" @@ -368,67 +371,6 @@ class DefaultStrategyOptionsTest(_fixtures.FixtureTest): # lastly, make sure they actually loaded properly eq_(users, self.static.user_all_result) - def test_noload_with_joinedload(self): - """Mapper load strategy defaults can be downgraded with - noload('*') option, while explicit joinedload() option - is still honored""" - sess = self._downgrade_fixture() - users = [] - - # test noload('*') shuts off 'orders' subquery, only 1 sql - def go(): - users[:] = ( - sess.query(self.classes.User) - .options(sa.orm.noload("*")) - .options(joinedload(self.classes.User.addresses)) - .order_by(self.classes.User.id) - .all() - ) - - self.assert_sql_count(testing.db, go, 1) - - # verify all the addresses were joined loaded (no more sql) - self._assert_addresses_loaded(users) - - # User.orders should have loaded "noload" (meaning []) - def go(): - for u in users: - assert u.orders == [] - - self.assert_sql_count(testing.db, go, 0) - - def test_noload_with_subqueryload(self): - """Mapper load strategy defaults can be downgraded with - noload('*') option, while explicit subqueryload() option - is still honored""" - sess = self._downgrade_fixture() - users = [] - - # test noload('*') option combined with subqueryload() - # shuts off 'addresses' load AND orders.items load: 2 sql expected - def go(): - users[:] = ( - sess.query(self.classes.User) - .options(sa.orm.noload("*")) - .options(subqueryload(self.classes.User.orders)) - .order_by(self.classes.User.id) - .all() - ) - - self.assert_sql_count(testing.db, go, 2) - - def go(): - # Verify orders have already been loaded: 0 sql - for u, static in zip(users, self.static.user_all_result): - assert len(u.orders) == len(static.orders) - # Verify noload('*') prevented orders.items load - # and set 'items' to [] - for u in users: - for o in u.orders: - assert o.items == [] - - self.assert_sql_count(testing.db, go, 0) - def test_joined(self): """Mapper load strategy defaults can be upgraded with joinedload('*') option.""" @@ -654,99 +596,6 @@ class DefaultStrategyOptionsTest(_fixtures.FixtureTest): self._assert_fully_loaded(users) -class NoLoadTest(_fixtures.FixtureTest): - run_inserts = "once" - run_deletes = None - - def test_o2m_noload(self): - Address, addresses, users, User = ( - self.classes.Address, - self.tables.addresses, - self.tables.users, - self.classes.User, - ) - - m = self.mapper_registry.map_imperatively( - User, - users, - properties=dict( - addresses=relationship( - self.mapper_registry.map_imperatively(Address, addresses), - lazy="noload", - ) - ), - ) - q = fixture_session().query(m) - result = [None] - - def go(): - x = q.filter(User.id == 7).all() - x[0].addresses - result[0] = x - - self.assert_sql_count(testing.db, go, 1) - - self.assert_result( - result[0], User, {"id": 7, "addresses": (Address, [])} - ) - - def test_upgrade_o2m_noload_lazyload_option(self): - Address, addresses, users, User = ( - self.classes.Address, - self.tables.addresses, - self.tables.users, - self.classes.User, - ) - - m = self.mapper_registry.map_imperatively( - User, - users, - properties=dict( - addresses=relationship( - self.mapper_registry.map_imperatively(Address, addresses), - lazy="noload", - ) - ), - ) - q = fixture_session().query(m).options(sa.orm.lazyload(User.addresses)) - result = [None] - - def go(): - x = q.filter(User.id == 7).all() - x[0].addresses - result[0] = x - - self.sql_count_(2, go) - - self.assert_result( - result[0], User, {"id": 7, "addresses": (Address, [{"id": 1}])} - ) - - def test_m2o_noload_option(self): - Address, addresses, users, User = ( - self.classes.Address, - self.tables.addresses, - self.tables.users, - self.classes.User, - ) - self.mapper_registry.map_imperatively( - Address, addresses, properties={"user": relationship(User)} - ) - self.mapper_registry.map_imperatively(User, users) - s = fixture_session() - a1 = ( - s.query(Address) - .filter_by(id=1) - .options(sa.orm.noload(Address.user)) - .first() - ) - - def go(): - eq_(a1.user, None) - - self.sql_count_(0, go) - - class Issue11292Test(fixtures.DeclarativeMappedTest): @classmethod def setup_classes(cls): diff --git a/test/orm/test_deprecations.py b/test/orm/test_deprecations.py index b99bc643a1..fa04a19d3e 100644 --- a/test/orm/test_deprecations.py +++ b/test/orm/test_deprecations.py @@ -40,12 +40,15 @@ from sqlalchemy.orm import relationship from sqlalchemy.orm import scoped_session from sqlalchemy.orm import Session from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import strategies from sqlalchemy.orm import subqueryload from sqlalchemy.orm import synonym from sqlalchemy.orm import undefer from sqlalchemy.orm import with_parent from sqlalchemy.orm import with_polymorphic from sqlalchemy.orm.collections import collection +from sqlalchemy.orm.strategy_options import lazyload +from sqlalchemy.orm.strategy_options import noload from sqlalchemy.testing import assert_raises_message from sqlalchemy.testing import assertions from sqlalchemy.testing import AssertsCompiledSQL @@ -56,6 +59,8 @@ from sqlalchemy.testing import expect_raises_message from sqlalchemy.testing import fixtures from sqlalchemy.testing import is_ from sqlalchemy.testing import mock +from sqlalchemy.testing.assertions import expect_noload_deprecation +from sqlalchemy.testing.assertions import in_ from sqlalchemy.testing.entities import ComparableEntity from sqlalchemy.testing.fixtures import CacheKeyFixture from sqlalchemy.testing.fixtures import fixture_session @@ -65,18 +70,15 @@ from . import _fixtures from .inheritance import _poly_fixtures from .inheritance._poly_fixtures import Manager from .inheritance._poly_fixtures import Person +from .test_default_strategies import DefaultStrategyOptionsTestFixtures from .test_deferred import InheritanceTest as _deferred_InheritanceTest +from .test_dynamic import _DynamicFixture +from .test_dynamic import _WriteOnlyFixture from .test_options import PathTest as OptionsPathTest from .test_options import PathTest from .test_options import QueryTest as OptionsQueryTest from .test_query import QueryTest -if True: - # hack - zimports won't stop reformatting this to be too-long for now - from .test_default_strategies import ( - DefaultStrategyOptionsTest as _DefaultStrategyOptionsTest, - ) - join_aliased_dep = ( r"The ``aliased`` and ``from_joinpoint`` keyword arguments to " r"Query.join\(\)" @@ -2732,7 +2734,7 @@ class MergeResultTest(_fixtures.FixtureTest): ) -class DefaultStrategyOptionsTest(_DefaultStrategyOptionsTest): +class DefaultStrategyOptionsTest(DefaultStrategyOptionsTestFixtures): def test_joined_path_wildcards(self): sess = self._upgrade_fixture() users = [] @@ -2787,6 +2789,69 @@ class DefaultStrategyOptionsTest(_DefaultStrategyOptionsTest): # verify everything loaded, with no additional sql needed self._assert_fully_loaded(users) + def test_noload_with_joinedload(self): + """Mapper load strategy defaults can be downgraded with + noload('*') option, while explicit joinedload() option + is still honored""" + sess = self._downgrade_fixture() + users = [] + + # test noload('*') shuts off 'orders' subquery, only 1 sql + def go(): + users[:] = ( + sess.query(self.classes.User) + .options(sa.orm.noload("*")) + .options(joinedload(self.classes.User.addresses)) + .order_by(self.classes.User.id) + .all() + ) + + with expect_noload_deprecation(): + self.assert_sql_count(testing.db, go, 1) + + # verify all the addresses were joined loaded (no more sql) + self._assert_addresses_loaded(users) + + # User.orders should have loaded "noload" (meaning []) + def go(): + for u in users: + assert u.orders == [] + + self.assert_sql_count(testing.db, go, 0) + + def test_noload_with_subqueryload(self): + """Mapper load strategy defaults can be downgraded with + noload('*') option, while explicit subqueryload() option + is still honored""" + sess = self._downgrade_fixture() + users = [] + + # test noload('*') option combined with subqueryload() + # shuts off 'addresses' load AND orders.items load: 2 sql expected + def go(): + users[:] = ( + sess.query(self.classes.User) + .options(sa.orm.noload("*")) + .options(subqueryload(self.classes.User.orders)) + .order_by(self.classes.User.id) + .all() + ) + + with expect_noload_deprecation(): + self.assert_sql_count(testing.db, go, 2) + + def go(): + # Verify orders have already been loaded: 0 sql + for u, static in zip(users, self.static.user_all_result): + assert len(u.orders) == len(static.orders) + # Verify noload('*') prevented orders.items load + # and set 'items' to [] + for u in users: + for o in u.orders: + assert o.items == [] + + self.assert_sql_count(testing.db, go, 0) + class Deferred_InheritanceTest(_deferred_InheritanceTest): def test_defer_on_wildcard_subclass(self): @@ -2812,3 +2877,326 @@ class Deferred_InheritanceTest(_deferred_InheritanceTest): ) # note this doesn't apply to "bound" loaders since they don't seem # to have this ".*" feature. + + +class NoLoadTest(_fixtures.FixtureTest): + run_inserts = "once" + run_deletes = None + + def test_o2m_noload(self): + Address, addresses, users, User = ( + self.classes.Address, + self.tables.addresses, + self.tables.users, + self.classes.User, + ) + + m = self.mapper_registry.map_imperatively( + User, + users, + properties=dict( + addresses=relationship( + self.mapper_registry.map_imperatively(Address, addresses), + lazy="noload", + ) + ), + ) + q = fixture_session().query(m) + result = [None] + + def go(): + x = q.filter(User.id == 7).all() + x[0].addresses + result[0] = x + + with expect_noload_deprecation(): + self.assert_sql_count(testing.db, go, 1) + + self.assert_result( + result[0], User, {"id": 7, "addresses": (Address, [])} + ) + + def test_upgrade_o2m_noload_lazyload_option(self): + Address, addresses, users, User = ( + self.classes.Address, + self.tables.addresses, + self.tables.users, + self.classes.User, + ) + + m = self.mapper_registry.map_imperatively( + User, + users, + properties=dict( + addresses=relationship( + self.mapper_registry.map_imperatively(Address, addresses), + lazy="noload", + ) + ), + ) + with expect_noload_deprecation(): + q = ( + fixture_session() + .query(m) + .options(sa.orm.lazyload(User.addresses)) + ) + result = [None] + + def go(): + x = q.filter(User.id == 7).all() + x[0].addresses + result[0] = x + + self.sql_count_(2, go) + + self.assert_result( + result[0], User, {"id": 7, "addresses": (Address, [{"id": 1}])} + ) + + def test_m2o_noload_option(self): + Address, addresses, users, User = ( + self.classes.Address, + self.tables.addresses, + self.tables.users, + self.classes.User, + ) + self.mapper_registry.map_imperatively( + Address, addresses, properties={"user": relationship(User)} + ) + self.mapper_registry.map_imperatively(User, users) + s = fixture_session() + with expect_noload_deprecation(): + a1 = ( + s.query(Address) + .filter_by(id=1) + .options(sa.orm.noload(Address.user)) + .first() + ) + + def go(): + eq_(a1.user, None) + + self.sql_count_(0, go) + + +class DynamicTest(_DynamicFixture, _fixtures.FixtureTest): + + @testing.combinations(("star",), ("attronly",), argnames="type_") + def test_noload_issue(self, type_, user_address_fixture): + """test #6420. a noload that hits the dynamic loader + should have no effect. + + """ + + User, Address = user_address_fixture() + + s = fixture_session() + + with expect_noload_deprecation(): + + if type_ == "star": + u1 = s.query(User).filter_by(id=7).options(noload("*")).first() + assert "name" not in u1.__dict__["name"] + elif type_ == "attronly": + u1 = ( + s.query(User) + .filter_by(id=7) + .options(noload(User.addresses)) + .first() + ) + + eq_(u1.__dict__["name"], "jack") + + # noload doesn't affect a dynamic loader, because it has no state + eq_(list(u1.addresses), [Address(id=1)]) + + +class WriteOnlyTest(_WriteOnlyFixture, _fixtures.FixtureTest): + + @testing.combinations(("star",), ("attronly",), argnames="type_") + def test_noload_issue(self, type_, user_address_fixture): + """test #6420. a noload that hits the dynamic loader + should have no effect. + + """ + + User, Address = user_address_fixture() + + s = fixture_session() + + with expect_noload_deprecation(): + + if type_ == "star": + u1 = s.query(User).filter_by(id=7).options(noload("*")).first() + assert "name" not in u1.__dict__["name"] + elif type_ == "attronly": + u1 = ( + s.query(User) + .filter_by(id=7) + .options(noload(User.addresses)) + .first() + ) + + eq_(u1.__dict__["name"], "jack") + + +class ExpireTest(_fixtures.FixtureTest): + def test_state_noload_to_lazy(self): + """Behavioral test to verify the current activity of + loader callables + + """ + + users, Address, addresses, User = ( + self.tables.users, + self.classes.Address, + self.tables.addresses, + self.classes.User, + ) + + self.mapper_registry.map_imperatively( + User, + users, + properties={"addresses": relationship(Address, lazy="noload")}, + ) + self.mapper_registry.map_imperatively(Address, addresses) + + sess = fixture_session(autoflush=False) + with expect_noload_deprecation(): + u1 = sess.query(User).options(lazyload(User.addresses)).first() + assert isinstance( + attributes.instance_state(u1).callables["addresses"], + strategies._LoadLazyAttribute, + ) + # expire, it goes away from callables as of 1.4 and is considered + # to be expired + sess.expire(u1) + + assert "addresses" in attributes.instance_state(u1).expired_attributes + assert "addresses" not in attributes.instance_state(u1).callables + + # load it + sess.query(User).first() + assert ( + "addresses" not in attributes.instance_state(u1).expired_attributes + ) + assert "addresses" not in attributes.instance_state(u1).callables + + sess.expunge_all() + u1 = sess.query(User).options(lazyload(User.addresses)).first() + sess.expire(u1, ["addresses"]) + assert ( + "addresses" not in attributes.instance_state(u1).expired_attributes + ) + assert isinstance( + attributes.instance_state(u1).callables["addresses"], + strategies._LoadLazyAttribute, + ) + + # load the attr, goes away + u1.addresses + assert ( + "addresses" not in attributes.instance_state(u1).expired_attributes + ) + assert "addresses" not in attributes.instance_state(u1).callables + + +class NoLoadBackPopulates(_fixtures.FixtureTest): + """test the noload stratgegy which unlike others doesn't use + lazyloader to set up instrumentation""" + + def test_o2m(self): + users, Address, addresses, User = ( + self.tables.users, + self.classes.Address, + self.tables.addresses, + self.classes.User, + ) + + self.mapper_registry.map_imperatively( + User, + users, + properties={ + "addresses": relationship( + Address, back_populates="user", lazy="noload" + ) + }, + ) + + self.mapper_registry.map_imperatively( + Address, addresses, properties={"user": relationship(User)} + ) + with expect_noload_deprecation(): + u1 = User() + a1 = Address() + u1.addresses.append(a1) + is_(a1.user, u1) + + def test_m2o(self): + users, Address, addresses, User = ( + self.tables.users, + self.classes.Address, + self.tables.addresses, + self.classes.User, + ) + + self.mapper_registry.map_imperatively( + User, users, properties={"addresses": relationship(Address)} + ) + + self.mapper_registry.map_imperatively( + Address, + addresses, + properties={ + "user": relationship( + User, back_populates="addresses", lazy="noload" + ) + }, + ) + with expect_noload_deprecation(): + u1 = User() + a1 = Address() + a1.user = u1 + in_(a1, u1.addresses) + + +class ManyToOneTest(_fixtures.FixtureTest): + run_inserts = None + + def test_bidirectional_no_load(self): + users, Address, addresses, User = ( + self.tables.users, + self.classes.Address, + self.tables.addresses, + self.classes.User, + ) + + self.mapper_registry.map_imperatively( + User, + users, + properties={ + "addresses": relationship( + Address, backref="user", lazy="noload" + ) + }, + ) + self.mapper_registry.map_imperatively(Address, addresses) + + # try it on unsaved objects + with expect_noload_deprecation(): + u1 = User(name="u1") + a1 = Address(email_address="e1") + a1.user = u1 + + session = fixture_session() + session.add(u1) + session.flush() + session.expunge_all() + + a1 = session.get(Address, a1.id) + + a1.user = None + session.flush() + session.expunge_all() + assert session.get(Address, a1.id).user is None + assert session.get(User, u1.id).addresses == [] diff --git a/test/orm/test_dynamic.py b/test/orm/test_dynamic.py index 465e29929e..9378f1ef50 100644 --- a/test/orm/test_dynamic.py +++ b/test/orm/test_dynamic.py @@ -16,7 +16,6 @@ from sqlalchemy.orm import configure_mappers from sqlalchemy.orm import exc as orm_exc from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column -from sqlalchemy.orm import noload from sqlalchemy.orm import PassiveFlag from sqlalchemy.orm import Query from sqlalchemy.orm import relationship @@ -548,33 +547,6 @@ class DynamicTest(_DynamicFixture, _fixtures.FixtureTest, AssertsCompiledSQL): [], ) - @testing.combinations(("star",), ("attronly",), argnames="type_") - def test_noload_issue(self, type_, user_address_fixture): - """test #6420. a noload that hits the dynamic loader - should have no effect. - - """ - - User, Address = user_address_fixture() - - s = fixture_session() - - if type_ == "star": - u1 = s.query(User).filter_by(id=7).options(noload("*")).first() - assert "name" not in u1.__dict__["name"] - elif type_ == "attronly": - u1 = ( - s.query(User) - .filter_by(id=7) - .options(noload(User.addresses)) - .first() - ) - - eq_(u1.__dict__["name"], "jack") - - # noload doesn't affect a dynamic loader, because it has no state - eq_(list(u1.addresses), [Address(id=1)]) - def test_m2m(self, order_item_fixture): Order, Item = order_item_fixture( items_args={"backref": backref("orders", lazy="dynamic")} @@ -799,30 +771,6 @@ class WriteOnlyTest( ): __dialect__ = "default" - @testing.combinations(("star",), ("attronly",), argnames="type_") - def test_noload_issue(self, type_, user_address_fixture): - """test #6420. a noload that hits the dynamic loader - should have no effect. - - """ - - User, Address = user_address_fixture() - - s = fixture_session() - - if type_ == "star": - u1 = s.query(User).filter_by(id=7).options(noload("*")).first() - assert "name" not in u1.__dict__["name"] - elif type_ == "attronly": - u1 = ( - s.query(User) - .filter_by(id=7) - .options(noload(User.addresses)) - .first() - ) - - eq_(u1.__dict__["name"], "jack") - def test_iteration_error(self, user_address_fixture): User, Address = user_address_fixture() diff --git a/test/orm/test_expire.py b/test/orm/test_expire.py index 2b15c2443c..84511cb226 100644 --- a/test/orm/test_expire.py +++ b/test/orm/test_expire.py @@ -1664,64 +1664,6 @@ class ExpireTest(_fixtures.FixtureTest): assert "name" in attributes.instance_state(u1).expired_attributes assert "name" not in attributes.instance_state(u1).callables - def test_state_noload_to_lazy(self): - """Behavioral test to verify the current activity of - loader callables - - """ - - users, Address, addresses, User = ( - self.tables.users, - self.classes.Address, - self.tables.addresses, - self.classes.User, - ) - - self.mapper_registry.map_imperatively( - User, - users, - properties={"addresses": relationship(Address, lazy="noload")}, - ) - self.mapper_registry.map_imperatively(Address, addresses) - - sess = fixture_session(autoflush=False) - u1 = sess.query(User).options(lazyload(User.addresses)).first() - assert isinstance( - attributes.instance_state(u1).callables["addresses"], - strategies._LoadLazyAttribute, - ) - # expire, it goes away from callables as of 1.4 and is considered - # to be expired - sess.expire(u1) - - assert "addresses" in attributes.instance_state(u1).expired_attributes - assert "addresses" not in attributes.instance_state(u1).callables - - # load it - sess.query(User).first() - assert ( - "addresses" not in attributes.instance_state(u1).expired_attributes - ) - assert "addresses" not in attributes.instance_state(u1).callables - - sess.expunge_all() - u1 = sess.query(User).options(lazyload(User.addresses)).first() - sess.expire(u1, ["addresses"]) - assert ( - "addresses" not in attributes.instance_state(u1).expired_attributes - ) - assert isinstance( - attributes.instance_state(u1).callables["addresses"], - strategies._LoadLazyAttribute, - ) - - # load the attr, goes away - u1.addresses - assert ( - "addresses" not in attributes.instance_state(u1).expired_attributes - ) - assert "addresses" not in attributes.instance_state(u1).callables - def test_deferred_expire_w_transient_to_detached(self): orders, Order = self.tables.orders, self.classes.Order self.mapper_registry.map_imperatively( diff --git a/test/orm/test_pickled.py b/test/orm/test_pickled.py index 18904cc386..0c69b2cc86 100644 --- a/test/orm/test_pickled.py +++ b/test/orm/test_pickled.py @@ -239,7 +239,7 @@ class PickleTest(fixtures.MappedTest): self.mapper_registry.map_imperatively( User, users, - properties={"addresses": relationship(Address, lazy="noload")}, + properties={"addresses": relationship(Address, lazy="raise")}, ) self.mapper_registry.map_imperatively(Address, addresses) @@ -305,7 +305,7 @@ class PickleTest(fixtures.MappedTest): self.mapper_registry.map_imperatively( User, users, - properties={"addresses": relationship(Address, lazy="noload")}, + properties={"addresses": relationship(Address)}, ) self.mapper_registry.map_imperatively(Address, addresses) @@ -321,7 +321,7 @@ class PickleTest(fixtures.MappedTest): self.mapper_registry.map_imperatively( User, users, - properties={"addresses": relationship(Address, lazy="noload")}, + properties={"addresses": relationship(Address)}, ) self.mapper_registry.map_imperatively(Address, addresses) diff --git a/test/orm/test_relationships.py b/test/orm/test_relationships.py index 0d4211656a..589dcf2fed 100644 --- a/test/orm/test_relationships.py +++ b/test/orm/test_relationships.py @@ -38,7 +38,6 @@ from sqlalchemy.testing import eq_ from sqlalchemy.testing import expect_raises_message from sqlalchemy.testing import expect_warnings from sqlalchemy.testing import fixtures -from sqlalchemy.testing import in_ from sqlalchemy.testing import is_ from sqlalchemy.testing.assertsql import assert_engine from sqlalchemy.testing.assertsql import CompiledSQL @@ -2478,65 +2477,6 @@ class ManualBackrefTest(_fixtures.FixtureTest): ) -class NoLoadBackPopulates(_fixtures.FixtureTest): - """test the noload stratgegy which unlike others doesn't use - lazyloader to set up instrumentation""" - - def test_o2m(self): - users, Address, addresses, User = ( - self.tables.users, - self.classes.Address, - self.tables.addresses, - self.classes.User, - ) - - self.mapper_registry.map_imperatively( - User, - users, - properties={ - "addresses": relationship( - Address, back_populates="user", lazy="noload" - ) - }, - ) - - self.mapper_registry.map_imperatively( - Address, addresses, properties={"user": relationship(User)} - ) - - u1 = User() - a1 = Address() - u1.addresses.append(a1) - is_(a1.user, u1) - - def test_m2o(self): - users, Address, addresses, User = ( - self.tables.users, - self.classes.Address, - self.tables.addresses, - self.classes.User, - ) - - self.mapper_registry.map_imperatively( - User, users, properties={"addresses": relationship(Address)} - ) - - self.mapper_registry.map_imperatively( - Address, - addresses, - properties={ - "user": relationship( - User, back_populates="addresses", lazy="noload" - ) - }, - ) - - u1 = User() - a1 = Address() - a1.user = u1 - in_(a1, u1.addresses) - - class JoinConditionErrorTest(fixtures.TestBase): def test_clauseelement_pj(self, registry): Base = registry.generate_base() diff --git a/test/orm/test_subquery_relations.py b/test/orm/test_subquery_relations.py index 538c77c0ce..4b839d8efc 100644 --- a/test/orm/test_subquery_relations.py +++ b/test/orm/test_subquery_relations.py @@ -3222,7 +3222,7 @@ class JoinedNoLoadConflictTest(fixtures.DeclarativeMappedTest): name = Column(String(20)) children = relationship( - "Child", back_populates="parent", lazy="noload" + "Child", back_populates="parent", lazy="raise" ) class Child(ComparableEntity, Base): diff --git a/test/orm/test_unitofwork.py b/test/orm/test_unitofwork.py index 7b29b4362a..eb290156b8 100644 --- a/test/orm/test_unitofwork.py +++ b/test/orm/test_unitofwork.py @@ -2463,43 +2463,6 @@ class ManyToOneTest(_fixtures.FixtureTest): u2 = session.get(User, u2.id) assert a1.user is u2 - def test_bidirectional_no_load(self): - users, Address, addresses, User = ( - self.tables.users, - self.classes.Address, - self.tables.addresses, - self.classes.User, - ) - - self.mapper_registry.map_imperatively( - User, - users, - properties={ - "addresses": relationship( - Address, backref="user", lazy="noload" - ) - }, - ) - self.mapper_registry.map_imperatively(Address, addresses) - - # try it on unsaved objects - u1 = User(name="u1") - a1 = Address(email_address="e1") - a1.user = u1 - - session = fixture_session() - session.add(u1) - session.flush() - session.expunge_all() - - a1 = session.get(Address, a1.id) - - a1.user = None - session.flush() - session.expunge_all() - assert session.get(Address, a1.id).user is None - assert session.get(User, u1.id).addresses == [] - class ManyToManyTest(_fixtures.FixtureTest): run_inserts = None