summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2023-02-02 14:38:37 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2023-02-06 11:08:50 -0500
commit3c6acaba017ef0a0b667864199f103f5eb6f79f9 (patch)
treec8d1ea054d5db18e0d56e2741aa9c75b651af959 /test
parent2459619e751f39a796bb1dd9fe75947dd0961fee (diff)
downloadsqlalchemy-3c6acaba017ef0a0b667864199f103f5eb6f79f9.tar.gz
port history meta to 2.0
first change: Reworked the :ref:`examples_versioned_history` to work with version 2.0, while at the same time improving the overall working of this example to use newer APIs, including a newly added hook :meth:`_orm.MapperEvents.after_mapper_constructed`. second change: Added new event hook :meth:`_orm.MapperEvents.after_mapper_constructed`, which supplies an event hook to take place right as the :class:`_orm.Mapper` object has been fully constructed, but before the :meth:`_orm.registry.configure` call has been called. This allows code that can create additional mappings and table structures based on the initial configuration of a :class:`_orm.Mapper`, which also integrates within Declarative configuration. Previously, when using Declarative, where the :class:`_orm.Mapper` object is created within the class creation process, there was no documented means of running code at this point. The change is to immediately benefit custom mapping schemes such as that of the :ref:`examples_versioned_history` example, which generate additional mappers and tables in response to the creation of mapped classes. third change: The infrequently used :attr:`_orm.Mapper.iterate_properties` attribute and :meth:`_orm.Mapper.get_property` method, which are primarily used internally, no longer implicitly invoke the :meth:`_orm.registry.configure` process. Public access to these methods is extremely rare and the only benefit to having :meth:`_orm.registry.configure` would have been allowing "backref" properties be present in these collections. In order to support the new :meth:`_orm.MapperEvents.after_mapper_constructed` event, iteration and access to the internal :class:`_orm.MapperProperty` objects is now possible without triggering an implicit configure of the mapper itself. The more-public facing route to iteration of all mapper attributes, the :attr:`_orm.Mapper.attrs` collection and similar, will still implicitly invoke the :meth:`_orm.registry.configure` step thus making backref attributes available. In all cases, the :meth:`_orm.registry.configure` is always available to be called directly. fourth change: Fixed obscure ORM inheritance issue caused by :ticket:`8705` where some scenarios of inheriting mappers that indicated groups of columns from the local table and the inheriting table together under a :func:`_orm.column_property` would nonetheless warn that properties of the same name were being combined implicitly. Fixes: #9220 Fixes: #9232 Change-Id: Id335b8e8071c8ea509c057c389df9dcd2059437d
Diffstat (limited to 'test')
-rw-r--r--test/base/test_examples.py23
-rw-r--r--test/ext/declarative/test_inheritance.py6
-rw-r--r--test/orm/inheritance/test_basic.py48
-rw-r--r--test/orm/inheritance/test_concrete.py6
-rw-r--r--test/orm/test_deprecations.py4
-rw-r--r--test/orm/test_events.py121
-rw-r--r--test/orm/test_mapper.py68
7 files changed, 247 insertions, 29 deletions
diff --git a/test/base/test_examples.py b/test/base/test_examples.py
new file mode 100644
index 000000000..50f0c01f2
--- /dev/null
+++ b/test/base/test_examples.py
@@ -0,0 +1,23 @@
+import os
+import sys
+
+from sqlalchemy.testing import fixtures
+
+
+here = os.path.dirname(__file__)
+sqla_base = os.path.normpath(os.path.join(here, "..", ".."))
+
+
+sys.path.insert(0, sqla_base)
+
+test_versioning = __import__(
+ "examples.versioned_history.test_versioning"
+).versioned_history.test_versioning
+
+
+class VersionedRowsTest(
+ test_versioning.TestVersioning,
+ fixtures.RemoveORMEventsGlobally,
+ fixtures.TestBase,
+):
+ pass
diff --git a/test/ext/declarative/test_inheritance.py b/test/ext/declarative/test_inheritance.py
index 2aa0f2cc3..d7cac669b 100644
--- a/test/ext/declarative/test_inheritance.py
+++ b/test/ext/declarative/test_inheritance.py
@@ -22,9 +22,9 @@ from sqlalchemy.testing import fixtures
from sqlalchemy.testing import mock
from sqlalchemy.testing.assertions import expect_raises_message
from sqlalchemy.testing.fixtures import fixture_session
+from sqlalchemy.testing.fixtures import RemoveORMEventsGlobally
from sqlalchemy.testing.schema import Column
from sqlalchemy.testing.schema import Table
-from test.orm.test_events import _RemoveListeners
Base = None
@@ -43,7 +43,7 @@ class DeclarativeTestBase(fixtures.TestBase, testing.AssertsExecutionResults):
class ConcreteInhTest(
- _RemoveListeners, DeclarativeTestBase, testing.AssertsCompiledSQL
+ RemoveORMEventsGlobally, DeclarativeTestBase, testing.AssertsCompiledSQL
):
def _roundtrip(
self,
@@ -735,7 +735,7 @@ class ConcreteInhTest(
class ConcreteExtensionConfigTest(
- _RemoveListeners, testing.AssertsCompiledSQL, DeclarativeTestBase
+ RemoveORMEventsGlobally, testing.AssertsCompiledSQL, DeclarativeTestBase
):
__dialect__ = "default"
diff --git a/test/orm/inheritance/test_basic.py b/test/orm/inheritance/test_basic.py
index 02f352786..1a0fe1629 100644
--- a/test/orm/inheritance/test_basic.py
+++ b/test/orm/inheritance/test_basic.py
@@ -2577,6 +2577,54 @@ class OverrideColKeyTest(fixtures.MappedTest):
is_(B.a_data.property.columns[0], A.__table__.c.a_data)
is_(B.b_data.property.columns[0], A.__table__.c.a_data)
+ def test_subsubclass_groups_super_cols(self, decl_base):
+ """tested for #9220, which is a regression caused by #8705."""
+
+ class BaseClass(decl_base):
+ __tablename__ = "basetable"
+
+ id = Column(Integer, primary_key=True)
+ name = Column(String(50))
+ type = Column(String(20))
+
+ __mapper_args__ = {
+ "polymorphic_on": type,
+ "polymorphic_identity": "base",
+ }
+
+ class SubClass(BaseClass):
+ __tablename__ = "subtable"
+
+ id = column_property(
+ Column(Integer, primary_key=True), BaseClass.id
+ )
+ base_id = Column(Integer, ForeignKey("basetable.id"))
+ subdata1 = Column(String(50))
+
+ __mapper_args__ = {"polymorphic_identity": "sub"}
+
+ class SubSubClass(SubClass):
+ __tablename__ = "subsubtable"
+
+ id = column_property(
+ Column(Integer, ForeignKey("subtable.id"), primary_key=True),
+ SubClass.id,
+ BaseClass.id,
+ )
+ subdata2 = Column(String(50))
+
+ __mapper_args__ = {"polymorphic_identity": "subsub"}
+
+ is_(SubSubClass.id.property.columns[0], SubSubClass.__table__.c.id)
+ is_(
+ SubSubClass.id.property.columns[1]._deannotate(),
+ SubClass.__table__.c.id,
+ )
+ is_(
+ SubSubClass.id.property.columns[2]._deannotate(),
+ BaseClass.__table__.c.id,
+ )
+
def test_column_setup_sanity_check(self, decl_base):
class A(decl_base):
__tablename__ = "a"
diff --git a/test/orm/inheritance/test_concrete.py b/test/orm/inheritance/test_concrete.py
index 5a6ecff21..cf560ca97 100644
--- a/test/orm/inheritance/test_concrete.py
+++ b/test/orm/inheritance/test_concrete.py
@@ -31,9 +31,9 @@ from sqlalchemy.testing import mock
from sqlalchemy.testing.assertsql import CompiledSQL
from sqlalchemy.testing.entities import ComparableEntity
from sqlalchemy.testing.fixtures import fixture_session
+from sqlalchemy.testing.fixtures import RemoveORMEventsGlobally
from sqlalchemy.testing.schema import Column
from sqlalchemy.testing.schema import Table
-from test.orm.test_events import _RemoveListeners
class ConcreteTest(AssertsCompiledSQL, fixtures.MappedTest):
@@ -1446,7 +1446,9 @@ class ColKeysTest(fixtures.MappedTest):
eq_(sess.get(Office, 2).name, "office2")
-class AdaptOnNamesTest(_RemoveListeners, fixtures.DeclarativeMappedTest):
+class AdaptOnNamesTest(
+ RemoveORMEventsGlobally, fixtures.DeclarativeMappedTest
+):
"""test the full integration case for #7805"""
@classmethod
diff --git a/test/orm/test_deprecations.py b/test/orm/test_deprecations.py
index 859ccf884..91d733877 100644
--- a/test/orm/test_deprecations.py
+++ b/test/orm/test_deprecations.py
@@ -54,6 +54,7 @@ from sqlalchemy.testing import is_true
from sqlalchemy.testing import mock
from sqlalchemy.testing.fixtures import CacheKeyFixture
from sqlalchemy.testing.fixtures import fixture_session
+from sqlalchemy.testing.fixtures import RemoveORMEventsGlobally
from sqlalchemy.testing.schema import Column
from sqlalchemy.testing.schema import Table
from . import _fixtures
@@ -61,7 +62,6 @@ from .inheritance import _poly_fixtures
from .inheritance._poly_fixtures import Manager
from .inheritance._poly_fixtures import Person
from .test_deferred import InheritanceTest as _deferred_InheritanceTest
-from .test_events import _RemoveListeners
from .test_options import PathTest as OptionsPathTest
from .test_options import PathTest
from .test_options import QueryTest as OptionsQueryTest
@@ -1659,7 +1659,7 @@ class InstancesTest(QueryTest, AssertsCompiledSQL):
self.assert_sql_count(testing.db, go, 1)
-class SessionEventsTest(_RemoveListeners, _fixtures.FixtureTest):
+class SessionEventsTest(RemoveORMEventsGlobally, _fixtures.FixtureTest):
run_inserts = None
def test_on_bulk_update_hook(self):
diff --git a/test/orm/test_events.py b/test/orm/test_events.py
index 05d5d376d..d2a43331f 100644
--- a/test/orm/test_events.py
+++ b/test/orm/test_events.py
@@ -22,7 +22,6 @@ from sqlalchemy.orm import class_mapper
from sqlalchemy.orm import configure_mappers
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import deferred
-from sqlalchemy.orm import events
from sqlalchemy.orm import EXT_SKIP
from sqlalchemy.orm import instrumentation
from sqlalchemy.orm import joinedload
@@ -49,24 +48,14 @@ from sqlalchemy.testing import is_not
from sqlalchemy.testing.assertions import expect_raises_message
from sqlalchemy.testing.assertsql import CompiledSQL
from sqlalchemy.testing.fixtures import fixture_session
+from sqlalchemy.testing.fixtures import RemoveORMEventsGlobally
from sqlalchemy.testing.schema import Column
from sqlalchemy.testing.schema import Table
from sqlalchemy.testing.util import gc_collect
from test.orm import _fixtures
-class _RemoveListeners:
- @testing.fixture(autouse=True)
- def _remove_listeners(self):
- yield
- events.MapperEvents._clear()
- events.InstanceEvents._clear()
- events.SessionEvents._clear()
- events.InstrumentationEvents._clear()
- events.QueryEvents._clear()
-
-
-class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest):
+class ORMExecuteTest(RemoveORMEventsGlobally, _fixtures.FixtureTest):
run_setup_mappers = "once"
run_inserts = "once"
run_deletes = None
@@ -787,7 +776,7 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest):
eq_(m1.mock_calls, [])
-class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
+class MapperEventsTest(RemoveORMEventsGlobally, _fixtures.FixtureTest):
run_inserts = None
@classmethod
@@ -1324,6 +1313,98 @@ class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
canary.mock_calls,
)
+ @testing.variation(
+ "listen_type",
+ ["listen_on_mapper", "listen_on_base", "listen_on_mixin"],
+ )
+ def test_mapper_config_sequence(self, decl_base, listen_type):
+
+ canary = Mock()
+
+ if listen_type.listen_on_mapper:
+ event.listen(Mapper, "instrument_class", canary.instrument_class)
+ event.listen(
+ Mapper,
+ "after_mapper_constructed",
+ canary.after_mapper_constructed,
+ )
+ elif listen_type.listen_on_base:
+ event.listen(
+ decl_base,
+ "instrument_class",
+ canary.instrument_class,
+ propagate=True,
+ )
+ event.listen(
+ decl_base,
+ "after_mapper_constructed",
+ canary.after_mapper_constructed,
+ propagate=True,
+ )
+ elif listen_type.listen_on_mixin:
+
+ class Mixin:
+ pass
+
+ event.listen(
+ Mixin,
+ "instrument_class",
+ canary.instrument_class,
+ propagate=True,
+ )
+ event.listen(
+ Mixin,
+ "after_mapper_constructed",
+ canary.after_mapper_constructed,
+ propagate=True,
+ )
+ else:
+ listen_type.fail()
+
+ event.listen(object, "class_instrument", canary.class_instrument)
+ event.listen(Mapper, "before_configured", canary.before_configured)
+ event.listen(
+ Mapper, "before_mapper_configured", canary.before_mapper_configured
+ )
+ event.listen(Mapper, "after_configured", canary.after_configured)
+
+ if listen_type.listen_on_mixin:
+
+ class Thing(Mixin, decl_base):
+ __tablename__ = "thing"
+
+ id = Column(Integer, primary_key=True)
+
+ else:
+
+ class Thing(decl_base):
+ __tablename__ = "thing"
+
+ id = Column(Integer, primary_key=True)
+
+ mp = inspect(Thing)
+ eq_(
+ canary.mock_calls,
+ [
+ call.instrument_class(mp, Thing),
+ call.class_instrument(Thing),
+ call.after_mapper_constructed(mp, Thing),
+ ],
+ )
+
+ decl_base.registry.configure()
+ eq_(
+ canary.mock_calls,
+ [
+ call.instrument_class(mp, Thing),
+ call.class_instrument(Thing),
+ call.after_mapper_constructed(mp, Thing),
+ call.before_configured(),
+ call.before_mapper_configured(mp, Thing),
+ call.after_configured(),
+ ],
+ )
+
@testing.combinations((True,), (False,), argnames="create_dependency")
@testing.combinations((True,), (False,), argnames="configure_at_once")
def test_before_mapper_configured_event(
@@ -1526,7 +1607,7 @@ class RestoreLoadContextTest(fixtures.DeclarativeMappedTest):
class DeclarativeEventListenTest(
- _RemoveListeners, fixtures.DeclarativeMappedTest
+ RemoveORMEventsGlobally, fixtures.DeclarativeMappedTest
):
run_setup_classes = "each"
run_deletes = None
@@ -1559,7 +1640,7 @@ class DeclarativeEventListenTest(
eq_(listen.mock_calls, [call(c1, "c"), call(b1, "b"), call(a1, "a")])
-class DeferredMapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
+class DeferredMapperEventsTest(RemoveORMEventsGlobally, _fixtures.FixtureTest):
""" "test event listeners against unmapped classes.
@@ -2228,7 +2309,7 @@ class RefreshTest(_fixtures.FixtureTest):
eq_(canary, [("refresh", None)])
-class SessionEventsTest(_RemoveListeners, _fixtures.FixtureTest):
+class SessionEventsTest(RemoveORMEventsGlobally, _fixtures.FixtureTest):
run_inserts = None
def test_class_listen(self):
@@ -2755,7 +2836,9 @@ class SessionEventsTest(_RemoveListeners, _fixtures.FixtureTest):
assert "name" not in u1.__dict__
-class SessionLifecycleEventsTest(_RemoveListeners, _fixtures.FixtureTest):
+class SessionLifecycleEventsTest(
+ RemoveORMEventsGlobally, _fixtures.FixtureTest
+):
run_inserts = None
def _fixture(self, include_address=False):
@@ -3297,7 +3380,7 @@ class SessionLifecycleEventsTest(_RemoveListeners, _fixtures.FixtureTest):
class QueryEventsTest(
- _RemoveListeners,
+ RemoveORMEventsGlobally,
_fixtures.FixtureTest,
AssertsCompiledSQL,
testing.AssertsExecutionResults,
diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py
index 8b36f0b59..e645556b5 100644
--- a/test/orm/test_mapper.py
+++ b/test/orm/test_mapper.py
@@ -800,7 +800,11 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL):
m = self.mapper(User, users)
assert not m.configured
assert list(m.iterate_properties)
- assert m.configured
+
+ # as of 2.0 #9220
+ # iterate properties doesn't do "configure" now, there is
+ # no reason for this
+ assert not m.configured
def test_configure_on_get_props_2(self):
User, users = self.classes.User, self.tables.users
@@ -808,7 +812,11 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL):
m = self.mapper(User, users)
assert not m.configured
assert m.get_property("name")
- assert m.configured
+
+ # as of 2.0 #9220
+ # get_property() doesn't do "configure" now, there is
+ # no reason for this
+ assert not m.configured
def test_configure_on_get_props_3(self):
users, Address, addresses, User = (
@@ -827,7 +835,61 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL):
addresses,
properties={"user": relationship(User, backref="addresses")},
)
- assert m.get_property("addresses")
+
+ # as of 2.0 #9220
+ # get_property() doesn't do "configure" now, there is
+ # no reason for this
+ with expect_raises_message(
+ sa.exc.InvalidRequestError, r".has no property 'addresses'"
+ ):
+ m.get_property("addresses")
+
+ configure_mappers()
+ is_(m.get_property("addresses"), m.attrs.addresses)
+
+ def test_backrefs_dont_automatically_configure(self):
+ """in #9220 for 2.0 we are changing an ancient behavior that
+ mapper.get_property() and mapper.iterate_properties() would call
+ configure_mappers(). The more modern public interface for this,
+ ``Mapper.attrs``, does.
+
+ """
+ users, Address, addresses, User = (
+ self.tables.users,
+ self.classes.Address,
+ self.tables.addresses,
+ self.classes.User,
+ )
+
+ m = self.mapper(User, users)
+ assert not m.configured
+ configure_mappers()
+
+ m2 = self.mapper(
+ Address,
+ addresses,
+ properties={"user": relationship(User, backref="addresses")},
+ )
+
+ with expect_raises_message(
+ AttributeError, r"no attribute 'addresses'"
+ ):
+ User.addresses
+
+ with expect_raises_message(
+ sa.exc.InvalidRequestError,
+ r"Mapper 'Mapper\[User\(users\)\]' has no property 'addresses'",
+ ):
+ User.__mapper__.get_property("addresses")
+
+ assert not m2.configured
+
+ # the more public-facing collection, mapper.attrs, *does* call
+ # configure still.
+
+ m.attrs.addresses
+ assert m2.configured
+ User.addresses
def test_info(self):
users = self.tables.users