mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-05-14 04:37:15 -04:00
call super().__init_subclass__(); support GenericAlias
Improved the :class:`.DeclarativeBase` class so that when combined with other mixins like :class:`.MappedAsDataclass`, the order of the classes may be in either order. Added support for mapped classes that are also ``Generic`` subclasses, to be specified as a ``GenericAlias`` object (e.g. ``MyClass[str]``) within statements and calls to :func:`_sa.inspect`. Fixes: #8665 Change-Id: I03063a28b0438a44b9e028fd9d45e8ce08bd18c4
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
.. change::
|
||||
:tags: bug, declarative, orm
|
||||
:tickets: 8665
|
||||
|
||||
Improved the :class:`.DeclarativeBase` class so that when combined with
|
||||
other mixins like :class:`.MappedAsDataclass`, the order of the classes may
|
||||
be in either order.
|
||||
|
||||
|
||||
.. change::
|
||||
:tags: usecase, declarative, orm
|
||||
:tickets: 8665
|
||||
|
||||
Added support for mapped classes that are also ``Generic`` subclasses,
|
||||
to be specified as a ``GenericAlias`` object (e.g. ``MyClass[str]``)
|
||||
within statements and calls to :func:`_sa.inspect`.
|
||||
|
||||
|
||||
@@ -581,7 +581,6 @@ class MappedAsDataclass(metaclass=DCTransformDeclarative):
|
||||
match_args: Union[_NoArg, bool] = _NoArg.NO_ARG,
|
||||
kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
|
||||
) -> None:
|
||||
|
||||
apply_dc_transforms: _DataclassArguments = {
|
||||
"init": init,
|
||||
"repr": repr,
|
||||
@@ -696,6 +695,7 @@ class DeclarativeBase(
|
||||
_setup_declarative_base(cls)
|
||||
else:
|
||||
_as_declarative(cls._sa_registry, cls, cls.__dict__)
|
||||
super().__init_subclass__()
|
||||
|
||||
|
||||
def _check_not_declarative(cls: Type[Any], base: Type[Any]) -> None:
|
||||
|
||||
@@ -79,6 +79,7 @@ from ..util.langhelpers import MemoizedSlots
|
||||
from ..util.typing import de_stringify_annotation
|
||||
from ..util.typing import is_origin_of_cls
|
||||
from ..util.typing import Literal
|
||||
from ..util.typing import typing_get_origin
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ._typing import _EntityType
|
||||
@@ -1361,6 +1362,18 @@ def _inspect_mc(
|
||||
return mapper
|
||||
|
||||
|
||||
GenericAlias = type(List[_T])
|
||||
|
||||
|
||||
@inspection._inspects(GenericAlias)
|
||||
def _inspect_generic_alias(
|
||||
class_: Type[_O],
|
||||
) -> Optional[Mapper[_O]]:
|
||||
|
||||
origin = cast("Type[_O]", typing_get_origin(class_))
|
||||
return _inspect_mc(origin)
|
||||
|
||||
|
||||
@inspection._self_inspects
|
||||
class Bundle(
|
||||
ORMColumnsClauseRole[_T],
|
||||
|
||||
@@ -3,10 +3,13 @@ import inspect as pyinspect
|
||||
from itertools import product
|
||||
from typing import Any
|
||||
from typing import ClassVar
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
from unittest import mock
|
||||
|
||||
from typing_extensions import Annotated
|
||||
@@ -17,6 +20,7 @@ from sqlalchemy import ForeignKey
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import JSON
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import testing
|
||||
@@ -47,6 +51,29 @@ from sqlalchemy.util import compat
|
||||
|
||||
|
||||
class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase):
|
||||
@testing.fixture(params=["(MAD, DB)", "(DB, MAD)"])
|
||||
def dc_decl_base(self, request, metadata):
|
||||
_md = metadata
|
||||
|
||||
if request.param == "(MAD, DB)":
|
||||
|
||||
class Base(MappedAsDataclass, DeclarativeBase):
|
||||
metadata = _md
|
||||
type_annotation_map = {
|
||||
str: String().with_variant(String(50), "mysql", "mariadb")
|
||||
}
|
||||
|
||||
else:
|
||||
# test #8665 by reversing the order of the classes
|
||||
class Base(DeclarativeBase, MappedAsDataclass):
|
||||
metadata = _md
|
||||
type_annotation_map = {
|
||||
str: String().with_variant(String(50), "mysql", "mariadb")
|
||||
}
|
||||
|
||||
yield Base
|
||||
Base.registry.dispose()
|
||||
|
||||
def test_basic_constructor_repr_base_cls(
|
||||
self, dc_decl_base: Type[MappedAsDataclass]
|
||||
):
|
||||
@@ -111,6 +138,33 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase):
|
||||
a3 = A("data")
|
||||
eq_(repr(a3), "some_module.A(id=None, data='data', x=None, bs=[])")
|
||||
|
||||
def test_generic_class(self):
|
||||
"""further test for #8665"""
|
||||
|
||||
T_Value = TypeVar("T_Value")
|
||||
|
||||
class SomeBaseClass(DeclarativeBase):
|
||||
pass
|
||||
|
||||
class GenericSetting(
|
||||
MappedAsDataclass, SomeBaseClass, Generic[T_Value]
|
||||
):
|
||||
__tablename__ = "xx"
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, init=False
|
||||
)
|
||||
|
||||
key: Mapped[str] = mapped_column(String, init=True)
|
||||
|
||||
value: Mapped[T_Value] = mapped_column(
|
||||
JSON, init=True, default_factory=lambda: {}
|
||||
)
|
||||
|
||||
new_instance: GenericSetting[ # noqa: F841
|
||||
Dict[str, Any]
|
||||
] = GenericSetting(key="x", value={"foo": "bar"})
|
||||
|
||||
def test_no_anno_doesnt_go_into_dc(
|
||||
self, dc_decl_base: Type[MappedAsDataclass]
|
||||
):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import dataclasses
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
from typing import ClassVar
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
@@ -22,6 +23,7 @@ from sqlalchemy import func
|
||||
from sqlalchemy import Identity
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import JSON
|
||||
from sqlalchemy import Numeric
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import String
|
||||
@@ -30,6 +32,7 @@ from sqlalchemy import testing
|
||||
from sqlalchemy import types
|
||||
from sqlalchemy import VARCHAR
|
||||
from sqlalchemy.exc import ArgumentError
|
||||
from sqlalchemy.ext.mutable import MutableDict
|
||||
from sqlalchemy.orm import as_declarative
|
||||
from sqlalchemy.orm import composite
|
||||
from sqlalchemy.orm import declarative_base
|
||||
@@ -39,12 +42,14 @@ from sqlalchemy.orm import deferred
|
||||
from sqlalchemy.orm import DynamicMapped
|
||||
from sqlalchemy.orm import Mapped
|
||||
from sqlalchemy.orm import mapped_column
|
||||
from sqlalchemy.orm import MappedAsDataclass
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import undefer
|
||||
from sqlalchemy.orm import WriteOnlyMapped
|
||||
from sqlalchemy.orm.collections import attribute_keyed_dict
|
||||
from sqlalchemy.orm.collections import KeyFuncDict
|
||||
from sqlalchemy.schema import CreateTable
|
||||
from sqlalchemy.testing import AssertsCompiledSQL
|
||||
from sqlalchemy.testing import eq_
|
||||
from sqlalchemy.testing import expect_raises
|
||||
from sqlalchemy.testing import expect_raises_message
|
||||
@@ -1898,3 +1903,52 @@ class WriteOnlyRelationshipTest(fixtures.TestBase):
|
||||
bs: WriteOnlyMapped[B] = relationship()
|
||||
|
||||
self._assertions(A, B, "write_only")
|
||||
|
||||
|
||||
class GenericMappingQueryTest(AssertsCompiledSQL, fixtures.TestBase):
|
||||
"""test the Generic support added as part of #8665"""
|
||||
|
||||
__dialect__ = "default"
|
||||
|
||||
@testing.fixture
|
||||
def mapping(self):
|
||||
T_Value = TypeVar("T_Value")
|
||||
|
||||
class SomeBaseClass(DeclarativeBase):
|
||||
pass
|
||||
|
||||
class GenericSetting(
|
||||
MappedAsDataclass, SomeBaseClass, Generic[T_Value]
|
||||
):
|
||||
"""Represents key value pairs for settings or values"""
|
||||
|
||||
__tablename__ = "xx"
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, init=False
|
||||
)
|
||||
|
||||
key: Mapped[str] = mapped_column(String, init=True)
|
||||
|
||||
value: Mapped[T_Value] = mapped_column(
|
||||
MutableDict.as_mutable(JSON),
|
||||
init=True,
|
||||
default_factory=lambda: {},
|
||||
)
|
||||
|
||||
return GenericSetting
|
||||
|
||||
def test_inspect(self, mapping):
|
||||
GenericSetting = mapping
|
||||
|
||||
typ = GenericSetting[Dict[str, Any]]
|
||||
is_(inspect(typ), GenericSetting.__mapper__)
|
||||
|
||||
def test_select(self, mapping):
|
||||
GenericSetting = mapping
|
||||
|
||||
typ = GenericSetting[Dict[str, Any]]
|
||||
self.assert_compile(
|
||||
select(typ).where(typ.key == "x"),
|
||||
"SELECT xx.id, xx.key, xx.value FROM xx WHERE xx.key = :key_1",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user