summaryrefslogtreecommitdiff
path: root/test/ext/test_associationproxy.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/ext/test_associationproxy.py')
-rw-r--r--test/ext/test_associationproxy.py244
1 files changed, 244 insertions, 0 deletions
diff --git a/test/ext/test_associationproxy.py b/test/ext/test_associationproxy.py
index 3dcb87746..73f5b3137 100644
--- a/test/ext/test_associationproxy.py
+++ b/test/ext/test_associationproxy.py
@@ -1,6 +1,10 @@
+from __future__ import annotations
+
from collections import abc
import copy
+import dataclasses
import pickle
+from typing import List
from unittest.mock import call
from unittest.mock import Mock
@@ -17,6 +21,7 @@ from sqlalchemy import testing
from sqlalchemy.engine import default
from sqlalchemy.ext.associationproxy import _AssociationList
from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy.ext.associationproxy import AssociationProxy
from sqlalchemy.orm import aliased
from sqlalchemy.orm import clear_mappers
from sqlalchemy.orm import collections
@@ -24,6 +29,8 @@ from sqlalchemy.orm import composite
from sqlalchemy.orm import configure_mappers
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import declared_attr
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import mapper
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
@@ -38,6 +45,7 @@ from sqlalchemy.testing import fixtures
from sqlalchemy.testing import is_
from sqlalchemy.testing import is_false
from sqlalchemy.testing.assertions import expect_raises_message
+from sqlalchemy.testing.entities import ComparableMixin # noqa
from sqlalchemy.testing.fixtures import fixture_session
from sqlalchemy.testing.schema import Column
from sqlalchemy.testing.schema import Table
@@ -3732,3 +3740,239 @@ class ScopeBehaviorTest(fixtures.DeclarativeMappedTest):
gc_collect()
assert len(a1bs) == 2
+
+
+class DeclOrmForms(fixtures.TestBase):
+ """test issues related to #8880, #8878, #8876"""
+
+ def test_straight_decl_usage(self, decl_base):
+ """test use of assoc prox as the default descriptor for a
+ dataclasses.field.
+
+ """
+
+ class User(decl_base):
+ __allow_unmapped__ = True
+
+ __tablename__ = "user"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+
+ user_keyword_associations: Mapped[
+ List[UserKeywordAssociation]
+ ] = relationship(
+ back_populates="user",
+ cascade="all, delete-orphan",
+ )
+
+ keywords: AssociationProxy[list[str]] = association_proxy(
+ "user_keyword_associations", "keyword"
+ )
+
+ UserKeywordAssociation, Keyword = self._keyword_mapping(
+ User, decl_base
+ )
+
+ self._assert_keyword_assoc_mapping(
+ User, UserKeywordAssociation, Keyword, init=True
+ )
+
+ @testing.variation("embed_in_field", [True, False])
+ @testing.combinations(
+ {},
+ {"repr": False},
+ {"repr": True},
+ ({"kw_only": True}, testing.requires.python310),
+ {"init": False},
+ {"default_factory": True},
+ argnames="field_kw",
+ )
+ def test_dc_decl_usage(self, dc_decl_base, embed_in_field, field_kw):
+ """test use of assoc prox as the default descriptor for a
+ dataclasses.field.
+
+ This exercises #8880
+
+ """
+
+ if field_kw.pop("default_factory", False) and not embed_in_field:
+ has_default_factory = True
+ field_kw["default_factory"] = lambda: [
+ Keyword("l1"),
+ Keyword("l2"),
+ Keyword("l3"),
+ ]
+ else:
+ has_default_factory = False
+
+ class User(dc_decl_base):
+ __allow_unmapped__ = True
+
+ __tablename__ = "user"
+
+ id: Mapped[int] = mapped_column(
+ primary_key=True, repr=True, init=False
+ )
+
+ user_keyword_associations: Mapped[
+ List[UserKeywordAssociation]
+ ] = relationship(
+ back_populates="user",
+ cascade="all, delete-orphan",
+ init=False,
+ )
+
+ if embed_in_field:
+ # this is an incorrect form to use with
+ # MappedAsDataclass. However, we want to make sure it
+ # works as kind of a test to ensure we are being as well
+ # behaved as possible with an explicit dataclasses.field(),
+ # by testing that it uses its normal descriptor-as-default
+ # behavior
+ keywords: AssociationProxy[list[str]] = dataclasses.field(
+ default=association_proxy(
+ "user_keyword_associations", "keyword"
+ ),
+ **field_kw,
+ )
+ else:
+ keywords: AssociationProxy[list[str]] = association_proxy(
+ "user_keyword_associations", "keyword", **field_kw
+ )
+
+ UserKeywordAssociation, Keyword = self._dc_keyword_mapping(
+ User, dc_decl_base
+ )
+
+ # simplify __qualname__ so we can test repr() more easily
+ User.__qualname__ = "mod.User"
+ UserKeywordAssociation.__qualname__ = "mod.UserKeywordAssociation"
+ Keyword.__qualname__ = "mod.Keyword"
+
+ init = field_kw.get("init", True)
+
+ u1 = self._assert_keyword_assoc_mapping(
+ User,
+ UserKeywordAssociation,
+ Keyword,
+ init=init,
+ has_default_factory=has_default_factory,
+ )
+
+ if field_kw.get("repr", True):
+ eq_(
+ repr(u1),
+ "mod.User(id=None, user_keyword_associations=["
+ "mod.UserKeywordAssociation(user_id=None, keyword_id=None, "
+ "keyword=mod.Keyword(id=None, keyword='k1'), user=...), "
+ "mod.UserKeywordAssociation(user_id=None, keyword_id=None, "
+ "keyword=mod.Keyword(id=None, keyword='k2'), user=...), "
+ "mod.UserKeywordAssociation(user_id=None, keyword_id=None, "
+ "keyword=mod.Keyword(id=None, keyword='k3'), user=...)], "
+ "keywords=[mod.Keyword(id=None, keyword='k1'), "
+ "mod.Keyword(id=None, keyword='k2'), "
+ "mod.Keyword(id=None, keyword='k3')])",
+ )
+ else:
+ eq_(
+ repr(u1),
+ "mod.User(id=None, user_keyword_associations=["
+ "mod.UserKeywordAssociation(user_id=None, keyword_id=None, "
+ "keyword=mod.Keyword(id=None, keyword='k1'), user=...), "
+ "mod.UserKeywordAssociation(user_id=None, keyword_id=None, "
+ "keyword=mod.Keyword(id=None, keyword='k2'), user=...), "
+ "mod.UserKeywordAssociation(user_id=None, keyword_id=None, "
+ "keyword=mod.Keyword(id=None, keyword='k3'), user=...)])",
+ )
+
+ def _assert_keyword_assoc_mapping(
+ self,
+ User,
+ UserKeywordAssociation,
+ Keyword,
+ *,
+ init,
+ has_default_factory=False,
+ ):
+ if not init:
+ with expect_raises_message(
+ TypeError, r"got an unexpected keyword argument 'keywords'"
+ ):
+ User(keywords=[Keyword("k1"), Keyword("k2"), Keyword("k3")])
+
+ if has_default_factory:
+ u1 = User()
+ eq_(u1.keywords, [Keyword("l1"), Keyword("l2"), Keyword("l3")])
+
+ eq_(
+ [ka.keyword.keyword for ka in u1.user_keyword_associations],
+ ["l1", "l2", "l3"],
+ )
+
+ if init:
+ u1 = User(keywords=[Keyword("k1"), Keyword("k2"), Keyword("k3")])
+ else:
+ u1 = User()
+ u1.keywords = [Keyword("k1"), Keyword("k2"), Keyword("k3")]
+
+ eq_(u1.keywords, [Keyword("k1"), Keyword("k2"), Keyword("k3")])
+
+ eq_(
+ [ka.keyword.keyword for ka in u1.user_keyword_associations],
+ ["k1", "k2", "k3"],
+ )
+
+ return u1
+
+ def _keyword_mapping(self, User, decl_base):
+ class UserKeywordAssociation(decl_base):
+ __tablename__ = "user_keyword"
+ user_id: Mapped[int] = mapped_column(
+ ForeignKey("user.id"), primary_key=True
+ )
+ keyword_id: Mapped[int] = mapped_column(
+ ForeignKey("keyword.id"), primary_key=True
+ )
+
+ user: Mapped[User] = relationship(
+ back_populates="user_keyword_associations",
+ )
+
+ keyword: Mapped[Keyword] = relationship()
+
+ def __init__(self, keyword=None, user=None):
+ self.user = user
+ self.keyword = keyword
+
+ class Keyword(ComparableMixin, decl_base):
+ __tablename__ = "keyword"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ keyword: Mapped[str] = mapped_column()
+
+ def __init__(self, keyword):
+ self.keyword = keyword
+
+ return UserKeywordAssociation, Keyword
+
+ def _dc_keyword_mapping(self, User, dc_decl_base):
+ class UserKeywordAssociation(dc_decl_base):
+ __tablename__ = "user_keyword"
+ user_id: Mapped[int] = mapped_column(
+ ForeignKey("user.id"), primary_key=True, init=False
+ )
+ keyword_id: Mapped[int] = mapped_column(
+ ForeignKey("keyword.id"), primary_key=True, init=False
+ )
+
+ keyword: Mapped[Keyword] = relationship(default=None)
+
+ user: Mapped[User] = relationship(
+ back_populates="user_keyword_associations", default=None
+ )
+
+ class Keyword(dc_decl_base):
+ __tablename__ = "keyword"
+ id: Mapped[int] = mapped_column(primary_key=True, init=False)
+ keyword: Mapped[str] = mapped_column(init=True)
+
+ return UserKeywordAssociation, Keyword