From: Mike Bayer Date: Sat, 23 Nov 2019 17:25:20 +0000 (-0500) Subject: Generalize DescriptorProps.uses_objects X-Git-Tag: rel_1_3_12~10 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6bef95307862286f1d53b887c20d03344284d948;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Generalize DescriptorProps.uses_objects Previously, uses_objects was specific to the SynonymAttribute; generalize it so that it defaults to False for other DescriptorProps. Immediate fix is against CompositeProperty. Fixed regression introduced in 1.3.0 related to the association proxy refactor in :ticket:`4351` that prevented :func:`.composite` attributes from working in terms of an association proxy that references them. Add test coverage for association proxies that refer to Composite attributes as endpoints. Fixes: #5000 Change-Id: Iea6fb1bd3314d861a9bc22491b0ae1e6c5e6340d (cherry picked from commit c2432d9d190bdc67f274b8da9296ff9ed044bef1) --- diff --git a/doc/build/changelog/unreleased_13/5000.rst b/doc/build/changelog/unreleased_13/5000.rst new file mode 100644 index 0000000000..2df2793b53 --- /dev/null +++ b/doc/build/changelog/unreleased_13/5000.rst @@ -0,0 +1,7 @@ +.. change:: + :tags: bug, orm + :tickets: 5000 + + Fixed regression introduced in 1.3.0 related to the association proxy + refactor in :ticket:`4351` that prevented :func:`.composite` attributes + from working in terms of an association proxy that references them. diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index 28b3bc5db3..b947a90449 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -31,6 +31,8 @@ class DescriptorProperty(MapperProperty): doc = None + uses_objects = False + def instrument_class(self, mapper): prop = self @@ -41,7 +43,7 @@ class DescriptorProperty(MapperProperty): @property def uses_objects(self): - return getattr(mapper.class_, prop.name).impl.uses_objects + return prop.uses_objects def __init__(self, key): self.key = key @@ -645,6 +647,10 @@ class SynonymProperty(DescriptorProperty): util.set_creation_order(self) + @property + def uses_objects(self): + return getattr(self.parent.class_, self.name).impl.uses_objects + # TODO: when initialized, check _proxied_property, # emit a warning if its not a column-based property diff --git a/lib/sqlalchemy/testing/fixtures.py b/lib/sqlalchemy/testing/fixtures.py index e2237fb171..04d09f8dd2 100644 --- a/lib/sqlalchemy/testing/fixtures.py +++ b/lib/sqlalchemy/testing/fixtures.py @@ -388,7 +388,10 @@ class DeclarativeMappedTest(MappedTest): cls=DeclarativeBasic, ) cls.DeclarativeBasic = _DeclBase - fn() + + # sets up cls.Basic which is helpful for things like composite + # classes + super(DeclarativeMappedTest, cls)._with_register_classes(fn) if cls.metadata.tables and cls.run_create_tables: cls.metadata.create_all(config.db) diff --git a/test/ext/test_associationproxy.py b/test/ext/test_associationproxy.py index d515ccf656..e7cc6251bb 100644 --- a/test/ext/test_associationproxy.py +++ b/test/ext/test_associationproxy.py @@ -18,6 +18,7 @@ from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import aliased from sqlalchemy.orm import clear_mappers from sqlalchemy.orm import collections +from sqlalchemy.orm import composite from sqlalchemy.orm import configure_mappers from sqlalchemy.orm import create_session from sqlalchemy.orm import mapper @@ -2330,6 +2331,74 @@ class DictOfTupleUpdateTest(fixtures.TestBase): ) +class CompositeAccessTest(fixtures.DeclarativeMappedTest): + @classmethod + def setup_classes(cls): + class Point(cls.Basic): + def __init__(self, x, y): + self.x = x + self.y = y + + def __composite_values__(self): + return [self.x, self.y] + + __hash__ = None + + def __eq__(self, other): + return ( + isinstance(other, Point) + and other.x == self.x + and other.y == self.y + ) + + def __ne__(self, other): + return not isinstance(other, Point) or not self.__eq__(other) + + class Graph(cls.DeclarativeBasic): + __tablename__ = "graph" + id = Column( + Integer, primary_key=True, test_needs_autoincrement=True + ) + name = Column(String(30)) + + point_data = relationship("PointData") + + points = association_proxy( + "point_data", + "point", + creator=lambda point: PointData(point=point), + ) + + class PointData(fixtures.ComparableEntity, cls.DeclarativeBasic): + __tablename__ = "point" + + id = Column( + Integer, primary_key=True, test_needs_autoincrement=True + ) + graph_id = Column(ForeignKey("graph.id")) + + x1 = Column(Integer) + y1 = Column(Integer) + + point = composite(Point, x1, y1) + + return Point, Graph, PointData + + def test_append(self): + Point, Graph, PointData = self.classes("Point", "Graph", "PointData") + + g1 = Graph() + g1.points.append(Point(3, 5)) + eq_(g1.point_data, [PointData(point=Point(3, 5))]) + + def test_access(self): + Point, Graph, PointData = self.classes("Point", "Graph", "PointData") + g1 = Graph() + g1.point_data.append(PointData(point=Point(3, 5))) + g1.point_data.append(PointData(point=Point(10, 7))) + eq_(g1.points, [Point(3, 5), Point(10, 7)]) + + class AttributeAccessTest(fixtures.TestBase): def teardown(self): clear_mappers() @@ -3080,7 +3149,7 @@ class MultiOwnerTest( is_(association_proxy_object.for_class(D, d2), inst2) def test_col_expressions_not_available(self): - D, = self.classes("D") + (D,) = self.classes("D") self._assert_raises_ambiguous(lambda: D.c_data == 5) @@ -3269,7 +3338,7 @@ class ProxyHybridTest(fixtures.DeclarativeMappedTest, AssertsCompiledSQL): ) def test_explicit_expr(self): - C, = self.classes("C") + (C,) = self.classes("C") s = Session() self.assert_compile(