:tags: bug, orm
:tickets: 11955
+ .. note:: this change has been revised in version 2.0.44. Simple matches
+ of ``TypeAliasType`` without a type map entry are no longer deprecated.
+
Consistently handle ``TypeAliasType`` (defined in PEP 695) obtained with
the ``type X = int`` syntax introduced in python 3.12. Now in all cases one
such alias must be explicitly added to the type map for it to be usable
--- /dev/null
+.. change::
+ :tags: usecase, orm
+ :tickets: 12829
+
+ The way ORM Annotated Declarative interprets Python :pep:`695` type aliases
+ in ``Mapped[]`` annotations has been refined to expand the lookup scheme. A
+ PEP 695 type can now be resolved based on either its direct presence in
+ :paramref:`_orm.registry.type_annotation_map` or its immediate resolved
+ value, as long as a recursive lookup across multiple pep-695 types is not
+ required for it to resolve. This change reverses part of the restrictions
+ introduced in 2.0.37 as part of :ticket:`11955`, which deprecated (and
+ disallowed in 2.1) the ability to resolve any PEP 695 type that was not
+ explicitly present in :paramref:`_orm.registry.type_annotation_map`.
+ Recursive lookups of PEP 695 types remains deprecated in 2.0 and disallowed
+ in version 2.1, as do implicit lookups of ``NewType`` types without an
+ entry in :paramref:`_orm.registry.type_annotation_map`.
+
+ Additionally, new support has been added for generic PEP 695 aliases that
+ refer to PEP 593 ``Annotated`` constructs containing
+ :func:`_orm.mapped_column` configurations. See the sections below for
+ examples.
+
+ .. seealso::
+
+ :ref:`orm_declarative_type_map_pep695_types`
+
+ :ref:`orm_declarative_mapped_column_generic_pep593`
Support for Type Alias Types (defined by PEP 695) and NewType
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
In contrast to the typing lookup described in
:ref:`orm_declarative_type_map_union_types`, Python typing also includes two
ways to create a composed type in a more formal way, using ``typing.NewType`` as
name), and this difference is honored in how SQLAlchemy resolves these
types from the type map.
-.. versionchanged:: 2.0.37 The behaviors described in this section for ``typing.NewType``
- as well as :pep:`695` ``type`` have been formalized and corrected.
- Deprecation warnings are now emitted for "loose matching" patterns that have
- worked in some 2.0 releases, but are to be removed in SQLAlchemy 2.1.
+.. versionchanged:: 2.0.44 Support for resolving pep-695 types without a
+ corresponding entry in :paramref:`_orm.registry.type_annotation_map`
+ has been expanded, reversing part of the restrictions introduced in 2.0.37.
Please ensure SQLAlchemy is up to date before attempting to use the features
described in this section.
+.. versionchanged:: 2.0.37 The behaviors described in this section for ``typing.NewType``
+ as well as :pep:`695` ``type`` were formalized to disallow these types
+ from being implicitly resolvable without entries in
+ :paramref:`_orm.registry.type_annotation_map`, with deprecation warnings
+ emitted when these patterns were detected. As of 2.0.44, a pep-695 type
+ is implicitly resolvable as long as the type it resolves to is present
+ in the type map.
+
The typing module allows the creation of "new types" using ``typing.NewType``::
from typing import NewType
nstr30 = NewType("nstr30", str)
nstr50 = NewType("nstr50", str)
-Additionally, in Python 3.12, a new feature defined by :pep:`695` was introduced which
-provides the ``type`` keyword to accomplish a similar task; using
-``type`` produces an object that is similar in many ways to ``typing.NewType``
-which is internally referred to as ``typing.TypeAliasType``::
+The ``NewType`` construct creates types that are analogous to creating a
+subclass of the referenced type.
+
+Additionally, :pep:`695` introduced in Python 3.12 provides a new ``type``
+keyword for creating type aliases with greater separation of concerns from plain
+aliases, as well as succinct support for generics without requiring explicit
+use of ``TypeVar`` or ``Generic`` elements. Types created by the ``type``
+keyword are represented at runtime by ``typing.TypeAliasType``::
type SmallInt = int
type BigInt = int
type JsonScalar = str | float | bool | None
-For the purposes of how SQLAlchemy treats these type objects when used
-for SQL type lookup inside of :class:`_orm.Mapped`, it's important to note
-that Python does not consider two equivalent ``typing.TypeAliasType``
-or ``typing.NewType`` objects to be equal::
-
- # two typing.NewType objects are not equal even if they are both str
- >>> nstr50 == nstr30
- False
-
- # two TypeAliasType objects are not equal even if they are both int
- >>> SmallInt == BigInt
- False
-
- # an equivalent union is not equal to JsonScalar
- >>> JsonScalar == str | float | bool | None
- False
-
-This is the opposite behavior from how ordinary unions are compared, and
-informs the correct behavior for SQLAlchemy's ``type_annotation_map``. When
-using ``typing.NewType`` or :pep:`695` ``type`` objects, the type object is
-expected to be explicit within the ``type_annotation_map`` for it to be matched
-from a :class:`_orm.Mapped` type, where the same object must be stated in order
-for a match to be made (excluding whether or not the type inside of
-:class:`_orm.Mapped` also unions on ``None``). This is distinct from the
-behavior described at :ref:`orm_declarative_type_map_union_types`, where a
-plain ``Union`` that is referenced directly will match to other ``Unions``
-based on the composition, rather than the object identity, of a particular type
-in ``type_annotation_map``.
-
-In the example below, the composed types for ``nstr30``, ``nstr50``,
-``SmallInt``, ``BigInt``, and ``JsonScalar`` have no overlap with each other
-and can be named distinctly within each :class:`_orm.Mapped` construct, and
-are also all explicit in ``type_annotation_map``. Any of these types may
-also be unioned with ``None`` or declared as ``Optional[]`` without affecting
-the lookup, only deriving column nullability::
+Both ``NewType`` and pep-695 ``type`` constructs may be used as arguments
+within :class:`_orm.Mapped` annotations, where they will be resolved to Python
+types using the following rules:
- from typing import NewType
+* When a ``TypeAliasType`` or ``NewType`` object is present in the
+ :paramref:`_orm.registry.type_annotation_map`, it will resolve directly::
- from sqlalchemy import SmallInteger, BigInteger, JSON, String
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
- from sqlalchemy.schema import CreateTable
+ from typing import NewType
+ from sqlalchemy import String, BigInteger
nstr30 = NewType("nstr30", str)
- nstr50 = NewType("nstr50", str)
- type SmallInt = int
type BigInt = int
- type JsonScalar = str | float | bool | None
- class TABase(DeclarativeBase):
- type_annotation_map = {
- nstr30: String(30),
- nstr50: String(50),
- SmallInt: SmallInteger,
- BigInteger: BigInteger,
- JsonScalar: JSON,
- }
+ class Base(DeclarativeBase):
+ type_annotation_map = {nstr30: String(30), BigInt: BigInteger}
- class SomeClass(TABase):
+ class SomeClass(Base):
__tablename__ = "some_table"
- id: Mapped[int] = mapped_column(primary_key=True)
- normal_str: Mapped[str]
+ # BigInt is in the type_annotation_map. So this
+ # will resolve to sqlalchemy.BigInteger
+ id: Mapped[BigInt] = mapped_column(primary_key=True)
- short_str: Mapped[nstr30]
- long_str_nullable: Mapped[nstr50 | None]
+ # nstr30 is in the type_annotation_map. So this
+ # will resolve to sqlalchemy.String(30)
+ data: Mapped[nstr30]
- small_int: Mapped[SmallInt]
- big_int: Mapped[BigInteger]
- scalar_col: Mapped[JsonScalar]
+* A ``TypeAliasType`` that refers **directly** to another type present
+ in the type map will resolve against that type::
-a CREATE TABLE for the above mapping will illustrate the different variants
-of integer and string we've configured, and looks like:
+ type PlainInt = int
-.. sourcecode:: pycon+sql
- >>> print(CreateTable(SomeClass.__table__))
- {printsql}CREATE TABLE some_table (
- id INTEGER NOT NULL,
- normal_str VARCHAR NOT NULL,
- short_str VARCHAR(30) NOT NULL,
- long_str_nullable VARCHAR(50),
- small_int SMALLINT NOT NULL,
- big_int BIGINT NOT NULL,
- scalar_col JSON,
- PRIMARY KEY (id)
- )
+ class Base(DeclarativeBase):
+ pass
+
+
+ class SomeClass(Base):
+ __tablename__ = "some_table"
+
+ # PlainInt refers to int, which is one of the default types
+ # already in the type_annotation_map. So this
+ # will resolve to sqlalchemy.Integer via the int type
+ id: Mapped[PlainInt] = mapped_column(primary_key=True)
+
+* A ``TypeAliasType`` that refers to another pep-695 ``TypeAliasType``
+ not present in the type map will not resolve (emits a deprecation
+ warning in 2.0), as this would involve a recursive lookup::
+
+ type PlainInt = int
+ type AlsoAnInt = PlainInt
+
+
+ class Base(DeclarativeBase):
+ pass
+
+
+ class SomeClass(Base):
+ __tablename__ = "some_table"
+
+ # AlsoAnInt refers to PlainInt, which is not in the type_annotation_map.
+ # This will emit a deprecation warning in 2.0, will fail in 2.1
+ id: Mapped[AlsoAnInt] = mapped_column(primary_key=True)
-Regarding nullability, the ``JsonScalar`` type includes ``None`` in its
-definition, which indicates a nullable column. Similarly the
-``long_str_nullable`` column applies a union of ``None`` to ``nstr50``,
-which matches to the ``nstr50`` type in the ``type_annotation_map`` while
-also applying nullability to the mapped column. The other columns all remain
-NOT NULL as they are not indicated as optional.
+* A ``NewType`` that is not in the type map will not resolve (emits a
+ deprecation warning in 2.0). Since ``NewType`` is analogous to creating an
+ entirely new type with different semantics than the type it extends, these
+ must be explicitly matched in the type map::
+
+
+ from typing import NewType
+
+ nstr30 = NewType("nstr30", str)
+
+
+ class Base(DeclarativeBase):
+ pass
+
+
+ class SomeClass(Base):
+ __tablename__ = "some_table"
+
+ # a NewType is a new kind of type, so this will emit a deprecation
+ # warning in 2.0 and fail in 2.1, as nstr30 is not present
+ # in the type_annotation_map.
+ id: Mapped[nstr30] = mapped_column(primary_key=True)
+
+For all of the above examples, any type that is combined with ``Optional[]``
+or ``| None`` will consider this to indicate the column is nullable, if
+no other directive for nullability is present.
+
+.. seealso::
+
+ :ref:`orm_declarative_mapped_column_generic_pep593`
.. _orm_declarative_mapped_column_type_map_pep593:
will raise a ``NotImplementedError`` exception at runtime, but
may be implemented in future releases.
+
+.. _orm_declarative_mapped_column_generic_pep593:
+
+Mapping Whole Column Declarations to Generic Python Types
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Using the ``Annotated`` approach from the previous section, we may also
+create a generic version that will apply particular :func:`_orm.mapped_column`
+elements across many different Python/SQL types in one step. Below
+illustrates a plain alias against a generic form of ``Annotated`` that
+will apply the ``primary_key=True`` option to any column to which it's applied::
+
+ from typing import Annotated
+ from typing import TypeVar
+
+ T = TypeVar("T", bound=Any)
+
+ PrimaryKey = Annotated[T, mapped_column(primary_key=True)]
+
+The above type can now apply ``primary_key=True`` to any Python type::
+
+ import uuid
+
+
+ class Base(DeclarativeBase):
+ pass
+
+
+ class A(Base):
+ __tablename__ = "a"
+
+ # will create an Integer primary key
+ id: Mapped[PrimaryKey[int]]
+
+
+ class B(Base):
+ __tablename__ = "b"
+
+ # will create a UUID primary key
+ id: Mapped[PrimaryKey[uuid.UUID]]
+
+The type alias may also be defined equivalently using the pep-695 ``type``
+keyword in Python 3.12 or above::
+
+ type PrimaryKey[T] = Annotated[T, mapped_column(primary_key=True)]
+
+.. versionadded:: 2.0.44 Generic pep-695 types may be used with pep-593
+ ``Annotated`` elements to create generic types that automatically
+ deliver :func:`_orm.mapped_column` arguments.
+
+
.. _orm_declarative_mapped_column_enums:
Using Python ``Enum`` or pep-586 ``Literal`` types in the type map
from ..util.typing import is_generic
from ..util.typing import is_literal
from ..util.typing import is_newtype
+from ..util.typing import is_pep593
from ..util.typing import is_pep695
from ..util.typing import Literal
from ..util.typing import LITERAL_TYPES
)
def _resolve_type(
- self, python_type: _MatchedOnType, _do_fallbacks: bool = True
+ self, python_type: _MatchedOnType, _do_fallbacks: bool = False
) -> Optional[sqltypes.TypeEngine[Any]]:
python_type_type: Type[Any]
search: Iterable[Tuple[_MatchedOnType, Type[Any]]]
if is_pep695(python_type):
# NOTE: assume there aren't type alias types of new types.
python_type_to_check = python_type
- while is_pep695(python_type_to_check):
+ while is_pep695(python_type_to_check) and not is_pep593(
+ python_type_to_check
+ ):
python_type_to_check = python_type_to_check.__value__
python_type_to_check = de_optionalize_union_types(
python_type_to_check
)
- kind = "TypeAliasType"
+ kind = "pep-695 type"
if is_newtype(python_type):
python_type_to_check = flatten_newtype(python_type)
kind = "NewType"
)
if res_after_fallback is not None:
assert kind is not None
- warn_deprecated(
- f"Matching the provided {kind} '{python_type}' on "
- "its resolved value without matching it in the "
- "type_annotation_map is deprecated; add this type to "
- "the type_annotation_map to allow it to match "
- "explicitly.",
- "2.0",
- )
+ if kind == "pep-695 type":
+ warn_deprecated(
+ f"Matching to {kind} '{python_type}' in "
+ "a recursive "
+ "fashion without the recursed type being present "
+ "in the type_annotation_map is deprecated; add "
+ "this type or its recursed value to "
+ "the type_annotation_map to allow it to match "
+ "explicitly.",
+ "2.0",
+ )
+ else:
+ warn_deprecated(
+ f"Matching the provided {kind} '{python_type}' on "
+ "its resolved value without matching it in the "
+ "type_annotation_map is deprecated; add this "
+ "type to "
+ "the type_annotation_map to allow it to match "
+ "explicitly.",
+ "2.0",
+ )
return res_after_fallback
return None
if not self._has_nullable:
self.column.nullable = nullable
- our_type = de_optionalize_union_types(argument)
-
find_mapped_in: Tuple[Any, ...] = ()
our_type_is_pep593 = False
raw_pep_593_type = None
+ raw_pep_695_type = None
+
+ our_type: Any = de_optionalize_union_types(argument)
+
+ if is_pep695(our_type):
+ raw_pep_695_type = our_type
+ our_type = de_optionalize_union_types(raw_pep_695_type.__value__)
+ our_args = get_args(raw_pep_695_type)
+ if our_args:
+ our_type = our_type[our_args]
if is_pep593(our_type):
our_type_is_pep593 = True
if nullable:
raw_pep_593_type = de_optionalize_union_types(raw_pep_593_type)
find_mapped_in = pep_593_components[1:]
- elif is_pep695(argument) and is_pep593(argument.__value__):
- # do not support nested annotation inside unions ets
- find_mapped_in = get_args(argument.__value__)[1:]
use_args_from: Optional[MappedColumn[Any]]
for elem in find_mapped_in:
else:
checks = [our_type]
+ if raw_pep_695_type is not None:
+ checks.insert(0, raw_pep_695_type)
+
for check_type in checks:
- new_sqltype = registry._resolve_type(check_type)
+ new_sqltype = registry._resolve_type(
+ check_type, _do_fallbacks=check_type is our_type
+ )
if new_sqltype is not None:
break
else:
"attribute Mapped annotation is the SQLAlchemy type "
f"{our_type}. Expected a Python type instead"
)
- elif is_a_type(our_type):
+ elif is_a_type(checks[0]):
+ if len(checks) == 1:
+ detail = (
+ "the type object is not resolvable by the registry"
+ )
+ elif len(checks) == 2:
+ detail = (
+ f"neither '{checks[0]}' nor '{checks[1]}' "
+ "are resolvable by the registry"
+ )
+ else:
+ detail = (
+ f"""none of {
+ ", ".join(f"'{t}'" for t in checks)
+ } """
+ "are resolvable by the registry"
+ )
raise orm_exc.MappedAnnotationError(
- "Could not locate SQLAlchemy Core type for Python "
- f"type {our_type} inside the {self.column.key!r} "
- "attribute Mapped annotation"
+ "Could not locate SQLAlchemy Core type when resolving "
+ f"for Python type indicated by '{checks[0]}' inside "
+ "the "
+ f"Mapped[] annotation for the {self.column.key!r} "
+ f"attribute; {detail}"
)
else:
raise orm_exc.MappedAnnotationError(
f"The object provided inside the {self.column.key!r} "
"attribute Mapped annotation is not a Python type, "
- f"it's the object {our_type!r}. Expected a Python "
+ f"it's the object {argument!r}. Expected a Python "
"type."
)
def is_a_type(type_: Any) -> bool:
return (
isinstance(type_, type)
- or hasattr(type_, "__origin__")
- or type_.__module__ in ("typing", "typing_extensions")
+ or get_origin(type_) is not None
+ or getattr(type_, "__module__", None)
+ in ("typing", "typing_extensions")
or type(type_).__mro__[0].__module__ in ("typing", "typing_extensions")
)
)
_RecursiveLiteral695 = TypeAliasType("_RecursiveLiteral695", _Literal695)
+_GenericPep593TypeAlias = Annotated[TV, mapped_column(info={"hi": "there"})]
+
+_GenericPep593Pep695 = TypingTypeAliasType(
+ "_GenericPep593Pep695",
+ Annotated[TV, mapped_column(info={"hi": "there"})],
+ type_params=(TV,),
+)
+
+_RecursivePep695Pep593 = TypingTypeAliasType(
+ "_RecursivePep695Pep593",
+ Annotated[_TypingStrPep695, mapped_column(info={"hi": "there"})],
+)
+
def expect_annotation_syntax_error(name):
return expect_raises_message(
assert Child.__mapper__.attrs.parent.strategy.use_get
- @testing.combinations(
- (BIGINT(),),
- (BIGINT,),
- (Integer().with_variant(BIGINT, "default")),
- (Integer().with_variant(BIGINT(), "default")),
- (BIGINT().with_variant(String(), "some_other_dialect")),
- )
- def test_type_map_varieties(self, typ):
- Base = declarative_base(type_annotation_map={int: typ})
-
- class MyClass(Base):
- __tablename__ = "mytable"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- x: Mapped[int]
- y: Mapped[int] = mapped_column()
- z: Mapped[int] = mapped_column(typ)
-
- self.assert_compile(
- CreateTable(MyClass.__table__),
- "CREATE TABLE mytable (id BIGINT NOT NULL, "
- "x BIGINT NOT NULL, y BIGINT NOT NULL, z BIGINT NOT NULL, "
- "PRIMARY KEY (id))",
- )
-
def test_required_no_arg(self, decl_base):
with expect_raises_message(
sa_exc.ArgumentError,
is_true(User.__table__.c.data.nullable)
assert isinstance(User.__table__.c.created_at.type, DateTime)
- def test_construct_lhs_type_missing(self, decl_base):
- global MyClass
-
- class MyClass:
- pass
-
- with expect_raises_message(
- sa_exc.ArgumentError,
- "Could not locate SQLAlchemy Core type for Python type "
- ".*MyClass.* inside the 'data' attribute Mapped annotation",
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[MyClass] = mapped_column()
-
- @testing.variation(
- "argtype",
- [
- "type",
- ("column", testing.requires.python310),
- ("mapped_column", testing.requires.python310),
- "column_class",
- "ref_to_type",
- ("ref_to_column", testing.requires.python310),
- ],
- )
- def test_construct_lhs_sqlalchemy_type(self, decl_base, argtype):
- """test for #12329.
-
- of note here are all the different messages we have for when the
- wrong thing is put into Mapped[], and in fact in #12329 we added
- another one.
-
- This is a lot of different messages, but at the same time they
- occur at different places in the interpretation of types. If
- we were to centralize all these messages, we'd still likely end up
- doing distinct messages for each scenario, so instead we added
- a new ArgumentError subclass MappedAnnotationError that provides
- some commonality to all of these cases.
-
-
- """
- expect_future_annotations = "annotations" in globals()
-
- if argtype.type:
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- # properties.py -> _init_column_for_annotation, type is
- # a SQL type
- "The type provided inside the 'data' attribute Mapped "
- "annotation is the SQLAlchemy type .*BigInteger.*. Expected "
- "a Python type instead",
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[BigInteger] = mapped_column()
-
- elif argtype.column:
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- # util.py -> _extract_mapped_subtype
- (
- re.escape(
- "Could not interpret annotation "
- "Mapped[Column('q', BigInteger)]."
- )
- if expect_future_annotations
- # properties.py -> _init_column_for_annotation, object is
- # not a SQL type or a python type, it's just some object
- else re.escape(
- "The object provided inside the 'data' attribute "
- "Mapped annotation is not a Python type, it's the "
- "object Column('q', BigInteger(), table=None). "
- "Expected a Python type."
- )
- ),
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[Column("q", BigInteger)] = ( # noqa: F821
- mapped_column()
- )
-
- elif argtype.mapped_column:
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- # properties.py -> _init_column_for_annotation, object is
- # not a SQL type or a python type, it's just some object
- # interestingly, this raises at the same point for both
- # future annotations mode and legacy annotations mode
- r"The object provided inside the 'data' attribute "
- "Mapped annotation is not a Python type, it's the object "
- r"\<sqlalchemy.orm.properties.MappedColumn.*\>. "
- "Expected a Python type.",
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- big_integer: Mapped[int] = mapped_column()
- data: Mapped[big_integer] = mapped_column()
-
- elif argtype.column_class:
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- # properties.py -> _init_column_for_annotation, type is not
- # a SQL type
- re.escape(
- "Could not locate SQLAlchemy Core type for Python type "
- "<class 'sqlalchemy.sql.schema.Column'> inside the "
- "'data' attribute Mapped annotation"
- ),
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[Column] = mapped_column()
-
- elif argtype.ref_to_type:
- mytype = BigInteger
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- (
- # decl_base.py -> _exract_mappable_attributes
- re.escape(
- "Could not resolve all types within mapped "
- 'annotation: "Mapped[mytype]"'
- )
- if expect_future_annotations
- # properties.py -> _init_column_for_annotation, type is
- # a SQL type
- else re.escape(
- "The type provided inside the 'data' attribute Mapped "
- "annotation is the SQLAlchemy type "
- "<class 'sqlalchemy.sql.sqltypes.BigInteger'>. "
- "Expected a Python type instead"
- )
- ),
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[mytype] = mapped_column()
-
- elif argtype.ref_to_column:
- mycol = Column("q", BigInteger)
-
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- # decl_base.py -> _exract_mappable_attributes
- (
- re.escape(
- "Could not resolve all types within mapped "
- 'annotation: "Mapped[mycol]"'
- )
- if expect_future_annotations
- else
- # properties.py -> _init_column_for_annotation, object is
- # not a SQL type or a python type, it's just some object
- re.escape(
- "The object provided inside the 'data' attribute "
- "Mapped "
- "annotation is not a Python type, it's the object "
- "Column('q', BigInteger(), table=None). "
- "Expected a Python type."
- )
- ),
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[mycol] = mapped_column()
-
- else:
- argtype.fail()
-
def test_construct_rhs_type_override_lhs(self, decl_base):
class Element(decl_base):
__tablename__ = "element"
id: Mapped["int"] = mapped_column(primary_key=True)
data_one: Mapped["str"]
- def test_pep593_types_as_typemap_keys(
- self, decl_base: Type[DeclarativeBase]
- ):
- """neat!!!"""
- global str50, str30, opt_str50, opt_str30
+ @testing.requires.python38
+ def test_typing_literal_identity(self, decl_base):
+ """See issue #11820"""
- str50 = Annotated[str, 50]
- str30 = Annotated[str, 30]
- opt_str50 = Optional[str50]
- opt_str30 = Optional[str30]
+ class Foo(decl_base):
+ __tablename__ = "footable"
- decl_base.registry.update_type_annotation_map(
- {str50: String(50), str30: String(30)}
- )
+ id: Mapped[int] = mapped_column(primary_key=True)
+ t: Mapped[_TypingLiteral]
+ te: Mapped[_TypingExtensionsLiteral]
- class MyClass(decl_base):
- __tablename__ = "my_table"
+ for col in (Foo.__table__.c.t, Foo.__table__.c.te):
+ is_true(isinstance(col.type, Enum))
+ eq_(col.type.enums, ["a", "b"])
+ is_(col.type.native_enum, False)
- id: Mapped[str50] = mapped_column(primary_key=True)
- data_one: Mapped[str30]
- data_two: Mapped[opt_str30]
- data_three: Mapped[str50]
- data_four: Mapped[opt_str50]
- data_five: Mapped[str]
- data_six: Mapped[Optional[str]]
+ @testing.requires.python310
+ def test_we_got_all_attrs_test_annotated(self):
+ argnames = _py_inspect.getfullargspec(mapped_column)
+ assert _annotated_names_tested.issuperset(argnames.kwonlyargs), (
+ f"annotated attributes were not tested: "
+ f"{set(argnames.kwonlyargs).difference(_annotated_names_tested)}"
+ )
- eq_(MyClass.__table__.c.data_one.type.length, 30)
- is_false(MyClass.__table__.c.data_one.nullable)
- eq_(MyClass.__table__.c.data_two.type.length, 30)
- is_true(MyClass.__table__.c.data_two.nullable)
- eq_(MyClass.__table__.c.data_three.type.length, 50)
-
- def test_plain_typealias_as_typemap_keys(
- self, decl_base: Type[DeclarativeBase]
+ @annotated_name_test_cases(
+ ("sort_order", 100, lambda sort_order: sort_order == 100),
+ ("nullable", False, lambda column: column.nullable is False),
+ (
+ "active_history",
+ True,
+ lambda column_property: column_property.active_history is True,
+ ),
+ (
+ "deferred",
+ True,
+ lambda column_property: column_property.deferred is True,
+ ),
+ (
+ "deferred",
+ _NoArg.NO_ARG,
+ lambda column_property: column_property is None,
+ ),
+ (
+ "deferred_group",
+ "mygroup",
+ lambda column_property: column_property.deferred is True
+ and column_property.group == "mygroup",
+ ),
+ (
+ "deferred_raiseload",
+ True,
+ lambda column_property: column_property.deferred is True
+ and column_property.raiseload is True,
+ ),
+ (
+ "server_default",
+ "25",
+ lambda column: column.server_default.arg == "25",
+ ),
+ (
+ "server_onupdate",
+ "25",
+ lambda column: column.server_onupdate.arg == "25",
+ ),
+ (
+ "default",
+ 25,
+ lambda column: column.default.arg == 25,
+ ),
+ (
+ "insert_default",
+ 25,
+ lambda column: column.default.arg == 25,
+ ),
+ (
+ "onupdate",
+ 25,
+ lambda column: column.onupdate.arg == 25,
+ ),
+ ("doc", "some doc", lambda column: column.doc == "some doc"),
+ (
+ "comment",
+ "some comment",
+ lambda column: column.comment == "some comment",
+ ),
+ ("index", True, lambda column: column.index is True),
+ ("index", _NoArg.NO_ARG, lambda column: column.index is None),
+ ("index", False, lambda column: column.index is False),
+ ("unique", True, lambda column: column.unique is True),
+ ("unique", False, lambda column: column.unique is False),
+ ("autoincrement", True, lambda column: column.autoincrement is True),
+ ("system", True, lambda column: column.system is True),
+ ("primary_key", True, lambda column: column.primary_key is True),
+ ("type_", BIGINT, lambda column: isinstance(column.type, BIGINT)),
+ ("info", {"foo": "bar"}, lambda column: column.info == {"foo": "bar"}),
+ (
+ "use_existing_column",
+ True,
+ lambda mc: mc._use_existing_column is True,
+ ),
+ (
+ "quote",
+ True,
+ exc.SADeprecationWarning(
+ "Can't use the 'key' or 'name' arguments in Annotated "
+ ),
+ ),
+ (
+ "key",
+ "mykey",
+ exc.SADeprecationWarning(
+ "Can't use the 'key' or 'name' arguments in Annotated "
+ ),
+ ),
+ (
+ "name",
+ "mykey",
+ exc.SADeprecationWarning(
+ "Can't use the 'key' or 'name' arguments in Annotated "
+ ),
+ ),
+ (
+ "kw_only",
+ True,
+ exc.SADeprecationWarning(
+ "Argument 'kw_only' is a dataclass argument "
+ ),
+ testing.requires.python310,
+ ),
+ (
+ "compare",
+ True,
+ exc.SADeprecationWarning(
+ "Argument 'compare' is a dataclass argument "
+ ),
+ testing.requires.python310,
+ ),
+ (
+ "default_factory",
+ lambda: 25,
+ exc.SADeprecationWarning(
+ "Argument 'default_factory' is a dataclass argument "
+ ),
+ ),
+ (
+ "repr",
+ True,
+ exc.SADeprecationWarning(
+ "Argument 'repr' is a dataclass argument "
+ ),
+ ),
+ (
+ "init",
+ True,
+ exc.SADeprecationWarning(
+ "Argument 'init' is a dataclass argument"
+ ),
+ ),
+ (
+ "hash",
+ True,
+ exc.SADeprecationWarning(
+ "Argument 'hash' is a dataclass argument"
+ ),
+ ),
+ (
+ "dataclass_metadata",
+ {},
+ exc.SADeprecationWarning(
+ "Argument 'dataclass_metadata' is a dataclass argument"
+ ),
+ ),
+ argnames="argname, argument, assertion",
+ )
+ @testing.variation("use_annotated", [True, False, "control"])
+ def test_names_encountered_for_annotated(
+ self, argname, argument, assertion, use_annotated, decl_base
):
- decl_base.registry.update_type_annotation_map(
- {_UnionTypeAlias: JSON, _StrTypeAlias: String(30)}
+ global myint
+
+ if argument is not _NoArg.NO_ARG:
+ kw = {argname: argument}
+
+ if argname == "quote":
+ kw["name"] = "somename"
+ else:
+ kw = {}
+
+ is_warning = isinstance(assertion, exc.SADeprecationWarning)
+ is_dataclass = argname in (
+ "kw_only",
+ "init",
+ "repr",
+ "compare",
+ "default_factory",
+ "hash",
+ "dataclass_metadata",
)
- class Test(decl_base):
- __tablename__ = "test"
+ if is_dataclass:
+
+ class Base(MappedAsDataclass, decl_base):
+ __abstract__ = True
+
+ else:
+ Base = decl_base
+
+ if use_annotated.control:
+ # test in reverse; that kw set on the main mapped_column() takes
+ # effect when the Annotated is there also and does not have the
+ # kw
+ amc = mapped_column()
+ myint = Annotated[int, amc]
+
+ mc = mapped_column(**kw)
+
+ class User(Base):
+ __tablename__ = "user"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ myname: Mapped[myint] = mc
+
+ elif use_annotated:
+ amc = mapped_column(**kw)
+ myint = Annotated[int, amc]
+
+ mc = mapped_column()
+
+ if is_warning:
+ with expect_deprecated(assertion.args[0]):
+
+ class User(Base):
+ __tablename__ = "user"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ myname: Mapped[myint] = mc
+
+ else:
+
+ class User(Base):
+ __tablename__ = "user"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ myname: Mapped[myint] = mc
+
+ else:
+ mc = cast(MappedColumn, mapped_column(**kw))
+
+ mapper_prop = mc.mapper_property_to_assign
+ column_to_assign, sort_order = mc.columns_to_assign[0]
+
+ if not is_warning:
+ assert_result = testing.resolve_lambda(
+ assertion,
+ sort_order=sort_order,
+ column_property=mapper_prop,
+ column=column_to_assign,
+ mc=mc,
+ )
+ assert assert_result
+ elif is_dataclass and (not use_annotated or use_annotated.control):
+ eq_(
+ getattr(mc._attribute_options, f"dataclasses_{argname}"),
+ argument,
+ )
+
+ @testing.combinations(("index",), ("unique",), argnames="paramname")
+ @testing.combinations((True,), (False,), (None,), argnames="orig")
+ @testing.combinations((True,), (False,), (None,), argnames="merging")
+ def test_index_unique_combinations(
+ self, paramname, orig, merging, decl_base
+ ):
+ """test #11091"""
+
+ global myint
+
+ amc = mapped_column(**{paramname: merging})
+ myint = Annotated[int, amc]
+
+ mc = mapped_column(**{paramname: orig})
+
+ class User(decl_base):
+ __tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[_StrTypeAlias]
- structure: Mapped[_UnionTypeAlias]
+ myname: Mapped[myint] = mc
- eq_(Test.__table__.c.data.type.length, 30)
- is_(Test.__table__.c.structure.type._type_affinity, JSON)
+ result = getattr(User.__table__.c.myname, paramname)
+ if orig is None:
+ is_(result, merging)
+ else:
+ is_(result, orig)
@testing.variation(
- "option",
+ "union",
[
- "plain",
"union",
- "union_604",
+ ("pep604", requires.python310),
"union_null",
- "union_null_604",
- "optional",
- "optional_union",
- "optional_union_604",
- "union_newtype",
- "union_null_newtype",
- "union_695",
- "union_null_695",
+ ("pep604_null", requires.python310),
],
)
- @testing.variation("in_map", ["yes", "no", "value"])
- @testing.requires.python312
- def test_pep695_behavior(self, decl_base, in_map, option):
- """Issue #11955"""
- global tat
+ def test_unions(self, union):
+ global UnionType
+ our_type = Numeric(10, 2)
- if option.plain:
- tat = TypeAliasType("tat", str)
- elif option.union:
- tat = TypeAliasType("tat", Union[str, int])
- elif option.union_604:
- tat = TypeAliasType("tat", str | int)
- elif option.union_null:
- tat = TypeAliasType("tat", Union[str, int, None])
- elif option.union_null_604:
- tat = TypeAliasType("tat", str | int | None)
- elif option.optional:
- tat = TypeAliasType("tat", Optional[str])
- elif option.optional_union:
- tat = TypeAliasType("tat", Optional[Union[str, int]])
- elif option.optional_union_604:
- tat = TypeAliasType("tat", Optional[str | int])
- elif option.union_newtype:
- # this seems to be illegal for typing but "works"
- tat = NewType("tat", Union[str, int])
- elif option.union_null_newtype:
- # this seems to be illegal for typing but "works"
- tat = NewType("tat", Union[str, int, None])
- elif option.union_695:
- tat = TypeAliasType("tat", str | int)
- elif option.union_null_695:
- tat = TypeAliasType("tat", str | int | None)
+ if union.union:
+ UnionType = Union[float, Decimal]
+ elif union.union_null:
+ UnionType = Union[float, Decimal, None]
+ elif union.pep604:
+ UnionType = float | Decimal
+ elif union.pep604_null:
+ UnionType = float | Decimal | None
else:
- option.fail()
+ union.fail()
- if in_map.yes:
- decl_base.registry.update_type_annotation_map({tat: String(99)})
- elif in_map.value and "newtype" not in option.name:
- decl_base.registry.update_type_annotation_map(
- {tat.__value__: String(99)}
+ class Base(DeclarativeBase):
+ type_annotation_map = {UnionType: our_type}
+
+ class User(Base):
+ __tablename__ = "users"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+
+ data: Mapped[Union[float, Decimal]]
+ reverse_data: Mapped[Union[Decimal, float]]
+
+ optional_data: Mapped[Optional[Union[float, Decimal]]] = (
+ mapped_column()
)
- def declare():
- class Test(decl_base):
- __tablename__ = "test"
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[tat]
+ # use Optional directly
+ reverse_optional_data: Mapped[Optional[Union[Decimal, float]]] = (
+ mapped_column()
+ )
- return Test.__table__.c.data
+ # use Union with None, same as Optional but presents differently
+ # (Optional object with __origin__ Union vs. Union)
+ reverse_u_optional_data: Mapped[Union[Decimal, float, None]] = (
+ mapped_column()
+ )
- if in_map.yes:
- col = declare()
- length = 99
- elif (
- in_map.value
- and "newtype" not in option.name
- or option.optional
- or option.plain
- ):
- with expect_deprecated(
- "Matching the provided TypeAliasType 'tat' on its "
- "resolved value without matching it in the "
- "type_annotation_map is deprecated; add this type to the "
- "type_annotation_map to allow it to match explicitly.",
- ):
- col = declare()
- length = 99 if in_map.value else None
- else:
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- r"Could not locate SQLAlchemy Core type for Python type .*tat "
- "inside the 'data' attribute Mapped annotation",
- ):
- declare()
- return
+ refer_union: Mapped[UnionType]
+ refer_union_optional: Mapped[Optional[UnionType]]
- is_true(isinstance(col.type, String))
- eq_(col.type.length, length)
- nullable = "null" in option.name or "optional" in option.name
- eq_(col.nullable, nullable)
+ # py38, 37 does not automatically flatten unions, add extra tests
+ # for this. maintain these in order to catch future regressions
+ # in the behavior of ``Union``
+ unflat_union_optional_data: Mapped[
+ Union[Union[Decimal, float, None], None]
+ ] = mapped_column()
- @testing.variation(
- "type_",
- [
- "str_extension",
- "str_typing",
- "generic_extension",
- "generic_typing",
- "generic_typed_extension",
- "generic_typed_typing",
- ],
- )
- @testing.requires.python312
- def test_pep695_typealias_as_typemap_keys(
- self, decl_base: Type[DeclarativeBase], type_
- ):
- """test #10807"""
+ float_data: Mapped[float] = mapped_column()
+ decimal_data: Mapped[Decimal] = mapped_column()
- decl_base.registry.update_type_annotation_map(
- {
- _UnionPep695: JSON,
- _StrPep695: String(30),
- _TypingStrPep695: String(30),
- _GenericPep695: String(30),
- _TypingGenericPep695: String(30),
- _GenericPep695Typed: String(30),
- _TypingGenericPep695Typed: String(30),
- }
- )
+ if compat.py310:
+ pep604_data: Mapped[float | Decimal] = mapped_column()
+ pep604_reverse: Mapped[Decimal | float] = mapped_column()
+ pep604_optional: Mapped[Decimal | float | None] = (
+ mapped_column()
+ )
+ pep604_data_fwd: Mapped["float | Decimal"] = mapped_column()
+ pep604_reverse_fwd: Mapped["Decimal | float"] = mapped_column()
+ pep604_optional_fwd: Mapped["Decimal | float | None"] = (
+ mapped_column()
+ )
- class Test(decl_base):
- __tablename__ = "test"
- id: Mapped[int] = mapped_column(primary_key=True)
- if type_.str_extension:
- data: Mapped[_StrPep695]
- elif type_.str_typing:
- data: Mapped[_TypingStrPep695]
- elif type_.generic_extension:
- data: Mapped[_GenericPep695]
- elif type_.generic_typing:
- data: Mapped[_TypingGenericPep695]
- elif type_.generic_typed_extension:
- data: Mapped[_GenericPep695Typed]
- elif type_.generic_typed_typing:
- data: Mapped[_TypingGenericPep695Typed]
- else:
- type_.fail()
- structure: Mapped[_UnionPep695]
+ info = [
+ ("data", False),
+ ("reverse_data", False),
+ ("optional_data", True),
+ ("reverse_optional_data", True),
+ ("reverse_u_optional_data", True),
+ ("refer_union", "null" in union.name),
+ ("refer_union_optional", True),
+ ("unflat_union_optional_data", True),
+ ]
+ if compat.py310:
+ info += [
+ ("pep604_data", False),
+ ("pep604_reverse", False),
+ ("pep604_optional", True),
+ ("pep604_data_fwd", False),
+ ("pep604_reverse_fwd", False),
+ ("pep604_optional_fwd", True),
+ ]
+
+ for name, nullable in info:
+ col = User.__table__.c[name]
+ is_(col.type, our_type, name)
+ is_(col.nullable, nullable, name)
- eq_(Test.__table__.c.data.type._type_affinity, String)
- eq_(Test.__table__.c.data.type.length, 30)
- is_(Test.__table__.c.structure.type._type_affinity, JSON)
+ is_true(isinstance(User.__table__.c.float_data.type, Float))
+ ne_(User.__table__.c.float_data.type, our_type)
+
+ is_true(isinstance(User.__table__.c.decimal_data.type, Numeric))
+ ne_(User.__table__.c.decimal_data.type, our_type)
@testing.variation(
- "alias_type",
- ["none", "typekeyword", "typealias", "typekeyword_nested"],
+ "union",
+ [
+ "union",
+ ("pep604", requires.python310),
+ ("pep695", requires.python312),
+ ],
)
- @testing.requires.python312
- def test_extract_pep593_from_pep695(
- self, decl_base: Type[DeclarativeBase], alias_type
- ):
- """test #11130"""
- if alias_type.typekeyword:
- decl_base.registry.update_type_annotation_map(
- {strtypalias_keyword: VARCHAR(33)} # noqa: F821
- )
- if alias_type.typekeyword_nested:
- decl_base.registry.update_type_annotation_map(
- {strtypalias_keyword_nested: VARCHAR(42)} # noqa: F821
- )
-
- class MyClass(decl_base):
- __tablename__ = "my_table"
-
- id: Mapped[int] = mapped_column(primary_key=True)
+ def test_optional_in_annotation_map(self, union):
+ """See issue #11370"""
- if alias_type.typekeyword:
- data_one: Mapped[strtypalias_keyword] # noqa: F821
- elif alias_type.typealias:
- data_one: Mapped[strtypalias_ta] # noqa: F821
- elif alias_type.none:
- data_one: Mapped[strtypalias_plain] # noqa: F821
- elif alias_type.typekeyword_nested:
- data_one: Mapped[strtypalias_keyword_nested] # noqa: F821
+ class Base(DeclarativeBase):
+ if union.union:
+ type_annotation_map = {_Json: JSON}
+ elif union.pep604:
+ type_annotation_map = {_JsonPep604: JSON}
+ elif union.pep695:
+ type_annotation_map = {_JsonPep695: JSON} # noqa: F821
else:
- alias_type.fail()
+ union.fail()
- table = MyClass.__table__
- assert table is not None
+ class A(Base):
+ __tablename__ = "a"
- if alias_type.typekeyword_nested:
- # a nested annotation is not supported
- eq_(MyClass.data_one.expression.info, {})
- else:
- eq_(MyClass.data_one.expression.info, {"hi": "there"})
+ id: Mapped[int] = mapped_column(primary_key=True)
+ if union.union:
+ json1: Mapped[_Json]
+ json2: Mapped[_Json] = mapped_column(nullable=False)
+ elif union.pep604:
+ json1: Mapped[_JsonPep604]
+ json2: Mapped[_JsonPep604] = mapped_column(nullable=False)
+ elif union.pep695:
+ json1: Mapped[_JsonPep695] # noqa: F821
+ json2: Mapped[_JsonPep695] = mapped_column( # noqa: F821
+ nullable=False
+ )
+ else:
+ union.fail()
- if alias_type.typekeyword:
- eq_(MyClass.data_one.type.length, 33)
- elif alias_type.typekeyword_nested:
- eq_(MyClass.data_one.type.length, 42)
- else:
- eq_(MyClass.data_one.type.length, None)
+ is_(A.__table__.c.json1.type._type_affinity, JSON)
+ is_(A.__table__.c.json2.type._type_affinity, JSON)
+ is_true(A.__table__.c.json1.nullable)
+ is_false(A.__table__.c.json2.nullable)
@testing.variation(
- "type_",
+ "option",
[
- "literal",
- "literal_typing",
- "recursive",
- "not_literal",
- "not_literal_typing",
- "generic",
- "generic_typing",
- "generic_typed",
- "generic_typed_typing",
+ "not_optional",
+ "optional",
+ "optional_fwd_ref",
+ "union_none",
+ ("pep604", testing.requires.python310),
+ ("pep604_fwd_ref", testing.requires.python310),
],
)
- @testing.combinations(True, False, argnames="in_map")
- @testing.requires.python312
- def test_pep695_literal_defaults_to_enum(self, decl_base, type_, in_map):
- """test #11305."""
+ @testing.variation("brackets", ["oneset", "twosets"])
+ @testing.combinations(
+ "include_mc_type", "derive_from_anno", argnames="include_mc_type"
+ )
+ def test_optional_styles_nested_brackets(
+ self, option, brackets, include_mc_type
+ ):
+ """composed types test, includes tests that were added later for
+ #12207"""
- def declare():
- class Foo(decl_base):
- __tablename__ = "footable"
+ class Base(DeclarativeBase):
+ if testing.requires.python310.enabled:
+ type_annotation_map = {
+ Dict[str, Decimal]: JSON,
+ dict[str, Decimal]: JSON,
+ Union[List[int], List[str]]: JSON,
+ list[int] | list[str]: JSON,
+ }
+ else:
+ type_annotation_map = {
+ Dict[str, Decimal]: JSON,
+ Union[List[int], List[str]]: JSON,
+ }
- id: Mapped[int] = mapped_column(primary_key=True)
- if type_.recursive:
- status: Mapped[_RecursiveLiteral695] # noqa: F821
- elif type_.literal:
- status: Mapped[_Literal695] # noqa: F821
- elif type_.literal_typing:
- status: Mapped[_TypingLiteral695] # noqa: F821
- elif type_.not_literal:
- status: Mapped[_StrPep695] # noqa: F821
- elif type_.not_literal_typing:
- status: Mapped[_TypingStrPep695] # noqa: F821
- elif type_.generic:
- status: Mapped[_GenericPep695] # noqa: F821
- elif type_.generic_typing:
- status: Mapped[_TypingGenericPep695] # noqa: F821
- elif type_.generic_typed:
- status: Mapped[_GenericPep695Typed] # noqa: F821
- elif type_.generic_typed_typing:
- status: Mapped[_TypingGenericPep695Typed] # noqa: F821
- else:
- type_.fail()
+ if include_mc_type == "include_mc_type":
+ mc = mapped_column(JSON)
+ mc2 = mapped_column(JSON)
+ else:
+ mc = mapped_column()
+ mc2 = mapped_column()
- return Foo
+ class A(Base):
+ __tablename__ = "a"
- if in_map:
- decl_base.registry.update_type_annotation_map(
- {
- _Literal695: Enum(enum.Enum), # noqa: F821
- _TypingLiteral695: Enum(enum.Enum), # noqa: F821
- _RecursiveLiteral695: Enum(enum.Enum), # noqa: F821
- _StrPep695: Enum(enum.Enum), # noqa: F821
- _TypingStrPep695: Enum(enum.Enum), # noqa: F821
- _GenericPep695: Enum(enum.Enum), # noqa: F821
- _TypingGenericPep695: Enum(enum.Enum), # noqa: F821
- _GenericPep695Typed: Enum(enum.Enum), # noqa: F821
- _TypingGenericPep695Typed: Enum(enum.Enum), # noqa: F821
- }
- )
- if type_.recursive:
- with expect_deprecated(
- "Mapping recursive TypeAliasType '.+' that resolve to "
- "literal to generate an Enum is deprecated. SQLAlchemy "
- "2.1 will not support this use case. Please avoid using "
- "recursing TypeAliasType",
- ):
- Foo = declare()
- elif type_.literal or type_.literal_typing:
- Foo = declare()
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[str] = mapped_column()
+
+ if brackets.oneset:
+ if option.not_optional:
+ json: Mapped[Dict[str, Decimal]] = mapped_column() # type: ignore # noqa: E501
+ if testing.requires.python310.enabled:
+ json2: Mapped[dict[str, Decimal]] = mapped_column() # type: ignore # noqa: E501
+ elif option.optional:
+ json: Mapped[Optional[Dict[str, Decimal]]] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[Optional[dict[str, Decimal]]] = mc2
+ elif option.optional_fwd_ref:
+ json: Mapped["Optional[Dict[str, Decimal]]"] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped["Optional[dict[str, Decimal]]"] = mc2
+ elif option.union_none:
+ json: Mapped[Union[Dict[str, Decimal], None]] = mc
+ json2: Mapped[Union[None, Dict[str, Decimal]]] = mc2
+ elif option.pep604:
+ json: Mapped[dict[str, Decimal] | None] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[None | dict[str, Decimal]] = mc2
+ elif option.pep604_fwd_ref:
+ json: Mapped["dict[str, Decimal] | None"] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped["None | dict[str, Decimal]"] = mc2
+ elif brackets.twosets:
+ if option.not_optional:
+ json: Mapped[Union[List[int], List[str]]] = mapped_column() # type: ignore # noqa: E501
+ elif option.optional:
+ json: Mapped[Optional[Union[List[int], List[str]]]] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[
+ Optional[Union[list[int], list[str]]]
+ ] = mc2
+ elif option.optional_fwd_ref:
+ json: Mapped["Optional[Union[List[int], List[str]]]"] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[
+ "Optional[Union[list[int], list[str]]]"
+ ] = mc2
+ elif option.union_none:
+ json: Mapped[Union[List[int], List[str], None]] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[Union[None, list[int], list[str]]] = mc2
+ elif option.pep604:
+ json: Mapped[list[int] | list[str] | None] = mc
+ json2: Mapped[None | list[int] | list[str]] = mc2
+ elif option.pep604_fwd_ref:
+ json: Mapped["list[int] | list[str] | None"] = mc
+ json2: Mapped["None | list[int] | list[str]"] = mc2
else:
- with expect_raises_message(
- exc.ArgumentError,
- "Can't associate TypeAliasType '.+' to an Enum "
- "since it's not a direct alias of a Literal. Only "
- "aliases in this form `type my_alias = Literal.'a', "
- "'b'.` are supported when generating Enums.",
- ):
- declare()
- return
- elif (
- type_.generic
- or type_.generic_typing
- or type_.generic_typed
- or type_.generic_typed_typing
- ):
- # This behaves like 2.1 -> rationale is that no-one asked to
- # support such types and in 2.1 will already be like this
- # so it makes little sense to add support this late in the 2.0
- # series
+ brackets.fail()
+
+ is_(A.__table__.c.json.type._type_affinity, JSON)
+ if hasattr(A, "json2"):
+ is_(A.__table__.c.json2.type._type_affinity, JSON)
+ if option.not_optional:
+ is_false(A.__table__.c.json2.nullable)
+ else:
+ is_true(A.__table__.c.json2.nullable)
+
+ if option.not_optional:
+ is_false(A.__table__.c.json.nullable)
+ else:
+ is_true(A.__table__.c.json.nullable)
+
+ @testing.variation("optional", [True, False])
+ @testing.variation("provide_type", [True, False])
+ @testing.variation("add_to_type_map", [True, False])
+ def test_recursive_type(
+ self, decl_base, optional, provide_type, add_to_type_map
+ ):
+ """test #9553"""
+
+ global T
+
+ T = Dict[str, Optional["T"]]
+
+ if not provide_type and not add_to_type_map:
with expect_raises_message(
- exc.ArgumentError,
- "Could not locate SQLAlchemy Core type for Python type "
- ".+ inside the 'status' attribute Mapped annotation",
+ sa_exc.ArgumentError,
+ r"Could not locate SQLAlchemy.*" r".*ForwardRef\('T'\).*",
):
- declare()
+
+ class TypeTest(decl_base):
+ __tablename__ = "my_table"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ if optional:
+ type_test: Mapped[Optional[T]] = mapped_column()
+ else:
+ type_test: Mapped[T] = mapped_column()
+
return
+
else:
- with expect_deprecated(
- "Matching the provided TypeAliasType '.*' on its "
- "resolved value without matching it in the "
- "type_annotation_map is deprecated; add this type to the "
- "type_annotation_map to allow it to match explicitly.",
- ):
- Foo = declare()
- col = Foo.__table__.c.status
- if in_map and not type_.not_literal:
- is_true(isinstance(col.type, Enum))
- eq_(col.type.enums, ["to-do", "in-progress", "done"])
- is_(col.type.native_enum, False)
+ if add_to_type_map:
+ decl_base.registry.update_type_annotation_map({T: JSON()})
+
+ class TypeTest(decl_base):
+ __tablename__ = "my_table"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+
+ if add_to_type_map:
+ if optional:
+ type_test: Mapped[Optional[T]] = mapped_column()
+ else:
+ type_test: Mapped[T] = mapped_column()
+ else:
+ if optional:
+ type_test: Mapped[Optional[T]] = mapped_column(JSON())
+ else:
+ type_test: Mapped[T] = mapped_column(JSON())
+
+ if optional:
+ is_(TypeTest.__table__.c.type_test.nullable, True)
else:
- is_true(isinstance(col.type, String))
+ is_(TypeTest.__table__.c.type_test.nullable, False)
- @testing.requires.python38
- def test_typing_literal_identity(self, decl_base):
- """See issue #11820"""
+ self.assert_compile(
+ select(TypeTest),
+ "SELECT my_table.id, my_table.type_test FROM my_table",
+ )
- class Foo(decl_base):
- __tablename__ = "footable"
+ def test_missing_mapped_lhs(self, decl_base):
+ with expect_annotation_syntax_error("User.name"):
- id: Mapped[int] = mapped_column(primary_key=True)
- t: Mapped[_TypingLiteral]
- te: Mapped[_TypingExtensionsLiteral]
+ class User(decl_base):
+ __tablename__ = "users"
- for col in (Foo.__table__.c.t, Foo.__table__.c.te):
- is_true(isinstance(col.type, Enum))
- eq_(col.type.enums, ["a", "b"])
- is_(col.type.native_enum, False)
+ id: Mapped[int] = mapped_column(primary_key=True)
+ name: str = mapped_column() # type: ignore
- @testing.requires.python310
- def test_we_got_all_attrs_test_annotated(self):
- argnames = _py_inspect.getfullargspec(mapped_column)
- assert _annotated_names_tested.issuperset(argnames.kwonlyargs), (
- f"annotated attributes were not tested: "
- f"{set(argnames.kwonlyargs).difference(_annotated_names_tested)}"
+ def test_construct_lhs_separate_name(self, decl_base):
+ class User(decl_base):
+ __tablename__ = "users"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ name: Mapped[str] = mapped_column()
+ data: Mapped[Optional[str]] = mapped_column("the_data")
+
+ self.assert_compile(
+ select(User.data), "SELECT users.the_data FROM users"
)
+ is_true(User.__table__.c.the_data.nullable)
- @annotated_name_test_cases(
- ("sort_order", 100, lambda sort_order: sort_order == 100),
- ("nullable", False, lambda column: column.nullable is False),
- (
- "active_history",
- True,
- lambda column_property: column_property.active_history is True,
- ),
- (
- "deferred",
- True,
- lambda column_property: column_property.deferred is True,
- ),
- (
- "deferred",
- _NoArg.NO_ARG,
- lambda column_property: column_property is None,
- ),
- (
- "deferred_group",
- "mygroup",
- lambda column_property: column_property.deferred is True
- and column_property.group == "mygroup",
- ),
- (
- "deferred_raiseload",
- True,
- lambda column_property: column_property.deferred is True
- and column_property.raiseload is True,
- ),
- (
- "server_default",
- "25",
- lambda column: column.server_default.arg == "25",
- ),
- (
- "server_onupdate",
- "25",
- lambda column: column.server_onupdate.arg == "25",
- ),
- (
- "default",
- 25,
- lambda column: column.default.arg == 25,
- ),
- (
- "insert_default",
- 25,
- lambda column: column.default.arg == 25,
- ),
- (
- "onupdate",
- 25,
- lambda column: column.onupdate.arg == 25,
- ),
- ("doc", "some doc", lambda column: column.doc == "some doc"),
- (
- "comment",
- "some comment",
- lambda column: column.comment == "some comment",
- ),
- ("index", True, lambda column: column.index is True),
- ("index", _NoArg.NO_ARG, lambda column: column.index is None),
- ("index", False, lambda column: column.index is False),
- ("unique", True, lambda column: column.unique is True),
- ("unique", False, lambda column: column.unique is False),
- ("autoincrement", True, lambda column: column.autoincrement is True),
- ("system", True, lambda column: column.system is True),
- ("primary_key", True, lambda column: column.primary_key is True),
- ("type_", BIGINT, lambda column: isinstance(column.type, BIGINT)),
- ("info", {"foo": "bar"}, lambda column: column.info == {"foo": "bar"}),
- (
- "use_existing_column",
- True,
- lambda mc: mc._use_existing_column is True,
- ),
- (
- "quote",
- True,
- exc.SADeprecationWarning(
- "Can't use the 'key' or 'name' arguments in Annotated "
- ),
- ),
- (
- "key",
- "mykey",
- exc.SADeprecationWarning(
- "Can't use the 'key' or 'name' arguments in Annotated "
- ),
- ),
- (
- "name",
- "mykey",
- exc.SADeprecationWarning(
- "Can't use the 'key' or 'name' arguments in Annotated "
- ),
- ),
- (
- "kw_only",
- True,
- exc.SADeprecationWarning(
- "Argument 'kw_only' is a dataclass argument "
- ),
- testing.requires.python310,
- ),
- (
- "compare",
- True,
- exc.SADeprecationWarning(
- "Argument 'compare' is a dataclass argument "
- ),
- testing.requires.python310,
- ),
- (
- "default_factory",
- lambda: 25,
- exc.SADeprecationWarning(
- "Argument 'default_factory' is a dataclass argument "
- ),
- ),
- (
- "repr",
- True,
- exc.SADeprecationWarning(
- "Argument 'repr' is a dataclass argument "
- ),
- ),
- (
- "init",
- True,
- exc.SADeprecationWarning(
- "Argument 'init' is a dataclass argument"
- ),
- ),
- (
- "hash",
- True,
- exc.SADeprecationWarning(
- "Argument 'hash' is a dataclass argument"
- ),
- ),
- (
- "dataclass_metadata",
- {},
- exc.SADeprecationWarning(
- "Argument 'dataclass_metadata' is a dataclass argument"
- ),
- ),
- argnames="argname, argument, assertion",
- )
- @testing.variation("use_annotated", [True, False, "control"])
- def test_names_encountered_for_annotated(
- self, argname, argument, assertion, use_annotated, decl_base
- ):
- global myint
+ def test_construct_works_in_expr(self, decl_base):
+ class User(decl_base):
+ __tablename__ = "users"
- if argument is not _NoArg.NO_ARG:
- kw = {argname: argument}
+ id: Mapped[int] = mapped_column(primary_key=True)
- if argname == "quote":
- kw["name"] = "somename"
- else:
- kw = {}
+ class Address(decl_base):
+ __tablename__ = "addresses"
- is_warning = isinstance(assertion, exc.SADeprecationWarning)
- is_dataclass = argname in (
- "kw_only",
- "init",
- "repr",
- "compare",
- "default_factory",
- "hash",
- "dataclass_metadata",
- )
+ id: Mapped[int] = mapped_column(primary_key=True)
+ user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
- if is_dataclass:
+ user = relationship(User, primaryjoin=user_id == User.id)
- class Base(MappedAsDataclass, decl_base):
- __abstract__ = True
+ self.assert_compile(
+ select(Address.user_id, User.id).join(Address.user),
+ "SELECT addresses.user_id, users.id FROM addresses "
+ "JOIN users ON addresses.user_id = users.id",
+ )
- else:
- Base = decl_base
+ def test_construct_works_as_polymorphic_on(self, decl_base):
+ class User(decl_base):
+ __tablename__ = "users"
- if use_annotated.control:
- # test in reverse; that kw set on the main mapped_column() takes
- # effect when the Annotated is there also and does not have the
- # kw
- amc = mapped_column()
- myint = Annotated[int, amc]
+ id: Mapped[int] = mapped_column(primary_key=True)
+ type: Mapped[str] = mapped_column()
- mc = mapped_column(**kw)
+ __mapper_args__ = {"polymorphic_on": type}
- class User(Base):
- __tablename__ = "user"
- id: Mapped[int] = mapped_column(primary_key=True)
- myname: Mapped[myint] = mc
+ decl_base.registry.configure()
+ is_(User.__table__.c.type, User.__mapper__.polymorphic_on)
- elif use_annotated:
- amc = mapped_column(**kw)
- myint = Annotated[int, amc]
+ def test_construct_works_as_version_id_col(self, decl_base):
+ class User(decl_base):
+ __tablename__ = "users"
- mc = mapped_column()
+ id: Mapped[int] = mapped_column(primary_key=True)
+ version_id: Mapped[int] = mapped_column()
- if is_warning:
- with expect_deprecated(assertion.args[0]):
+ __mapper_args__ = {"version_id_col": version_id}
- class User(Base):
- __tablename__ = "user"
- id: Mapped[int] = mapped_column(primary_key=True)
- myname: Mapped[myint] = mc
+ decl_base.registry.configure()
+ is_(User.__table__.c.version_id, User.__mapper__.version_id_col)
- else:
+ def test_construct_works_in_deferred(self, decl_base):
+ class User(decl_base):
+ __tablename__ = "users"
- class User(Base):
- __tablename__ = "user"
- id: Mapped[int] = mapped_column(primary_key=True)
- myname: Mapped[myint] = mc
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[str] = deferred(mapped_column())
- else:
- mc = cast(MappedColumn, mapped_column(**kw))
+ self.assert_compile(select(User), "SELECT users.id FROM users")
+ self.assert_compile(
+ select(User).options(undefer(User.data)),
+ "SELECT users.id, users.data FROM users",
+ )
- mapper_prop = mc.mapper_property_to_assign
- column_to_assign, sort_order = mc.columns_to_assign[0]
+ def test_deferred_kw(self, decl_base):
+ class User(decl_base):
+ __tablename__ = "users"
- if not is_warning:
- assert_result = testing.resolve_lambda(
- assertion,
- sort_order=sort_order,
- column_property=mapper_prop,
- column=column_to_assign,
- mc=mc,
- )
- assert assert_result
- elif is_dataclass and (not use_annotated or use_annotated.control):
- eq_(
- getattr(mc._attribute_options, f"dataclasses_{argname}"),
- argument,
- )
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[str] = mapped_column(deferred=True)
- @testing.combinations(("index",), ("unique",), argnames="paramname")
- @testing.combinations((True,), (False,), (None,), argnames="orig")
- @testing.combinations((True,), (False,), (None,), argnames="merging")
- def test_index_unique_combinations(
- self, paramname, orig, merging, decl_base
- ):
- """test #11091"""
+ self.assert_compile(select(User), "SELECT users.id FROM users")
+ self.assert_compile(
+ select(User).options(undefer(User.data)),
+ "SELECT users.id, users.data FROM users",
+ )
- global myint
- amc = mapped_column(**{paramname: merging})
- myint = Annotated[int, amc]
+class Pep593InterpretationTests(fixtures.TestBase, testing.AssertsCompiledSQL):
+ __dialect__ = "default"
- mc = mapped_column(**{paramname: orig})
+ def test_extract_from_pep593(self, decl_base):
+ global Address
+
+ @dataclasses.dataclass
+ class Address:
+ street: str
+ state: str
+ zip_: str
class User(decl_base):
__tablename__ = "user"
+
id: Mapped[int] = mapped_column(primary_key=True)
- myname: Mapped[myint] = mc
+ name: Mapped[str] = mapped_column()
- result = getattr(User.__table__.c.myname, paramname)
- if orig is None:
- is_(result, merging)
- else:
- is_(result, orig)
+ address: Mapped[Annotated[Address, "foo"]] = composite(
+ mapped_column(), mapped_column(), mapped_column("zip")
+ )
- def test_pep484_newtypes_as_typemap_keys(
+ self.assert_compile(
+ select(User),
+ 'SELECT "user".id, "user".name, "user".street, '
+ '"user".state, "user".zip FROM "user"',
+ dialect="default",
+ )
+
+ def test_pep593_types_as_typemap_keys(
self, decl_base: Type[DeclarativeBase]
):
- global str50, str30, str3050
+ """neat!!!"""
+ global str50, str30, opt_str50, opt_str30
- str50 = NewType("str50", str)
- str30 = NewType("str30", str)
- str3050 = NewType("str30", str50)
+ str50 = Annotated[str, 50]
+ str30 = Annotated[str, 30]
+ opt_str50 = Optional[str50]
+ opt_str30 = Optional[str30]
decl_base.registry.update_type_annotation_map(
- {str50: String(50), str30: String(30), str3050: String(150)}
+ {str50: String(50), str30: String(30)}
)
class MyClass(decl_base):
id: Mapped[str50] = mapped_column(primary_key=True)
data_one: Mapped[str30]
- data_two: Mapped[str50]
- data_three: Mapped[Optional[str30]]
- data_four: Mapped[str3050]
+ data_two: Mapped[opt_str30]
+ data_three: Mapped[str50]
+ data_four: Mapped[opt_str50]
+ data_five: Mapped[str]
+ data_six: Mapped[Optional[str]]
eq_(MyClass.__table__.c.data_one.type.length, 30)
is_false(MyClass.__table__.c.data_one.nullable)
+ eq_(MyClass.__table__.c.data_two.type.length, 30)
+ is_true(MyClass.__table__.c.data_two.nullable)
+ eq_(MyClass.__table__.c.data_three.type.length, 50)
- eq_(MyClass.__table__.c.data_two.type.length, 50)
- is_false(MyClass.__table__.c.data_two.nullable)
+ @testing.variation(
+ "alias_type",
+ [
+ "none",
+ "typekeyword",
+ "typekeyword_unpopulated",
+ "typealias",
+ "typekeyword_nested",
+ ],
+ )
+ @testing.requires.python312
+ def test_extract_pep593_from_pep695(
+ self, decl_base: Type[DeclarativeBase], alias_type
+ ):
+ """test #11130"""
+ if alias_type.typekeyword:
+ decl_base.registry.update_type_annotation_map(
+ {strtypalias_keyword: VARCHAR(33)} # noqa: F821
+ )
+ if alias_type.typekeyword_nested:
+ decl_base.registry.update_type_annotation_map(
+ {strtypalias_keyword_nested: VARCHAR(42)} # noqa: F821
+ )
- eq_(MyClass.__table__.c.data_three.type.length, 30)
- is_true(MyClass.__table__.c.data_three.nullable)
+ class MyClass(decl_base):
+ __tablename__ = "my_table"
- eq_(MyClass.__table__.c.data_four.type.length, 150)
- is_false(MyClass.__table__.c.data_four.nullable)
+ id: Mapped[int] = mapped_column(primary_key=True)
- def test_newtype_missing_from_map(self, decl_base):
- global str50
+ if alias_type.typekeyword or alias_type.typekeyword_unpopulated:
+ data_one: Mapped[strtypalias_keyword] # noqa: F821
+ elif alias_type.typealias:
+ data_one: Mapped[strtypalias_ta] # noqa: F821
+ elif alias_type.none:
+ data_one: Mapped[strtypalias_plain] # noqa: F821
+ elif alias_type.typekeyword_nested:
+ data_one: Mapped[strtypalias_keyword_nested] # noqa: F821
+ else:
+ alias_type.fail()
- str50 = NewType("str50", str)
+ table = MyClass.__table__
+ assert table is not None
- if compat.py310:
- text = ".*str50"
+ if alias_type.typekeyword_nested:
+ # a nested annotation is not supported
+ eq_(MyClass.data_one.expression.info, {})
else:
- # NewTypes before 3.10 had a very bad repr
- # <function NewType.<locals>.new_type at 0x...>
- text = ".*NewType.*"
+ eq_(MyClass.data_one.expression.info, {"hi": "there"})
- with expect_deprecated(
- f"Matching the provided NewType '{text}' on its "
- "resolved value without matching it in the "
- "type_annotation_map is deprecated; add this type to the "
- "type_annotation_map to allow it to match explicitly.",
+ if alias_type.typekeyword:
+ eq_(MyClass.data_one.type.length, 33)
+ elif alias_type.typekeyword_nested:
+ eq_(MyClass.data_one.type.length, 42)
+ else:
+ eq_(MyClass.data_one.type.length, None)
+
+ @testing.requires.python312
+ def test_no_recursive_pep593_from_pep695(
+ self, decl_base: Type[DeclarativeBase]
+ ):
+ def declare():
+ class MyClass(decl_base):
+ __tablename__ = "my_table"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+
+ data_one: Mapped[_RecursivePep695Pep593] # noqa: F821
+
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ r"Could not locate SQLAlchemy Core type when resolving for Python "
+ r"type "
+ r"indicated by '_RecursivePep695Pep593' inside the Mapped\[\] "
+ r"annotation for the 'data_one' attribute; none of "
+ r"'_RecursivePep695Pep593', "
+ r"'typing.Annotated\[_TypingStrPep695, .*\]', '_TypingStrPep695' "
+ r"are resolvable by the registry",
):
+ declare()
+
+ @testing.variation("in_map", [True, False])
+ @testing.variation("alias_type", ["plain", "pep695"])
+ @testing.requires.python312
+ def test_generic_typealias_pep593(
+ self, decl_base: Type[DeclarativeBase], alias_type: Variation, in_map
+ ):
+
+ if in_map:
+ decl_base.registry.update_type_annotation_map(
+ {
+ _GenericPep593TypeAlias[str]: VARCHAR(33),
+ _GenericPep593Pep695[str]: VARCHAR(33),
+ }
+ )
- class MyClass(decl_base):
- __tablename__ = "my_table"
+ class MyClass(decl_base):
+ __tablename__ = "my_table"
- id: Mapped[int] = mapped_column(primary_key=True)
- data_one: Mapped[str50]
+ id: Mapped[int] = mapped_column(primary_key=True)
- is_true(isinstance(MyClass.data_one.type, String))
+ if alias_type.plain:
+ data_one: Mapped[_GenericPep593TypeAlias[str]] # noqa: F821
+ elif alias_type.pep695:
+ data_one: Mapped[_GenericPep593Pep695[str]] # noqa: F821
+ else:
+ alias_type.fail()
+
+ eq_(MyClass.data_one.expression.info, {"hi": "there"})
+ if in_map:
+ eq_(MyClass.data_one.expression.type.length, 33)
+ else:
+ eq_(MyClass.data_one.expression.type.length, None)
def test_extract_base_type_from_pep593(
self, decl_base: Type[DeclarativeBase]
eq_(A_1.label.property.columns[0].table, A.__table__)
eq_(A_2.label.property.columns[0].table, A.__table__)
- @testing.variation(
- "union",
- [
- "union",
- ("pep604", requires.python310),
- "union_null",
- ("pep604_null", requires.python310),
- ],
- )
- def test_unions(self, union):
- global UnionType
- our_type = Numeric(10, 2)
-
- if union.union:
- UnionType = Union[float, Decimal]
- elif union.union_null:
- UnionType = Union[float, Decimal, None]
- elif union.pep604:
- UnionType = float | Decimal
- elif union.pep604_null:
- UnionType = float | Decimal | None
- else:
- union.fail()
- class Base(DeclarativeBase):
- type_annotation_map = {UnionType: our_type}
+class TypeResolutionTests(fixtures.TestBase, testing.AssertsCompiledSQL):
+ __dialect__ = "default"
- class User(Base):
- __tablename__ = "users"
+ @testing.combinations(
+ (str, types.String),
+ (Decimal, types.Numeric),
+ (float, types.Float),
+ (datetime.datetime, types.DateTime),
+ (uuid.UUID, types.Uuid),
+ argnames="pytype_arg,sqltype",
+ )
+ def test_datatype_lookups(self, decl_base, pytype_arg, sqltype):
+ global pytype
+ pytype = pytype_arg
+ class MyClass(decl_base):
+ __tablename__ = "mytable"
id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[Union[float, Decimal]]
- reverse_data: Mapped[Union[Decimal, float]]
-
- optional_data: Mapped[Optional[Union[float, Decimal]]] = (
- mapped_column()
- )
-
- # use Optional directly
- reverse_optional_data: Mapped[Optional[Union[Decimal, float]]] = (
- mapped_column()
- )
-
- # use Union with None, same as Optional but presents differently
- # (Optional object with __origin__ Union vs. Union)
- reverse_u_optional_data: Mapped[Union[Decimal, float, None]] = (
- mapped_column()
- )
-
- refer_union: Mapped[UnionType]
- refer_union_optional: Mapped[Optional[UnionType]]
-
- # py38, 37 does not automatically flatten unions, add extra tests
- # for this. maintain these in order to catch future regressions
- # in the behavior of ``Union``
- unflat_union_optional_data: Mapped[
- Union[Union[Decimal, float, None], None]
- ] = mapped_column()
+ data: Mapped[pytype]
- float_data: Mapped[float] = mapped_column()
- decimal_data: Mapped[Decimal] = mapped_column()
+ assert isinstance(MyClass.__table__.c.data.type, sqltype)
- if compat.py310:
- pep604_data: Mapped[float | Decimal] = mapped_column()
- pep604_reverse: Mapped[Decimal | float] = mapped_column()
- pep604_optional: Mapped[Decimal | float | None] = (
- mapped_column()
- )
- pep604_data_fwd: Mapped["float | Decimal"] = mapped_column()
- pep604_reverse_fwd: Mapped["Decimal | float"] = mapped_column()
- pep604_optional_fwd: Mapped["Decimal | float | None"] = (
- mapped_column()
- )
+ @testing.combinations(
+ (BIGINT(),),
+ (BIGINT,),
+ (Integer().with_variant(BIGINT, "default")),
+ (Integer().with_variant(BIGINT(), "default")),
+ (BIGINT().with_variant(String(), "some_other_dialect")),
+ )
+ def test_type_map_varieties(self, typ):
+ Base = declarative_base(type_annotation_map={int: typ})
- info = [
- ("data", False),
- ("reverse_data", False),
- ("optional_data", True),
- ("reverse_optional_data", True),
- ("reverse_u_optional_data", True),
- ("refer_union", "null" in union.name),
- ("refer_union_optional", True),
- ("unflat_union_optional_data", True),
- ]
- if compat.py310:
- info += [
- ("pep604_data", False),
- ("pep604_reverse", False),
- ("pep604_optional", True),
- ("pep604_data_fwd", False),
- ("pep604_reverse_fwd", False),
- ("pep604_optional_fwd", True),
- ]
+ class MyClass(Base):
+ __tablename__ = "mytable"
- for name, nullable in info:
- col = User.__table__.c[name]
- is_(col.type, our_type, name)
- is_(col.nullable, nullable, name)
+ id: Mapped[int] = mapped_column(primary_key=True)
+ x: Mapped[int]
+ y: Mapped[int] = mapped_column()
+ z: Mapped[int] = mapped_column(typ)
- is_true(isinstance(User.__table__.c.float_data.type, Float))
- ne_(User.__table__.c.float_data.type, our_type)
+ self.assert_compile(
+ CreateTable(MyClass.__table__),
+ "CREATE TABLE mytable (id BIGINT NOT NULL, "
+ "x BIGINT NOT NULL, y BIGINT NOT NULL, z BIGINT NOT NULL, "
+ "PRIMARY KEY (id))",
+ )
- is_true(isinstance(User.__table__.c.decimal_data.type, Numeric))
- ne_(User.__table__.c.decimal_data.type, our_type)
+ def test_dont_ignore_unresolvable(self, decl_base):
+ """test #8888"""
- @testing.variation(
- "union",
- [
- "union",
- ("pep604", requires.python310),
- ("pep695", requires.python312),
- ],
- )
- def test_optional_in_annotation_map(self, union):
- """See issue #11370"""
+ with expect_raises_message(
+ sa_exc.ArgumentError,
+ r"Could not resolve all types within mapped annotation: "
+ r"\".*Mapped\[.*fake.*\]\". Ensure all types are written "
+ r"correctly and are imported within the module in use.",
+ ):
- class Base(DeclarativeBase):
- if union.union:
- type_annotation_map = {_Json: JSON}
- elif union.pep604:
- type_annotation_map = {_JsonPep604: JSON}
- elif union.pep695:
- type_annotation_map = {_JsonPep695: JSON} # noqa: F821
- else:
- union.fail()
+ class A(decl_base):
+ __tablename__ = "a"
- class A(Base):
- __tablename__ = "a"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped["fake"] # noqa
- id: Mapped[int] = mapped_column(primary_key=True)
- if union.union:
- json1: Mapped[_Json]
- json2: Mapped[_Json] = mapped_column(nullable=False)
- elif union.pep604:
- json1: Mapped[_JsonPep604]
- json2: Mapped[_JsonPep604] = mapped_column(nullable=False)
- elif union.pep695:
- json1: Mapped[_JsonPep695] # noqa: F821
- json2: Mapped[_JsonPep695] = mapped_column( # noqa: F821
- nullable=False
- )
- else:
- union.fail()
+ def test_type_dont_mis_resolve_on_superclass(self):
+ """test for #8859.
- is_(A.__table__.c.json1.type._type_affinity, JSON)
- is_(A.__table__.c.json2.type._type_affinity, JSON)
- is_true(A.__table__.c.json1.nullable)
- is_false(A.__table__.c.json2.nullable)
+ For subclasses of a type that's in the map, don't resolve this
+ by default, even though we do a search through __mro__.
- @testing.variation(
- "option",
- [
- "not_optional",
- "optional",
- "optional_fwd_ref",
- "union_none",
- ("pep604", testing.requires.python310),
- ("pep604_fwd_ref", testing.requires.python310),
- ],
- )
- @testing.variation("brackets", ["oneset", "twosets"])
- @testing.combinations(
- "include_mc_type", "derive_from_anno", argnames="include_mc_type"
- )
- def test_optional_styles_nested_brackets(
- self, option, brackets, include_mc_type
- ):
- """composed types test, includes tests that were added later for
- #12207"""
+ """
+ global int_sub
- class Base(DeclarativeBase):
- if testing.requires.python310.enabled:
- type_annotation_map = {
- Dict[str, Decimal]: JSON,
- dict[str, Decimal]: JSON,
- Union[List[int], List[str]]: JSON,
- list[int] | list[str]: JSON,
- }
- else:
- type_annotation_map = {
- Dict[str, Decimal]: JSON,
- Union[List[int], List[str]]: JSON,
- }
+ class int_sub(int):
+ pass
- if include_mc_type == "include_mc_type":
- mc = mapped_column(JSON)
- mc2 = mapped_column(JSON)
- else:
- mc = mapped_column()
- mc2 = mapped_column()
+ Base = declarative_base(
+ type_annotation_map={
+ int: Integer,
+ }
+ )
- class A(Base):
- __tablename__ = "a"
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ "Could not locate SQLAlchemy Core type",
+ ):
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[str] = mapped_column()
+ class MyClass(Base):
+ __tablename__ = "mytable"
- if brackets.oneset:
- if option.not_optional:
- json: Mapped[Dict[str, Decimal]] = mapped_column() # type: ignore # noqa: E501
- if testing.requires.python310.enabled:
- json2: Mapped[dict[str, Decimal]] = mapped_column() # type: ignore # noqa: E501
- elif option.optional:
- json: Mapped[Optional[Dict[str, Decimal]]] = mc
- if testing.requires.python310.enabled:
- json2: Mapped[Optional[dict[str, Decimal]]] = mc2
- elif option.optional_fwd_ref:
- json: Mapped["Optional[Dict[str, Decimal]]"] = mc
- if testing.requires.python310.enabled:
- json2: Mapped["Optional[dict[str, Decimal]]"] = mc2
- elif option.union_none:
- json: Mapped[Union[Dict[str, Decimal], None]] = mc
- json2: Mapped[Union[None, Dict[str, Decimal]]] = mc2
- elif option.pep604:
- json: Mapped[dict[str, Decimal] | None] = mc
- if testing.requires.python310.enabled:
- json2: Mapped[None | dict[str, Decimal]] = mc2
- elif option.pep604_fwd_ref:
- json: Mapped["dict[str, Decimal] | None"] = mc
- if testing.requires.python310.enabled:
- json2: Mapped["None | dict[str, Decimal]"] = mc2
- elif brackets.twosets:
- if option.not_optional:
- json: Mapped[Union[List[int], List[str]]] = mapped_column() # type: ignore # noqa: E501
- elif option.optional:
- json: Mapped[Optional[Union[List[int], List[str]]]] = mc
- if testing.requires.python310.enabled:
- json2: Mapped[
- Optional[Union[list[int], list[str]]]
- ] = mc2
- elif option.optional_fwd_ref:
- json: Mapped["Optional[Union[List[int], List[str]]]"] = mc
- if testing.requires.python310.enabled:
- json2: Mapped[
- "Optional[Union[list[int], list[str]]]"
- ] = mc2
- elif option.union_none:
- json: Mapped[Union[List[int], List[str], None]] = mc
- if testing.requires.python310.enabled:
- json2: Mapped[Union[None, list[int], list[str]]] = mc2
- elif option.pep604:
- json: Mapped[list[int] | list[str] | None] = mc
- json2: Mapped[None | list[int] | list[str]] = mc2
- elif option.pep604_fwd_ref:
- json: Mapped["list[int] | list[str] | None"] = mc
- json2: Mapped["None | list[int] | list[str]"] = mc2
- else:
- brackets.fail()
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[int_sub]
- is_(A.__table__.c.json.type._type_affinity, JSON)
- if hasattr(A, "json2"):
- is_(A.__table__.c.json2.type._type_affinity, JSON)
- if option.not_optional:
- is_false(A.__table__.c.json2.nullable)
- else:
- is_true(A.__table__.c.json2.nullable)
+ @testing.variation("in_map", ["yes", "no", "value"])
+ @testing.variation("lookup", ["A", "B", "value"])
+ def test_recursive_pep695_cases(
+ self, decl_base, in_map: Variation, lookup: Variation
+ ):
+ global A, B
+ A = TypingTypeAliasType("A", Union[int, float])
+ B = TypingTypeAliasType("B", A)
- if option.not_optional:
- is_false(A.__table__.c.json.nullable)
- else:
- is_true(A.__table__.c.json.nullable)
+ if in_map.yes:
+ decl_base.registry.update_type_annotation_map({A: Numeric(10, 5)})
+ elif in_map.value:
+ decl_base.registry.update_type_annotation_map(
+ {A.__value__: Numeric(10, 5)}
+ )
- @testing.variation("optional", [True, False])
- @testing.variation("provide_type", [True, False])
- @testing.variation("add_to_type_map", [True, False])
- def test_recursive_type(
- self, decl_base, optional, provide_type, add_to_type_map
- ):
- """test #9553"""
+ def declare():
+ class MyClass(decl_base):
+ __tablename__ = "my_table"
+ id: Mapped[int] = mapped_column(primary_key=True)
- global T
+ if lookup.A:
+ data: Mapped[A]
+ elif lookup.B:
+ data: Mapped[B]
+ elif lookup.value:
+ data: Mapped[Union[int, float]]
+ else:
+ lookup.fail()
- T = Dict[str, Optional["T"]]
+ return MyClass
- if not provide_type and not add_to_type_map:
+ if in_map.value and lookup.B:
+ with expect_deprecated(
+ "Matching to pep-695 type 'A' in a recursive fashion"
+ ):
+ MyClass = declare()
+ eq_(MyClass.data.expression.type.precision, 10)
+ elif in_map.no or (in_map.yes and lookup.value):
with expect_raises_message(
- sa_exc.ArgumentError,
- r"Could not locate SQLAlchemy.*" r".*ForwardRef\('T'\).*",
+ orm_exc.MappedAnnotationError,
+ "Could not locate SQLAlchemy Core type when resolving "
+ "for Python type indicated by",
):
+ declare()
+ else:
+ MyClass = declare()
+ eq_(MyClass.data.expression.type.precision, 10)
- class TypeTest(decl_base):
- __tablename__ = "my_table"
+ @testing.variation(
+ "dict_key", ["typing", ("plain", testing.requires.python310)]
+ )
+ def test_type_dont_mis_resolve_on_non_generic(self, dict_key):
+ """test for #8859.
- id: Mapped[int] = mapped_column(primary_key=True)
- if optional:
- type_test: Mapped[Optional[T]] = mapped_column()
- else:
- type_test: Mapped[T] = mapped_column()
+ For a specific generic type with arguments, don't do any MRO
+ lookup.
- return
+ """
- else:
- if add_to_type_map:
- decl_base.registry.update_type_annotation_map({T: JSON()})
+ Base = declarative_base(
+ type_annotation_map={
+ dict: String,
+ }
+ )
- class TypeTest(decl_base):
- __tablename__ = "my_table"
+ with expect_raises_message(
+ sa_exc.ArgumentError, "Could not locate SQLAlchemy Core type"
+ ):
+
+ class MyClass(Base):
+ __tablename__ = "mytable"
id: Mapped[int] = mapped_column(primary_key=True)
- if add_to_type_map:
- if optional:
- type_test: Mapped[Optional[T]] = mapped_column()
- else:
- type_test: Mapped[T] = mapped_column()
- else:
- if optional:
- type_test: Mapped[Optional[T]] = mapped_column(JSON())
- else:
- type_test: Mapped[T] = mapped_column(JSON())
+ if dict_key.plain:
+ data: Mapped[dict[str, str]]
+ elif dict_key.typing:
+ data: Mapped[Dict[str, str]]
- if optional:
- is_(TypeTest.__table__.c.type_test.nullable, True)
- else:
- is_(TypeTest.__table__.c.type_test.nullable, False)
+ def test_type_secondary_resolution(self):
+ class MyString(String):
+ def _resolve_for_python_type(
+ self, python_type, matched_type, matched_on_flattened
+ ):
+ return String(length=42)
- self.assert_compile(
- select(TypeTest),
- "SELECT my_table.id, my_table.type_test FROM my_table",
- )
+ Base = declarative_base(type_annotation_map={str: MyString})
- def test_missing_mapped_lhs(self, decl_base):
- with expect_annotation_syntax_error("User.name"):
+ class MyClass(Base):
+ __tablename__ = "mytable"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[str]
+
+ is_true(isinstance(MyClass.__table__.c.data.type, String))
+ eq_(MyClass.__table__.c.data.type.length, 42)
+
+ def test_construct_lhs_type_missing(self, decl_base):
+ global MyClass
+
+ class MyClass:
+ pass
+
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ "Could not locate SQLAlchemy Core type when resolving for Python "
+ r"type indicated by '.*class .*MyClass.*' inside the "
+ r"Mapped\[\] annotation for the 'data' attribute; the type "
+ "object is not resolvable by the registry",
+ ):
class User(decl_base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
- name: str = mapped_column() # type: ignore
+ data: Mapped[MyClass] = mapped_column()
- def test_construct_lhs_separate_name(self, decl_base):
- class User(decl_base):
- __tablename__ = "users"
+ @testing.variation(
+ "argtype",
+ [
+ "type",
+ ("column", testing.requires.python310),
+ ("mapped_column", testing.requires.python310),
+ "column_class",
+ "ref_to_type",
+ ("ref_to_column", testing.requires.python310),
+ ],
+ )
+ def test_construct_lhs_sqlalchemy_type(self, decl_base, argtype):
+ """test for #12329.
- id: Mapped[int] = mapped_column(primary_key=True)
- name: Mapped[str] = mapped_column()
- data: Mapped[Optional[str]] = mapped_column("the_data")
+ of note here are all the different messages we have for when the
+ wrong thing is put into Mapped[], and in fact in #12329 we added
+ another one.
- self.assert_compile(
- select(User.data), "SELECT users.the_data FROM users"
- )
- is_true(User.__table__.c.the_data.nullable)
+ This is a lot of different messages, but at the same time they
+ occur at different places in the interpretation of types. If
+ we were to centralize all these messages, we'd still likely end up
+ doing distinct messages for each scenario, so instead we added
+ a new ArgumentError subclass MappedAnnotationError that provides
+ some commonality to all of these cases.
- def test_construct_works_in_expr(self, decl_base):
- class User(decl_base):
- __tablename__ = "users"
- id: Mapped[int] = mapped_column(primary_key=True)
+ """
+ expect_future_annotations = "annotations" in globals()
+
+ if argtype.type:
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ # properties.py -> _init_column_for_annotation, type is
+ # a SQL type
+ "The type provided inside the 'data' attribute Mapped "
+ "annotation is the SQLAlchemy type .*BigInteger.*. Expected "
+ "a Python type instead",
+ ):
+
+ class User(decl_base):
+ __tablename__ = "users"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[BigInteger] = mapped_column()
+
+ elif argtype.column:
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ # util.py -> _extract_mapped_subtype
+ (
+ re.escape(
+ "Could not interpret annotation "
+ "Mapped[Column('q', BigInteger)]."
+ )
+ if expect_future_annotations
+ # properties.py -> _init_column_for_annotation, object is
+ # not a SQL type or a python type, it's just some object
+ else re.escape(
+ "The object provided inside the 'data' attribute "
+ "Mapped annotation is not a Python type, it's the "
+ "object Column('q', BigInteger(), table=None). "
+ "Expected a Python type."
+ )
+ ),
+ ):
+
+ class User(decl_base):
+ __tablename__ = "users"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[Column("q", BigInteger)] = ( # noqa: F821
+ mapped_column()
+ )
- class Address(decl_base):
- __tablename__ = "addresses"
+ elif argtype.mapped_column:
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ # properties.py -> _init_column_for_annotation, object is
+ # not a SQL type or a python type, it's just some object
+ # interestingly, this raises at the same point for both
+ # future annotations mode and legacy annotations mode
+ r"The object provided inside the 'data' attribute "
+ "Mapped annotation is not a Python type, it's the object "
+ r"\<sqlalchemy.orm.properties.MappedColumn.*\>. "
+ "Expected a Python type.",
+ ):
- id: Mapped[int] = mapped_column(primary_key=True)
- user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
+ class User(decl_base):
+ __tablename__ = "users"
- user = relationship(User, primaryjoin=user_id == User.id)
+ id: Mapped[int] = mapped_column(primary_key=True)
+ big_integer: Mapped[int] = mapped_column()
+ data: Mapped[big_integer] = mapped_column()
- self.assert_compile(
- select(Address.user_id, User.id).join(Address.user),
- "SELECT addresses.user_id, users.id FROM addresses "
- "JOIN users ON addresses.user_id = users.id",
- )
+ elif argtype.column_class:
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ # properties.py -> _init_column_for_annotation, type is not
+ # a SQL type
+ "Could not locate SQLAlchemy Core type when resolving for "
+ "Python type indicated by "
+ r"'.*class .*.Column.*' inside the "
+ r"Mapped\[\] annotation for the 'data' attribute; the "
+ "type object is not resolvable by the registry",
+ ):
- def test_construct_works_as_polymorphic_on(self, decl_base):
- class User(decl_base):
- __tablename__ = "users"
+ class User(decl_base):
+ __tablename__ = "users"
- id: Mapped[int] = mapped_column(primary_key=True)
- type: Mapped[str] = mapped_column()
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[Column] = mapped_column()
- __mapper_args__ = {"polymorphic_on": type}
+ elif argtype.ref_to_type:
+ mytype = BigInteger
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ (
+ # decl_base.py -> _exract_mappable_attributes
+ re.escape(
+ "Could not resolve all types within mapped "
+ 'annotation: "Mapped[mytype]"'
+ )
+ if expect_future_annotations
+ # properties.py -> _init_column_for_annotation, type is
+ # a SQL type
+ else re.escape(
+ "The type provided inside the 'data' attribute Mapped "
+ "annotation is the SQLAlchemy type "
+ "<class 'sqlalchemy.sql.sqltypes.BigInteger'>. "
+ "Expected a Python type instead"
+ )
+ ),
+ ):
- decl_base.registry.configure()
- is_(User.__table__.c.type, User.__mapper__.polymorphic_on)
+ class User(decl_base):
+ __tablename__ = "users"
- def test_construct_works_as_version_id_col(self, decl_base):
- class User(decl_base):
- __tablename__ = "users"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[mytype] = mapped_column()
- id: Mapped[int] = mapped_column(primary_key=True)
- version_id: Mapped[int] = mapped_column()
+ elif argtype.ref_to_column:
+ mycol = Column("q", BigInteger)
- __mapper_args__ = {"version_id_col": version_id}
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ # decl_base.py -> _exract_mappable_attributes
+ (
+ re.escape(
+ "Could not resolve all types within mapped "
+ 'annotation: "Mapped[mycol]"'
+ )
+ if expect_future_annotations
+ else
+ # properties.py -> _init_column_for_annotation, object is
+ # not a SQL type or a python type, it's just some object
+ re.escape(
+ "The object provided inside the 'data' attribute "
+ "Mapped "
+ "annotation is not a Python type, it's the object "
+ "Column('q', BigInteger(), table=None). "
+ "Expected a Python type."
+ )
+ ),
+ ):
- decl_base.registry.configure()
- is_(User.__table__.c.version_id, User.__mapper__.version_id_col)
+ class User(decl_base):
+ __tablename__ = "users"
- def test_construct_works_in_deferred(self, decl_base):
- class User(decl_base):
- __tablename__ = "users"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[mycol] = mapped_column()
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[str] = deferred(mapped_column())
+ else:
+ argtype.fail()
- self.assert_compile(select(User), "SELECT users.id FROM users")
- self.assert_compile(
- select(User).options(undefer(User.data)),
- "SELECT users.id, users.data FROM users",
+ def test_plain_typealias_as_typemap_keys(
+ self, decl_base: Type[DeclarativeBase]
+ ):
+ decl_base.registry.update_type_annotation_map(
+ {_UnionTypeAlias: JSON, _StrTypeAlias: String(30)}
)
- def test_deferred_kw(self, decl_base):
- class User(decl_base):
- __tablename__ = "users"
-
+ class Test(decl_base):
+ __tablename__ = "test"
id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[str] = mapped_column(deferred=True)
+ data: Mapped[_StrTypeAlias]
+ structure: Mapped[_UnionTypeAlias]
- self.assert_compile(select(User), "SELECT users.id FROM users")
- self.assert_compile(
- select(User).options(undefer(User.data)),
- "SELECT users.id, users.data FROM users",
- )
+ eq_(Test.__table__.c.data.type.length, 30)
+ is_(Test.__table__.c.structure.type._type_affinity, JSON)
- @testing.combinations(
- (str, types.String),
- (Decimal, types.Numeric),
- (float, types.Float),
- (datetime.datetime, types.DateTime),
- (uuid.UUID, types.Uuid),
- argnames="pytype_arg,sqltype",
+ @testing.variation(
+ "option",
+ [
+ "plain",
+ "union",
+ "union_604",
+ "union_null",
+ "union_null_604",
+ "optional",
+ "optional_union",
+ "optional_union_604",
+ "union_newtype",
+ "union_null_newtype",
+ "union_695",
+ "union_null_695",
+ ],
)
- def test_datatype_lookups(self, decl_base, pytype_arg, sqltype):
- global pytype
- pytype = pytype_arg
+ @testing.variation("in_map", ["yes", "no", "value"])
+ @testing.requires.python312
+ def test_pep695_behavior(self, decl_base, in_map, option):
+ """Issue #11955; later issue #12829"""
- class MyClass(decl_base):
- __tablename__ = "mytable"
- id: Mapped[int] = mapped_column(primary_key=True)
+ global tat
- data: Mapped[pytype]
+ if option.plain:
+ tat = TypeAliasType("tat", str)
+ elif option.union:
+ tat = TypeAliasType("tat", Union[str, int])
+ elif option.union_604:
+ tat = TypeAliasType("tat", str | int)
+ elif option.union_null:
+ tat = TypeAliasType("tat", Union[str, int, None])
+ elif option.union_null_604:
+ tat = TypeAliasType("tat", str | int | None)
+ elif option.optional:
+ tat = TypeAliasType("tat", Optional[str])
+ elif option.optional_union:
+ tat = TypeAliasType("tat", Optional[Union[str, int]])
+ elif option.optional_union_604:
+ tat = TypeAliasType("tat", Optional[str | int])
+ elif option.union_newtype:
+ # this seems to be illegal for typing but "works"
+ tat = NewType("tat", Union[str, int])
+ elif option.union_null_newtype:
+ # this seems to be illegal for typing but "works"
+ tat = NewType("tat", Union[str, int, None])
+ elif option.union_695:
+ tat = TypeAliasType("tat", str | int)
+ elif option.union_null_695:
+ tat = TypeAliasType("tat", str | int | None)
+ else:
+ option.fail()
- assert isinstance(MyClass.__table__.c.data.type, sqltype)
+ is_newtype = "newtype" in option.name
+ if in_map.yes:
+ decl_base.registry.update_type_annotation_map({tat: String(99)})
+ elif in_map.value and not is_newtype:
+ decl_base.registry.update_type_annotation_map(
+ {tat.__value__: String(99)}
+ )
- def test_dont_ignore_unresolvable(self, decl_base):
- """test #8888"""
+ def declare():
+ class Test(decl_base):
+ __tablename__ = "test"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[tat]
- with expect_raises_message(
- sa_exc.ArgumentError,
- r"Could not resolve all types within mapped annotation: "
- r"\".*Mapped\[.*fake.*\]\". Ensure all types are written "
- r"correctly and are imported within the module in use.",
- ):
+ return Test.__table__.c.data
+
+ if in_map.yes or (in_map.value and not is_newtype):
+ col = declare()
+ # String(99) inside the type_map
+ is_true(isinstance(col.type, String))
+ eq_(col.type.length, 99)
+ nullable = "null" in option.name or "optional" in option.name
+ eq_(col.nullable, nullable)
+ elif option.plain or option.optional:
+ col = declare()
+ # plain string from default lookup
+ is_true(isinstance(col.type, String))
+ eq_(col.type.length, None)
+ nullable = "null" in option.name or "optional" in option.name
+ eq_(col.nullable, nullable)
+ else:
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ r"Could not locate SQLAlchemy Core type when resolving "
+ r"for Python type "
+ r"indicated by '.*tat' inside the Mapped\[\] "
+ r"annotation for the 'data' attribute;",
+ ):
+ declare()
+ return
+
+ @testing.variation(
+ "type_",
+ [
+ "str_extension",
+ "str_typing",
+ "generic_extension",
+ "generic_typing",
+ "generic_typed_extension",
+ "generic_typed_typing",
+ ],
+ )
+ @testing.requires.python312
+ def test_pep695_typealias_as_typemap_keys(
+ self, decl_base: Type[DeclarativeBase], type_
+ ):
+ """test #10807, #12829"""
+
+ decl_base.registry.update_type_annotation_map(
+ {
+ _UnionPep695: JSON,
+ _StrPep695: String(30),
+ _TypingStrPep695: String(30),
+ _GenericPep695: String(30),
+ _TypingGenericPep695: String(30),
+ _GenericPep695Typed: String(30),
+ _TypingGenericPep695Typed: String(30),
+ }
+ )
- class A(decl_base):
- __tablename__ = "a"
+ class Test(decl_base):
+ __tablename__ = "test"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ if type_.str_extension:
+ data: Mapped[_StrPep695]
+ elif type_.str_typing:
+ data: Mapped[_TypingStrPep695]
+ elif type_.generic_extension:
+ data: Mapped[_GenericPep695]
+ elif type_.generic_typing:
+ data: Mapped[_TypingGenericPep695]
+ elif type_.generic_typed_extension:
+ data: Mapped[_GenericPep695Typed]
+ elif type_.generic_typed_typing:
+ data: Mapped[_TypingGenericPep695Typed]
+ else:
+ type_.fail()
+ structure: Mapped[_UnionPep695]
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped["fake"] # noqa
+ eq_(Test.__table__.c.data.type._type_affinity, String)
+ eq_(Test.__table__.c.data.type.length, 30)
+ is_(Test.__table__.c.structure.type._type_affinity, JSON)
- def test_type_dont_mis_resolve_on_superclass(self):
- """test for #8859.
+ def test_pep484_newtypes_as_typemap_keys(
+ self, decl_base: Type[DeclarativeBase]
+ ):
+ global str50, str30, str3050
- For subclasses of a type that's in the map, don't resolve this
- by default, even though we do a search through __mro__.
+ str50 = NewType("str50", str)
+ str30 = NewType("str30", str)
+ str3050 = NewType("str30", str50)
- """
- global int_sub
+ decl_base.registry.update_type_annotation_map(
+ {str50: String(50), str30: String(30), str3050: String(150)}
+ )
- class int_sub(int):
- pass
+ class MyClass(decl_base):
+ __tablename__ = "my_table"
- Base = declarative_base(
- type_annotation_map={
- int: Integer,
- }
- )
+ id: Mapped[str50] = mapped_column(primary_key=True)
+ data_one: Mapped[str30]
+ data_two: Mapped[str50]
+ data_three: Mapped[Optional[str30]]
+ data_four: Mapped[str3050]
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- "Could not locate SQLAlchemy Core type",
- ):
+ eq_(MyClass.__table__.c.data_one.type.length, 30)
+ is_false(MyClass.__table__.c.data_one.nullable)
- class MyClass(Base):
- __tablename__ = "mytable"
+ eq_(MyClass.__table__.c.data_two.type.length, 50)
+ is_false(MyClass.__table__.c.data_two.nullable)
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[int_sub]
+ eq_(MyClass.__table__.c.data_three.type.length, 30)
+ is_true(MyClass.__table__.c.data_three.nullable)
- @testing.variation(
- "dict_key", ["typing", ("plain", testing.requires.python310)]
- )
- def test_type_dont_mis_resolve_on_non_generic(self, dict_key):
- """test for #8859.
+ eq_(MyClass.__table__.c.data_four.type.length, 150)
+ is_false(MyClass.__table__.c.data_four.nullable)
- For a specific generic type with arguments, don't do any MRO
- lookup.
+ def test_newtype_missing_from_map(self, decl_base):
+ global str50
- """
+ str50 = NewType("str50", str)
- Base = declarative_base(
- type_annotation_map={
- dict: String,
- }
- )
+ if compat.py310:
+ text = ".*str50"
+ else:
+ # NewTypes before 3.10 had a very bad repr
+ # <function NewType.<locals>.new_type at 0x...>
+ text = ".*NewType.*"
- with expect_raises_message(
- sa_exc.ArgumentError, "Could not locate SQLAlchemy Core type"
+ with expect_deprecated(
+ f"Matching the provided NewType '{text}' on its "
+ "resolved value without matching it in the "
+ "type_annotation_map is deprecated; add this type to the "
+ "type_annotation_map to allow it to match explicitly.",
):
- class MyClass(Base):
- __tablename__ = "mytable"
+ class MyClass(decl_base):
+ __tablename__ = "my_table"
id: Mapped[int] = mapped_column(primary_key=True)
+ data_one: Mapped[str50]
- if dict_key.plain:
- data: Mapped[dict[str, str]]
- elif dict_key.typing:
- data: Mapped[Dict[str, str]]
-
- def test_type_secondary_resolution(self):
- class MyString(String):
- def _resolve_for_python_type(
- self, python_type, matched_type, matched_on_flattened
- ):
- return String(length=42)
-
- Base = declarative_base(type_annotation_map={str: MyString})
-
- class MyClass(Base):
- __tablename__ = "mytable"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[str]
-
- is_true(isinstance(MyClass.__table__.c.data.type, String))
- eq_(MyClass.__table__.c.data.type.length, 42)
+ is_true(isinstance(MyClass.data_one.type, String))
-class EnumOrLiteralTypeMapTest(fixtures.TestBase, testing.AssertsCompiledSQL):
+class ResolveToEnumTest(fixtures.TestBase, testing.AssertsCompiledSQL):
__dialect__ = "default"
@testing.variation("use_explicit_name", [True, False])
is_true(isinstance(Foo.__table__.c.status.type, JSON))
+ @testing.variation(
+ "type_",
+ [
+ "literal",
+ "literal_typing",
+ "recursive",
+ "not_literal",
+ "not_literal_typing",
+ "generic",
+ "generic_typing",
+ "generic_typed",
+ "generic_typed_typing",
+ ],
+ )
+ @testing.combinations(True, False, argnames="in_map")
+ @testing.requires.python312
+ def test_pep695_literal_defaults_to_enum(self, decl_base, type_, in_map):
+ """test #11305."""
+
+ def declare():
+ class Foo(decl_base):
+ __tablename__ = "footable"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ if type_.recursive:
+ status: Mapped[_RecursiveLiteral695] # noqa: F821
+ elif type_.literal:
+ status: Mapped[_Literal695] # noqa: F821
+ elif type_.literal_typing:
+ status: Mapped[_TypingLiteral695] # noqa: F821
+ elif type_.not_literal:
+ status: Mapped[_StrPep695] # noqa: F821
+ elif type_.not_literal_typing:
+ status: Mapped[_TypingStrPep695] # noqa: F821
+ elif type_.generic:
+ status: Mapped[_GenericPep695] # noqa: F821
+ elif type_.generic_typing:
+ status: Mapped[_TypingGenericPep695] # noqa: F821
+ elif type_.generic_typed:
+ status: Mapped[_GenericPep695Typed] # noqa: F821
+ elif type_.generic_typed_typing:
+ status: Mapped[_TypingGenericPep695Typed] # noqa: F821
+ else:
+ type_.fail()
+
+ return Foo
+
+ if in_map:
+ decl_base.registry.update_type_annotation_map(
+ {
+ _Literal695: Enum(enum.Enum), # noqa: F821
+ _TypingLiteral695: Enum(enum.Enum), # noqa: F821
+ _RecursiveLiteral695: Enum(enum.Enum), # noqa: F821
+ _StrPep695: Enum(enum.Enum), # noqa: F821
+ _TypingStrPep695: Enum(enum.Enum), # noqa: F821
+ _GenericPep695: Enum(enum.Enum), # noqa: F821
+ _TypingGenericPep695: Enum(enum.Enum), # noqa: F821
+ _GenericPep695Typed: Enum(enum.Enum), # noqa: F821
+ _TypingGenericPep695Typed: Enum(enum.Enum), # noqa: F821
+ }
+ )
+ if type_.recursive:
+ with expect_deprecated(
+ "Mapping recursive TypeAliasType '.+' that resolve to "
+ "literal to generate an Enum is deprecated. SQLAlchemy "
+ "2.1 will not support this use case. Please avoid using "
+ "recursing TypeAliasType",
+ ):
+ Foo = declare()
+ elif type_.literal or type_.literal_typing:
+ Foo = declare()
+ else:
+ with expect_raises_message(
+ exc.ArgumentError,
+ "Can't associate TypeAliasType '.+' to an Enum "
+ "since it's not a direct alias of a Literal. Only "
+ "aliases in this form `type my_alias = Literal.'a', "
+ "'b'.` are supported when generating Enums.",
+ ):
+ declare()
+ elif type_.literal or type_.literal_typing:
+ Foo = declare()
+ col = Foo.__table__.c.status
+ is_true(isinstance(col.type, Enum))
+ eq_(col.type.enums, ["to-do", "in-progress", "done"])
+ is_(col.type.native_enum, False)
+ elif type_.not_literal or type_.not_literal_typing:
+ Foo = declare()
+ col = Foo.__table__.c.status
+ is_true(isinstance(col.type, String))
+ elif type_.recursive:
+ with expect_deprecated(
+ "Matching to pep-695 type '_Literal695' in a "
+ "recursive fashion "
+ "without the recursed type being present in the "
+ "type_annotation_map is deprecated; add this type or its "
+ "recursed value to the type_annotation_map to allow it to "
+ "match explicitly."
+ ):
+ Foo = declare()
+ else:
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ r"Could not locate SQLAlchemy Core type when resolving "
+ r"for Python type "
+ r"indicated by '.+' inside the Mapped\[\] "
+ r"annotation for the 'status' attribute",
+ ):
+ declare()
+ return
+
class MixinTest(fixtures.TestBase, testing.AssertsCompiledSQL):
__dialect__ = "default"
mapped_column(), mapped_column(), mapped_column("zip")
)
- def test_extract_from_pep593(self, decl_base):
- global Address
-
- @dataclasses.dataclass
- class Address:
- street: str
- state: str
- zip_: str
-
- class User(decl_base):
- __tablename__ = "user"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- name: Mapped[str] = mapped_column()
-
- address: Mapped[Annotated[Address, "foo"]] = composite(
- mapped_column(), mapped_column(), mapped_column("zip")
- )
-
- self.assert_compile(
- select(User),
- 'SELECT "user".id, "user".name, "user".street, '
- '"user".state, "user".zip FROM "user"',
- dialect="default",
- )
-
def test_cls_not_composite_compliant(self, decl_base):
global Address
)
_RecursiveLiteral695 = TypeAliasType("_RecursiveLiteral695", _Literal695)
+_GenericPep593TypeAlias = Annotated[TV, mapped_column(info={"hi": "there"})]
+
+_GenericPep593Pep695 = TypingTypeAliasType(
+ "_GenericPep593Pep695",
+ Annotated[TV, mapped_column(info={"hi": "there"})],
+ type_params=(TV,),
+)
+
+_RecursivePep695Pep593 = TypingTypeAliasType(
+ "_RecursivePep695Pep593",
+ Annotated[_TypingStrPep695, mapped_column(info={"hi": "there"})],
+)
+
def expect_annotation_syntax_error(name):
return expect_raises_message(
assert Child.__mapper__.attrs.parent.strategy.use_get
- @testing.combinations(
- (BIGINT(),),
- (BIGINT,),
- (Integer().with_variant(BIGINT, "default")),
- (Integer().with_variant(BIGINT(), "default")),
- (BIGINT().with_variant(String(), "some_other_dialect")),
- )
- def test_type_map_varieties(self, typ):
- Base = declarative_base(type_annotation_map={int: typ})
-
- class MyClass(Base):
- __tablename__ = "mytable"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- x: Mapped[int]
- y: Mapped[int] = mapped_column()
- z: Mapped[int] = mapped_column(typ)
-
- self.assert_compile(
- CreateTable(MyClass.__table__),
- "CREATE TABLE mytable (id BIGINT NOT NULL, "
- "x BIGINT NOT NULL, y BIGINT NOT NULL, z BIGINT NOT NULL, "
- "PRIMARY KEY (id))",
- )
-
def test_required_no_arg(self, decl_base):
with expect_raises_message(
sa_exc.ArgumentError,
is_true(User.__table__.c.data.nullable)
assert isinstance(User.__table__.c.created_at.type, DateTime)
- def test_construct_lhs_type_missing(self, decl_base):
- # anno only: global MyClass
-
- class MyClass:
- pass
-
- with expect_raises_message(
- sa_exc.ArgumentError,
- "Could not locate SQLAlchemy Core type for Python type "
- ".*MyClass.* inside the 'data' attribute Mapped annotation",
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[MyClass] = mapped_column()
-
- @testing.variation(
- "argtype",
- [
- "type",
- ("column", testing.requires.python310),
- ("mapped_column", testing.requires.python310),
- "column_class",
- "ref_to_type",
- ("ref_to_column", testing.requires.python310),
- ],
- )
- def test_construct_lhs_sqlalchemy_type(self, decl_base, argtype):
- """test for #12329.
-
- of note here are all the different messages we have for when the
- wrong thing is put into Mapped[], and in fact in #12329 we added
- another one.
-
- This is a lot of different messages, but at the same time they
- occur at different places in the interpretation of types. If
- we were to centralize all these messages, we'd still likely end up
- doing distinct messages for each scenario, so instead we added
- a new ArgumentError subclass MappedAnnotationError that provides
- some commonality to all of these cases.
-
-
- """
- expect_future_annotations = "annotations" in globals()
-
- if argtype.type:
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- # properties.py -> _init_column_for_annotation, type is
- # a SQL type
- "The type provided inside the 'data' attribute Mapped "
- "annotation is the SQLAlchemy type .*BigInteger.*. Expected "
- "a Python type instead",
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[BigInteger] = mapped_column()
-
- elif argtype.column:
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- # util.py -> _extract_mapped_subtype
- (
- re.escape(
- "Could not interpret annotation "
- "Mapped[Column('q', BigInteger)]."
- )
- if expect_future_annotations
- # properties.py -> _init_column_for_annotation, object is
- # not a SQL type or a python type, it's just some object
- else re.escape(
- "The object provided inside the 'data' attribute "
- "Mapped annotation is not a Python type, it's the "
- "object Column('q', BigInteger(), table=None). "
- "Expected a Python type."
- )
- ),
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[Column("q", BigInteger)] = ( # noqa: F821
- mapped_column()
- )
-
- elif argtype.mapped_column:
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- # properties.py -> _init_column_for_annotation, object is
- # not a SQL type or a python type, it's just some object
- # interestingly, this raises at the same point for both
- # future annotations mode and legacy annotations mode
- r"The object provided inside the 'data' attribute "
- "Mapped annotation is not a Python type, it's the object "
- r"\<sqlalchemy.orm.properties.MappedColumn.*\>. "
- "Expected a Python type.",
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- big_integer: Mapped[int] = mapped_column()
- data: Mapped[big_integer] = mapped_column()
-
- elif argtype.column_class:
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- # properties.py -> _init_column_for_annotation, type is not
- # a SQL type
- re.escape(
- "Could not locate SQLAlchemy Core type for Python type "
- "<class 'sqlalchemy.sql.schema.Column'> inside the "
- "'data' attribute Mapped annotation"
- ),
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[Column] = mapped_column()
-
- elif argtype.ref_to_type:
- mytype = BigInteger
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- (
- # decl_base.py -> _exract_mappable_attributes
- re.escape(
- "Could not resolve all types within mapped "
- 'annotation: "Mapped[mytype]"'
- )
- if expect_future_annotations
- # properties.py -> _init_column_for_annotation, type is
- # a SQL type
- else re.escape(
- "The type provided inside the 'data' attribute Mapped "
- "annotation is the SQLAlchemy type "
- "<class 'sqlalchemy.sql.sqltypes.BigInteger'>. "
- "Expected a Python type instead"
- )
- ),
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[mytype] = mapped_column()
-
- elif argtype.ref_to_column:
- mycol = Column("q", BigInteger)
-
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- # decl_base.py -> _exract_mappable_attributes
- (
- re.escape(
- "Could not resolve all types within mapped "
- 'annotation: "Mapped[mycol]"'
- )
- if expect_future_annotations
- else
- # properties.py -> _init_column_for_annotation, object is
- # not a SQL type or a python type, it's just some object
- re.escape(
- "The object provided inside the 'data' attribute "
- "Mapped "
- "annotation is not a Python type, it's the object "
- "Column('q', BigInteger(), table=None). "
- "Expected a Python type."
- )
- ),
- ):
-
- class User(decl_base):
- __tablename__ = "users"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[mycol] = mapped_column()
-
- else:
- argtype.fail()
-
def test_construct_rhs_type_override_lhs(self, decl_base):
class Element(decl_base):
__tablename__ = "element"
id: Mapped["int"] = mapped_column(primary_key=True)
data_one: Mapped["str"]
- def test_pep593_types_as_typemap_keys(
- self, decl_base: Type[DeclarativeBase]
- ):
- """neat!!!"""
- # anno only: global str50, str30, opt_str50, opt_str30
+ @testing.requires.python38
+ def test_typing_literal_identity(self, decl_base):
+ """See issue #11820"""
- str50 = Annotated[str, 50]
- str30 = Annotated[str, 30]
- opt_str50 = Optional[str50]
- opt_str30 = Optional[str30]
+ class Foo(decl_base):
+ __tablename__ = "footable"
- decl_base.registry.update_type_annotation_map(
- {str50: String(50), str30: String(30)}
- )
+ id: Mapped[int] = mapped_column(primary_key=True)
+ t: Mapped[_TypingLiteral]
+ te: Mapped[_TypingExtensionsLiteral]
- class MyClass(decl_base):
- __tablename__ = "my_table"
+ for col in (Foo.__table__.c.t, Foo.__table__.c.te):
+ is_true(isinstance(col.type, Enum))
+ eq_(col.type.enums, ["a", "b"])
+ is_(col.type.native_enum, False)
- id: Mapped[str50] = mapped_column(primary_key=True)
- data_one: Mapped[str30]
- data_two: Mapped[opt_str30]
- data_three: Mapped[str50]
- data_four: Mapped[opt_str50]
- data_five: Mapped[str]
- data_six: Mapped[Optional[str]]
+ @testing.requires.python310
+ def test_we_got_all_attrs_test_annotated(self):
+ argnames = _py_inspect.getfullargspec(mapped_column)
+ assert _annotated_names_tested.issuperset(argnames.kwonlyargs), (
+ f"annotated attributes were not tested: "
+ f"{set(argnames.kwonlyargs).difference(_annotated_names_tested)}"
+ )
- eq_(MyClass.__table__.c.data_one.type.length, 30)
- is_false(MyClass.__table__.c.data_one.nullable)
- eq_(MyClass.__table__.c.data_two.type.length, 30)
- is_true(MyClass.__table__.c.data_two.nullable)
- eq_(MyClass.__table__.c.data_three.type.length, 50)
-
- def test_plain_typealias_as_typemap_keys(
- self, decl_base: Type[DeclarativeBase]
+ @annotated_name_test_cases(
+ ("sort_order", 100, lambda sort_order: sort_order == 100),
+ ("nullable", False, lambda column: column.nullable is False),
+ (
+ "active_history",
+ True,
+ lambda column_property: column_property.active_history is True,
+ ),
+ (
+ "deferred",
+ True,
+ lambda column_property: column_property.deferred is True,
+ ),
+ (
+ "deferred",
+ _NoArg.NO_ARG,
+ lambda column_property: column_property is None,
+ ),
+ (
+ "deferred_group",
+ "mygroup",
+ lambda column_property: column_property.deferred is True
+ and column_property.group == "mygroup",
+ ),
+ (
+ "deferred_raiseload",
+ True,
+ lambda column_property: column_property.deferred is True
+ and column_property.raiseload is True,
+ ),
+ (
+ "server_default",
+ "25",
+ lambda column: column.server_default.arg == "25",
+ ),
+ (
+ "server_onupdate",
+ "25",
+ lambda column: column.server_onupdate.arg == "25",
+ ),
+ (
+ "default",
+ 25,
+ lambda column: column.default.arg == 25,
+ ),
+ (
+ "insert_default",
+ 25,
+ lambda column: column.default.arg == 25,
+ ),
+ (
+ "onupdate",
+ 25,
+ lambda column: column.onupdate.arg == 25,
+ ),
+ ("doc", "some doc", lambda column: column.doc == "some doc"),
+ (
+ "comment",
+ "some comment",
+ lambda column: column.comment == "some comment",
+ ),
+ ("index", True, lambda column: column.index is True),
+ ("index", _NoArg.NO_ARG, lambda column: column.index is None),
+ ("index", False, lambda column: column.index is False),
+ ("unique", True, lambda column: column.unique is True),
+ ("unique", False, lambda column: column.unique is False),
+ ("autoincrement", True, lambda column: column.autoincrement is True),
+ ("system", True, lambda column: column.system is True),
+ ("primary_key", True, lambda column: column.primary_key is True),
+ ("type_", BIGINT, lambda column: isinstance(column.type, BIGINT)),
+ ("info", {"foo": "bar"}, lambda column: column.info == {"foo": "bar"}),
+ (
+ "use_existing_column",
+ True,
+ lambda mc: mc._use_existing_column is True,
+ ),
+ (
+ "quote",
+ True,
+ exc.SADeprecationWarning(
+ "Can't use the 'key' or 'name' arguments in Annotated "
+ ),
+ ),
+ (
+ "key",
+ "mykey",
+ exc.SADeprecationWarning(
+ "Can't use the 'key' or 'name' arguments in Annotated "
+ ),
+ ),
+ (
+ "name",
+ "mykey",
+ exc.SADeprecationWarning(
+ "Can't use the 'key' or 'name' arguments in Annotated "
+ ),
+ ),
+ (
+ "kw_only",
+ True,
+ exc.SADeprecationWarning(
+ "Argument 'kw_only' is a dataclass argument "
+ ),
+ testing.requires.python310,
+ ),
+ (
+ "compare",
+ True,
+ exc.SADeprecationWarning(
+ "Argument 'compare' is a dataclass argument "
+ ),
+ testing.requires.python310,
+ ),
+ (
+ "default_factory",
+ lambda: 25,
+ exc.SADeprecationWarning(
+ "Argument 'default_factory' is a dataclass argument "
+ ),
+ ),
+ (
+ "repr",
+ True,
+ exc.SADeprecationWarning(
+ "Argument 'repr' is a dataclass argument "
+ ),
+ ),
+ (
+ "init",
+ True,
+ exc.SADeprecationWarning(
+ "Argument 'init' is a dataclass argument"
+ ),
+ ),
+ (
+ "hash",
+ True,
+ exc.SADeprecationWarning(
+ "Argument 'hash' is a dataclass argument"
+ ),
+ ),
+ (
+ "dataclass_metadata",
+ {},
+ exc.SADeprecationWarning(
+ "Argument 'dataclass_metadata' is a dataclass argument"
+ ),
+ ),
+ argnames="argname, argument, assertion",
+ )
+ @testing.variation("use_annotated", [True, False, "control"])
+ def test_names_encountered_for_annotated(
+ self, argname, argument, assertion, use_annotated, decl_base
):
- decl_base.registry.update_type_annotation_map(
- {_UnionTypeAlias: JSON, _StrTypeAlias: String(30)}
+ # anno only: global myint
+
+ if argument is not _NoArg.NO_ARG:
+ kw = {argname: argument}
+
+ if argname == "quote":
+ kw["name"] = "somename"
+ else:
+ kw = {}
+
+ is_warning = isinstance(assertion, exc.SADeprecationWarning)
+ is_dataclass = argname in (
+ "kw_only",
+ "init",
+ "repr",
+ "compare",
+ "default_factory",
+ "hash",
+ "dataclass_metadata",
)
- class Test(decl_base):
- __tablename__ = "test"
+ if is_dataclass:
+
+ class Base(MappedAsDataclass, decl_base):
+ __abstract__ = True
+
+ else:
+ Base = decl_base
+
+ if use_annotated.control:
+ # test in reverse; that kw set on the main mapped_column() takes
+ # effect when the Annotated is there also and does not have the
+ # kw
+ amc = mapped_column()
+ myint = Annotated[int, amc]
+
+ mc = mapped_column(**kw)
+
+ class User(Base):
+ __tablename__ = "user"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ myname: Mapped[myint] = mc
+
+ elif use_annotated:
+ amc = mapped_column(**kw)
+ myint = Annotated[int, amc]
+
+ mc = mapped_column()
+
+ if is_warning:
+ with expect_deprecated(assertion.args[0]):
+
+ class User(Base):
+ __tablename__ = "user"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ myname: Mapped[myint] = mc
+
+ else:
+
+ class User(Base):
+ __tablename__ = "user"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ myname: Mapped[myint] = mc
+
+ else:
+ mc = cast(MappedColumn, mapped_column(**kw))
+
+ mapper_prop = mc.mapper_property_to_assign
+ column_to_assign, sort_order = mc.columns_to_assign[0]
+
+ if not is_warning:
+ assert_result = testing.resolve_lambda(
+ assertion,
+ sort_order=sort_order,
+ column_property=mapper_prop,
+ column=column_to_assign,
+ mc=mc,
+ )
+ assert assert_result
+ elif is_dataclass and (not use_annotated or use_annotated.control):
+ eq_(
+ getattr(mc._attribute_options, f"dataclasses_{argname}"),
+ argument,
+ )
+
+ @testing.combinations(("index",), ("unique",), argnames="paramname")
+ @testing.combinations((True,), (False,), (None,), argnames="orig")
+ @testing.combinations((True,), (False,), (None,), argnames="merging")
+ def test_index_unique_combinations(
+ self, paramname, orig, merging, decl_base
+ ):
+ """test #11091"""
+
+ # anno only: global myint
+
+ amc = mapped_column(**{paramname: merging})
+ myint = Annotated[int, amc]
+
+ mc = mapped_column(**{paramname: orig})
+
+ class User(decl_base):
+ __tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[_StrTypeAlias]
- structure: Mapped[_UnionTypeAlias]
+ myname: Mapped[myint] = mc
- eq_(Test.__table__.c.data.type.length, 30)
- is_(Test.__table__.c.structure.type._type_affinity, JSON)
+ result = getattr(User.__table__.c.myname, paramname)
+ if orig is None:
+ is_(result, merging)
+ else:
+ is_(result, orig)
@testing.variation(
- "option",
+ "union",
[
- "plain",
"union",
- "union_604",
+ ("pep604", requires.python310),
"union_null",
- "union_null_604",
- "optional",
- "optional_union",
- "optional_union_604",
- "union_newtype",
- "union_null_newtype",
- "union_695",
- "union_null_695",
+ ("pep604_null", requires.python310),
],
)
- @testing.variation("in_map", ["yes", "no", "value"])
- @testing.requires.python312
- def test_pep695_behavior(self, decl_base, in_map, option):
- """Issue #11955"""
- # anno only: global tat
+ def test_unions(self, union):
+ # anno only: global UnionType
+ our_type = Numeric(10, 2)
- if option.plain:
- tat = TypeAliasType("tat", str)
- elif option.union:
- tat = TypeAliasType("tat", Union[str, int])
- elif option.union_604:
- tat = TypeAliasType("tat", str | int)
- elif option.union_null:
- tat = TypeAliasType("tat", Union[str, int, None])
- elif option.union_null_604:
- tat = TypeAliasType("tat", str | int | None)
- elif option.optional:
- tat = TypeAliasType("tat", Optional[str])
- elif option.optional_union:
- tat = TypeAliasType("tat", Optional[Union[str, int]])
- elif option.optional_union_604:
- tat = TypeAliasType("tat", Optional[str | int])
- elif option.union_newtype:
- # this seems to be illegal for typing but "works"
- tat = NewType("tat", Union[str, int])
- elif option.union_null_newtype:
- # this seems to be illegal for typing but "works"
- tat = NewType("tat", Union[str, int, None])
- elif option.union_695:
- tat = TypeAliasType("tat", str | int)
- elif option.union_null_695:
- tat = TypeAliasType("tat", str | int | None)
+ if union.union:
+ UnionType = Union[float, Decimal]
+ elif union.union_null:
+ UnionType = Union[float, Decimal, None]
+ elif union.pep604:
+ UnionType = float | Decimal
+ elif union.pep604_null:
+ UnionType = float | Decimal | None
else:
- option.fail()
+ union.fail()
- if in_map.yes:
- decl_base.registry.update_type_annotation_map({tat: String(99)})
- elif in_map.value and "newtype" not in option.name:
- decl_base.registry.update_type_annotation_map(
- {tat.__value__: String(99)}
+ class Base(DeclarativeBase):
+ type_annotation_map = {UnionType: our_type}
+
+ class User(Base):
+ __tablename__ = "users"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+
+ data: Mapped[Union[float, Decimal]]
+ reverse_data: Mapped[Union[Decimal, float]]
+
+ optional_data: Mapped[Optional[Union[float, Decimal]]] = (
+ mapped_column()
)
- def declare():
- class Test(decl_base):
- __tablename__ = "test"
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[tat]
+ # use Optional directly
+ reverse_optional_data: Mapped[Optional[Union[Decimal, float]]] = (
+ mapped_column()
+ )
- return Test.__table__.c.data
+ # use Union with None, same as Optional but presents differently
+ # (Optional object with __origin__ Union vs. Union)
+ reverse_u_optional_data: Mapped[Union[Decimal, float, None]] = (
+ mapped_column()
+ )
- if in_map.yes:
- col = declare()
- length = 99
- elif (
- in_map.value
- and "newtype" not in option.name
- or option.optional
- or option.plain
- ):
- with expect_deprecated(
- "Matching the provided TypeAliasType 'tat' on its "
- "resolved value without matching it in the "
- "type_annotation_map is deprecated; add this type to the "
- "type_annotation_map to allow it to match explicitly.",
- ):
- col = declare()
- length = 99 if in_map.value else None
- else:
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- r"Could not locate SQLAlchemy Core type for Python type .*tat "
- "inside the 'data' attribute Mapped annotation",
- ):
- declare()
- return
+ refer_union: Mapped[UnionType]
+ refer_union_optional: Mapped[Optional[UnionType]]
- is_true(isinstance(col.type, String))
- eq_(col.type.length, length)
- nullable = "null" in option.name or "optional" in option.name
- eq_(col.nullable, nullable)
+ # py38, 37 does not automatically flatten unions, add extra tests
+ # for this. maintain these in order to catch future regressions
+ # in the behavior of ``Union``
+ unflat_union_optional_data: Mapped[
+ Union[Union[Decimal, float, None], None]
+ ] = mapped_column()
- @testing.variation(
- "type_",
- [
- "str_extension",
- "str_typing",
- "generic_extension",
- "generic_typing",
- "generic_typed_extension",
- "generic_typed_typing",
- ],
- )
- @testing.requires.python312
- def test_pep695_typealias_as_typemap_keys(
- self, decl_base: Type[DeclarativeBase], type_
- ):
- """test #10807"""
+ float_data: Mapped[float] = mapped_column()
+ decimal_data: Mapped[Decimal] = mapped_column()
- decl_base.registry.update_type_annotation_map(
- {
- _UnionPep695: JSON,
- _StrPep695: String(30),
- _TypingStrPep695: String(30),
- _GenericPep695: String(30),
- _TypingGenericPep695: String(30),
- _GenericPep695Typed: String(30),
- _TypingGenericPep695Typed: String(30),
- }
- )
+ if compat.py310:
+ pep604_data: Mapped[float | Decimal] = mapped_column()
+ pep604_reverse: Mapped[Decimal | float] = mapped_column()
+ pep604_optional: Mapped[Decimal | float | None] = (
+ mapped_column()
+ )
+ pep604_data_fwd: Mapped["float | Decimal"] = mapped_column()
+ pep604_reverse_fwd: Mapped["Decimal | float"] = mapped_column()
+ pep604_optional_fwd: Mapped["Decimal | float | None"] = (
+ mapped_column()
+ )
- class Test(decl_base):
- __tablename__ = "test"
- id: Mapped[int] = mapped_column(primary_key=True)
- if type_.str_extension:
- data: Mapped[_StrPep695]
- elif type_.str_typing:
- data: Mapped[_TypingStrPep695]
- elif type_.generic_extension:
- data: Mapped[_GenericPep695]
- elif type_.generic_typing:
- data: Mapped[_TypingGenericPep695]
- elif type_.generic_typed_extension:
- data: Mapped[_GenericPep695Typed]
- elif type_.generic_typed_typing:
- data: Mapped[_TypingGenericPep695Typed]
- else:
- type_.fail()
- structure: Mapped[_UnionPep695]
+ info = [
+ ("data", False),
+ ("reverse_data", False),
+ ("optional_data", True),
+ ("reverse_optional_data", True),
+ ("reverse_u_optional_data", True),
+ ("refer_union", "null" in union.name),
+ ("refer_union_optional", True),
+ ("unflat_union_optional_data", True),
+ ]
+ if compat.py310:
+ info += [
+ ("pep604_data", False),
+ ("pep604_reverse", False),
+ ("pep604_optional", True),
+ ("pep604_data_fwd", False),
+ ("pep604_reverse_fwd", False),
+ ("pep604_optional_fwd", True),
+ ]
+
+ for name, nullable in info:
+ col = User.__table__.c[name]
+ is_(col.type, our_type, name)
+ is_(col.nullable, nullable, name)
- eq_(Test.__table__.c.data.type._type_affinity, String)
- eq_(Test.__table__.c.data.type.length, 30)
- is_(Test.__table__.c.structure.type._type_affinity, JSON)
+ is_true(isinstance(User.__table__.c.float_data.type, Float))
+ ne_(User.__table__.c.float_data.type, our_type)
+
+ is_true(isinstance(User.__table__.c.decimal_data.type, Numeric))
+ ne_(User.__table__.c.decimal_data.type, our_type)
@testing.variation(
- "alias_type",
- ["none", "typekeyword", "typealias", "typekeyword_nested"],
+ "union",
+ [
+ "union",
+ ("pep604", requires.python310),
+ ("pep695", requires.python312),
+ ],
)
- @testing.requires.python312
- def test_extract_pep593_from_pep695(
- self, decl_base: Type[DeclarativeBase], alias_type
- ):
- """test #11130"""
- if alias_type.typekeyword:
- decl_base.registry.update_type_annotation_map(
- {strtypalias_keyword: VARCHAR(33)} # noqa: F821
- )
- if alias_type.typekeyword_nested:
- decl_base.registry.update_type_annotation_map(
- {strtypalias_keyword_nested: VARCHAR(42)} # noqa: F821
- )
-
- class MyClass(decl_base):
- __tablename__ = "my_table"
-
- id: Mapped[int] = mapped_column(primary_key=True)
+ def test_optional_in_annotation_map(self, union):
+ """See issue #11370"""
- if alias_type.typekeyword:
- data_one: Mapped[strtypalias_keyword] # noqa: F821
- elif alias_type.typealias:
- data_one: Mapped[strtypalias_ta] # noqa: F821
- elif alias_type.none:
- data_one: Mapped[strtypalias_plain] # noqa: F821
- elif alias_type.typekeyword_nested:
- data_one: Mapped[strtypalias_keyword_nested] # noqa: F821
+ class Base(DeclarativeBase):
+ if union.union:
+ type_annotation_map = {_Json: JSON}
+ elif union.pep604:
+ type_annotation_map = {_JsonPep604: JSON}
+ elif union.pep695:
+ type_annotation_map = {_JsonPep695: JSON} # noqa: F821
else:
- alias_type.fail()
+ union.fail()
- table = MyClass.__table__
- assert table is not None
+ class A(Base):
+ __tablename__ = "a"
- if alias_type.typekeyword_nested:
- # a nested annotation is not supported
- eq_(MyClass.data_one.expression.info, {})
- else:
- eq_(MyClass.data_one.expression.info, {"hi": "there"})
+ id: Mapped[int] = mapped_column(primary_key=True)
+ if union.union:
+ json1: Mapped[_Json]
+ json2: Mapped[_Json] = mapped_column(nullable=False)
+ elif union.pep604:
+ json1: Mapped[_JsonPep604]
+ json2: Mapped[_JsonPep604] = mapped_column(nullable=False)
+ elif union.pep695:
+ json1: Mapped[_JsonPep695] # noqa: F821
+ json2: Mapped[_JsonPep695] = mapped_column( # noqa: F821
+ nullable=False
+ )
+ else:
+ union.fail()
- if alias_type.typekeyword:
- eq_(MyClass.data_one.type.length, 33)
- elif alias_type.typekeyword_nested:
- eq_(MyClass.data_one.type.length, 42)
- else:
- eq_(MyClass.data_one.type.length, None)
+ is_(A.__table__.c.json1.type._type_affinity, JSON)
+ is_(A.__table__.c.json2.type._type_affinity, JSON)
+ is_true(A.__table__.c.json1.nullable)
+ is_false(A.__table__.c.json2.nullable)
@testing.variation(
- "type_",
+ "option",
[
- "literal",
- "literal_typing",
- "recursive",
- "not_literal",
- "not_literal_typing",
- "generic",
- "generic_typing",
- "generic_typed",
- "generic_typed_typing",
+ "not_optional",
+ "optional",
+ "optional_fwd_ref",
+ "union_none",
+ ("pep604", testing.requires.python310),
+ ("pep604_fwd_ref", testing.requires.python310),
],
)
- @testing.combinations(True, False, argnames="in_map")
- @testing.requires.python312
- def test_pep695_literal_defaults_to_enum(self, decl_base, type_, in_map):
- """test #11305."""
+ @testing.variation("brackets", ["oneset", "twosets"])
+ @testing.combinations(
+ "include_mc_type", "derive_from_anno", argnames="include_mc_type"
+ )
+ def test_optional_styles_nested_brackets(
+ self, option, brackets, include_mc_type
+ ):
+ """composed types test, includes tests that were added later for
+ #12207"""
- def declare():
- class Foo(decl_base):
- __tablename__ = "footable"
+ class Base(DeclarativeBase):
+ if testing.requires.python310.enabled:
+ type_annotation_map = {
+ Dict[str, Decimal]: JSON,
+ dict[str, Decimal]: JSON,
+ Union[List[int], List[str]]: JSON,
+ list[int] | list[str]: JSON,
+ }
+ else:
+ type_annotation_map = {
+ Dict[str, Decimal]: JSON,
+ Union[List[int], List[str]]: JSON,
+ }
- id: Mapped[int] = mapped_column(primary_key=True)
- if type_.recursive:
- status: Mapped[_RecursiveLiteral695] # noqa: F821
- elif type_.literal:
- status: Mapped[_Literal695] # noqa: F821
- elif type_.literal_typing:
- status: Mapped[_TypingLiteral695] # noqa: F821
- elif type_.not_literal:
- status: Mapped[_StrPep695] # noqa: F821
- elif type_.not_literal_typing:
- status: Mapped[_TypingStrPep695] # noqa: F821
- elif type_.generic:
- status: Mapped[_GenericPep695] # noqa: F821
- elif type_.generic_typing:
- status: Mapped[_TypingGenericPep695] # noqa: F821
- elif type_.generic_typed:
- status: Mapped[_GenericPep695Typed] # noqa: F821
- elif type_.generic_typed_typing:
- status: Mapped[_TypingGenericPep695Typed] # noqa: F821
- else:
- type_.fail()
+ if include_mc_type == "include_mc_type":
+ mc = mapped_column(JSON)
+ mc2 = mapped_column(JSON)
+ else:
+ mc = mapped_column()
+ mc2 = mapped_column()
- return Foo
+ class A(Base):
+ __tablename__ = "a"
- if in_map:
- decl_base.registry.update_type_annotation_map(
- {
- _Literal695: Enum(enum.Enum), # noqa: F821
- _TypingLiteral695: Enum(enum.Enum), # noqa: F821
- _RecursiveLiteral695: Enum(enum.Enum), # noqa: F821
- _StrPep695: Enum(enum.Enum), # noqa: F821
- _TypingStrPep695: Enum(enum.Enum), # noqa: F821
- _GenericPep695: Enum(enum.Enum), # noqa: F821
- _TypingGenericPep695: Enum(enum.Enum), # noqa: F821
- _GenericPep695Typed: Enum(enum.Enum), # noqa: F821
- _TypingGenericPep695Typed: Enum(enum.Enum), # noqa: F821
- }
- )
- if type_.recursive:
- with expect_deprecated(
- "Mapping recursive TypeAliasType '.+' that resolve to "
- "literal to generate an Enum is deprecated. SQLAlchemy "
- "2.1 will not support this use case. Please avoid using "
- "recursing TypeAliasType",
- ):
- Foo = declare()
- elif type_.literal or type_.literal_typing:
- Foo = declare()
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[str] = mapped_column()
+
+ if brackets.oneset:
+ if option.not_optional:
+ json: Mapped[Dict[str, Decimal]] = mapped_column() # type: ignore # noqa: E501
+ if testing.requires.python310.enabled:
+ json2: Mapped[dict[str, Decimal]] = mapped_column() # type: ignore # noqa: E501
+ elif option.optional:
+ json: Mapped[Optional[Dict[str, Decimal]]] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[Optional[dict[str, Decimal]]] = mc2
+ elif option.optional_fwd_ref:
+ json: Mapped["Optional[Dict[str, Decimal]]"] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped["Optional[dict[str, Decimal]]"] = mc2
+ elif option.union_none:
+ json: Mapped[Union[Dict[str, Decimal], None]] = mc
+ json2: Mapped[Union[None, Dict[str, Decimal]]] = mc2
+ elif option.pep604:
+ json: Mapped[dict[str, Decimal] | None] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[None | dict[str, Decimal]] = mc2
+ elif option.pep604_fwd_ref:
+ json: Mapped["dict[str, Decimal] | None"] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped["None | dict[str, Decimal]"] = mc2
+ elif brackets.twosets:
+ if option.not_optional:
+ json: Mapped[Union[List[int], List[str]]] = mapped_column() # type: ignore # noqa: E501
+ elif option.optional:
+ json: Mapped[Optional[Union[List[int], List[str]]]] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[
+ Optional[Union[list[int], list[str]]]
+ ] = mc2
+ elif option.optional_fwd_ref:
+ json: Mapped["Optional[Union[List[int], List[str]]]"] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[
+ "Optional[Union[list[int], list[str]]]"
+ ] = mc2
+ elif option.union_none:
+ json: Mapped[Union[List[int], List[str], None]] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[Union[None, list[int], list[str]]] = mc2
+ elif option.pep604:
+ json: Mapped[list[int] | list[str] | None] = mc
+ json2: Mapped[None | list[int] | list[str]] = mc2
+ elif option.pep604_fwd_ref:
+ json: Mapped["list[int] | list[str] | None"] = mc
+ json2: Mapped["None | list[int] | list[str]"] = mc2
else:
- with expect_raises_message(
- exc.ArgumentError,
- "Can't associate TypeAliasType '.+' to an Enum "
- "since it's not a direct alias of a Literal. Only "
- "aliases in this form `type my_alias = Literal.'a', "
- "'b'.` are supported when generating Enums.",
- ):
- declare()
- return
- elif (
- type_.generic
- or type_.generic_typing
- or type_.generic_typed
- or type_.generic_typed_typing
- ):
- # This behaves like 2.1 -> rationale is that no-one asked to
- # support such types and in 2.1 will already be like this
- # so it makes little sense to add support this late in the 2.0
- # series
+ brackets.fail()
+
+ is_(A.__table__.c.json.type._type_affinity, JSON)
+ if hasattr(A, "json2"):
+ is_(A.__table__.c.json2.type._type_affinity, JSON)
+ if option.not_optional:
+ is_false(A.__table__.c.json2.nullable)
+ else:
+ is_true(A.__table__.c.json2.nullable)
+
+ if option.not_optional:
+ is_false(A.__table__.c.json.nullable)
+ else:
+ is_true(A.__table__.c.json.nullable)
+
+ @testing.variation("optional", [True, False])
+ @testing.variation("provide_type", [True, False])
+ @testing.variation("add_to_type_map", [True, False])
+ def test_recursive_type(
+ self, decl_base, optional, provide_type, add_to_type_map
+ ):
+ """test #9553"""
+
+ global T
+
+ T = Dict[str, Optional["T"]]
+
+ if not provide_type and not add_to_type_map:
with expect_raises_message(
- exc.ArgumentError,
- "Could not locate SQLAlchemy Core type for Python type "
- ".+ inside the 'status' attribute Mapped annotation",
+ sa_exc.ArgumentError,
+ r"Could not locate SQLAlchemy.*" r".*ForwardRef\('T'\).*",
):
- declare()
+
+ class TypeTest(decl_base):
+ __tablename__ = "my_table"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ if optional:
+ type_test: Mapped[Optional[T]] = mapped_column()
+ else:
+ type_test: Mapped[T] = mapped_column()
+
return
+
else:
- with expect_deprecated(
- "Matching the provided TypeAliasType '.*' on its "
- "resolved value without matching it in the "
- "type_annotation_map is deprecated; add this type to the "
- "type_annotation_map to allow it to match explicitly.",
- ):
- Foo = declare()
- col = Foo.__table__.c.status
- if in_map and not type_.not_literal:
- is_true(isinstance(col.type, Enum))
- eq_(col.type.enums, ["to-do", "in-progress", "done"])
- is_(col.type.native_enum, False)
+ if add_to_type_map:
+ decl_base.registry.update_type_annotation_map({T: JSON()})
+
+ class TypeTest(decl_base):
+ __tablename__ = "my_table"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+
+ if add_to_type_map:
+ if optional:
+ type_test: Mapped[Optional[T]] = mapped_column()
+ else:
+ type_test: Mapped[T] = mapped_column()
+ else:
+ if optional:
+ type_test: Mapped[Optional[T]] = mapped_column(JSON())
+ else:
+ type_test: Mapped[T] = mapped_column(JSON())
+
+ if optional:
+ is_(TypeTest.__table__.c.type_test.nullable, True)
else:
- is_true(isinstance(col.type, String))
+ is_(TypeTest.__table__.c.type_test.nullable, False)
- @testing.requires.python38
- def test_typing_literal_identity(self, decl_base):
- """See issue #11820"""
+ self.assert_compile(
+ select(TypeTest),
+ "SELECT my_table.id, my_table.type_test FROM my_table",
+ )
- class Foo(decl_base):
- __tablename__ = "footable"
+ def test_missing_mapped_lhs(self, decl_base):
+ with expect_annotation_syntax_error("User.name"):
- id: Mapped[int] = mapped_column(primary_key=True)
- t: Mapped[_TypingLiteral]
- te: Mapped[_TypingExtensionsLiteral]
+ class User(decl_base):
+ __tablename__ = "users"
- for col in (Foo.__table__.c.t, Foo.__table__.c.te):
- is_true(isinstance(col.type, Enum))
- eq_(col.type.enums, ["a", "b"])
- is_(col.type.native_enum, False)
+ id: Mapped[int] = mapped_column(primary_key=True)
+ name: str = mapped_column() # type: ignore
- @testing.requires.python310
- def test_we_got_all_attrs_test_annotated(self):
- argnames = _py_inspect.getfullargspec(mapped_column)
- assert _annotated_names_tested.issuperset(argnames.kwonlyargs), (
- f"annotated attributes were not tested: "
- f"{set(argnames.kwonlyargs).difference(_annotated_names_tested)}"
+ def test_construct_lhs_separate_name(self, decl_base):
+ class User(decl_base):
+ __tablename__ = "users"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ name: Mapped[str] = mapped_column()
+ data: Mapped[Optional[str]] = mapped_column("the_data")
+
+ self.assert_compile(
+ select(User.data), "SELECT users.the_data FROM users"
)
+ is_true(User.__table__.c.the_data.nullable)
- @annotated_name_test_cases(
- ("sort_order", 100, lambda sort_order: sort_order == 100),
- ("nullable", False, lambda column: column.nullable is False),
- (
- "active_history",
- True,
- lambda column_property: column_property.active_history is True,
- ),
- (
- "deferred",
- True,
- lambda column_property: column_property.deferred is True,
- ),
- (
- "deferred",
- _NoArg.NO_ARG,
- lambda column_property: column_property is None,
- ),
- (
- "deferred_group",
- "mygroup",
- lambda column_property: column_property.deferred is True
- and column_property.group == "mygroup",
- ),
- (
- "deferred_raiseload",
- True,
- lambda column_property: column_property.deferred is True
- and column_property.raiseload is True,
- ),
- (
- "server_default",
- "25",
- lambda column: column.server_default.arg == "25",
- ),
- (
- "server_onupdate",
- "25",
- lambda column: column.server_onupdate.arg == "25",
- ),
- (
- "default",
- 25,
- lambda column: column.default.arg == 25,
- ),
- (
- "insert_default",
- 25,
- lambda column: column.default.arg == 25,
- ),
- (
- "onupdate",
- 25,
- lambda column: column.onupdate.arg == 25,
- ),
- ("doc", "some doc", lambda column: column.doc == "some doc"),
- (
- "comment",
- "some comment",
- lambda column: column.comment == "some comment",
- ),
- ("index", True, lambda column: column.index is True),
- ("index", _NoArg.NO_ARG, lambda column: column.index is None),
- ("index", False, lambda column: column.index is False),
- ("unique", True, lambda column: column.unique is True),
- ("unique", False, lambda column: column.unique is False),
- ("autoincrement", True, lambda column: column.autoincrement is True),
- ("system", True, lambda column: column.system is True),
- ("primary_key", True, lambda column: column.primary_key is True),
- ("type_", BIGINT, lambda column: isinstance(column.type, BIGINT)),
- ("info", {"foo": "bar"}, lambda column: column.info == {"foo": "bar"}),
- (
- "use_existing_column",
- True,
- lambda mc: mc._use_existing_column is True,
- ),
- (
- "quote",
- True,
- exc.SADeprecationWarning(
- "Can't use the 'key' or 'name' arguments in Annotated "
- ),
- ),
- (
- "key",
- "mykey",
- exc.SADeprecationWarning(
- "Can't use the 'key' or 'name' arguments in Annotated "
- ),
- ),
- (
- "name",
- "mykey",
- exc.SADeprecationWarning(
- "Can't use the 'key' or 'name' arguments in Annotated "
- ),
- ),
- (
- "kw_only",
- True,
- exc.SADeprecationWarning(
- "Argument 'kw_only' is a dataclass argument "
- ),
- testing.requires.python310,
- ),
- (
- "compare",
- True,
- exc.SADeprecationWarning(
- "Argument 'compare' is a dataclass argument "
- ),
- testing.requires.python310,
- ),
- (
- "default_factory",
- lambda: 25,
- exc.SADeprecationWarning(
- "Argument 'default_factory' is a dataclass argument "
- ),
- ),
- (
- "repr",
- True,
- exc.SADeprecationWarning(
- "Argument 'repr' is a dataclass argument "
- ),
- ),
- (
- "init",
- True,
- exc.SADeprecationWarning(
- "Argument 'init' is a dataclass argument"
- ),
- ),
- (
- "hash",
- True,
- exc.SADeprecationWarning(
- "Argument 'hash' is a dataclass argument"
- ),
- ),
- (
- "dataclass_metadata",
- {},
- exc.SADeprecationWarning(
- "Argument 'dataclass_metadata' is a dataclass argument"
- ),
- ),
- argnames="argname, argument, assertion",
- )
- @testing.variation("use_annotated", [True, False, "control"])
- def test_names_encountered_for_annotated(
- self, argname, argument, assertion, use_annotated, decl_base
- ):
- # anno only: global myint
+ def test_construct_works_in_expr(self, decl_base):
+ class User(decl_base):
+ __tablename__ = "users"
- if argument is not _NoArg.NO_ARG:
- kw = {argname: argument}
+ id: Mapped[int] = mapped_column(primary_key=True)
- if argname == "quote":
- kw["name"] = "somename"
- else:
- kw = {}
+ class Address(decl_base):
+ __tablename__ = "addresses"
- is_warning = isinstance(assertion, exc.SADeprecationWarning)
- is_dataclass = argname in (
- "kw_only",
- "init",
- "repr",
- "compare",
- "default_factory",
- "hash",
- "dataclass_metadata",
- )
+ id: Mapped[int] = mapped_column(primary_key=True)
+ user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
- if is_dataclass:
+ user = relationship(User, primaryjoin=user_id == User.id)
- class Base(MappedAsDataclass, decl_base):
- __abstract__ = True
+ self.assert_compile(
+ select(Address.user_id, User.id).join(Address.user),
+ "SELECT addresses.user_id, users.id FROM addresses "
+ "JOIN users ON addresses.user_id = users.id",
+ )
- else:
- Base = decl_base
+ def test_construct_works_as_polymorphic_on(self, decl_base):
+ class User(decl_base):
+ __tablename__ = "users"
- if use_annotated.control:
- # test in reverse; that kw set on the main mapped_column() takes
- # effect when the Annotated is there also and does not have the
- # kw
- amc = mapped_column()
- myint = Annotated[int, amc]
+ id: Mapped[int] = mapped_column(primary_key=True)
+ type: Mapped[str] = mapped_column()
- mc = mapped_column(**kw)
+ __mapper_args__ = {"polymorphic_on": type}
- class User(Base):
- __tablename__ = "user"
- id: Mapped[int] = mapped_column(primary_key=True)
- myname: Mapped[myint] = mc
+ decl_base.registry.configure()
+ is_(User.__table__.c.type, User.__mapper__.polymorphic_on)
- elif use_annotated:
- amc = mapped_column(**kw)
- myint = Annotated[int, amc]
+ def test_construct_works_as_version_id_col(self, decl_base):
+ class User(decl_base):
+ __tablename__ = "users"
- mc = mapped_column()
+ id: Mapped[int] = mapped_column(primary_key=True)
+ version_id: Mapped[int] = mapped_column()
- if is_warning:
- with expect_deprecated(assertion.args[0]):
+ __mapper_args__ = {"version_id_col": version_id}
- class User(Base):
- __tablename__ = "user"
- id: Mapped[int] = mapped_column(primary_key=True)
- myname: Mapped[myint] = mc
+ decl_base.registry.configure()
+ is_(User.__table__.c.version_id, User.__mapper__.version_id_col)
- else:
+ def test_construct_works_in_deferred(self, decl_base):
+ class User(decl_base):
+ __tablename__ = "users"
- class User(Base):
- __tablename__ = "user"
- id: Mapped[int] = mapped_column(primary_key=True)
- myname: Mapped[myint] = mc
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[str] = deferred(mapped_column())
- else:
- mc = cast(MappedColumn, mapped_column(**kw))
+ self.assert_compile(select(User), "SELECT users.id FROM users")
+ self.assert_compile(
+ select(User).options(undefer(User.data)),
+ "SELECT users.id, users.data FROM users",
+ )
- mapper_prop = mc.mapper_property_to_assign
- column_to_assign, sort_order = mc.columns_to_assign[0]
+ def test_deferred_kw(self, decl_base):
+ class User(decl_base):
+ __tablename__ = "users"
- if not is_warning:
- assert_result = testing.resolve_lambda(
- assertion,
- sort_order=sort_order,
- column_property=mapper_prop,
- column=column_to_assign,
- mc=mc,
- )
- assert assert_result
- elif is_dataclass and (not use_annotated or use_annotated.control):
- eq_(
- getattr(mc._attribute_options, f"dataclasses_{argname}"),
- argument,
- )
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[str] = mapped_column(deferred=True)
- @testing.combinations(("index",), ("unique",), argnames="paramname")
- @testing.combinations((True,), (False,), (None,), argnames="orig")
- @testing.combinations((True,), (False,), (None,), argnames="merging")
- def test_index_unique_combinations(
- self, paramname, orig, merging, decl_base
- ):
- """test #11091"""
+ self.assert_compile(select(User), "SELECT users.id FROM users")
+ self.assert_compile(
+ select(User).options(undefer(User.data)),
+ "SELECT users.id, users.data FROM users",
+ )
- # anno only: global myint
- amc = mapped_column(**{paramname: merging})
- myint = Annotated[int, amc]
+class Pep593InterpretationTests(fixtures.TestBase, testing.AssertsCompiledSQL):
+ __dialect__ = "default"
- mc = mapped_column(**{paramname: orig})
+ def test_extract_from_pep593(self, decl_base):
+ # anno only: global Address
+
+ @dataclasses.dataclass
+ class Address:
+ street: str
+ state: str
+ zip_: str
class User(decl_base):
__tablename__ = "user"
+
id: Mapped[int] = mapped_column(primary_key=True)
- myname: Mapped[myint] = mc
+ name: Mapped[str] = mapped_column()
- result = getattr(User.__table__.c.myname, paramname)
- if orig is None:
- is_(result, merging)
- else:
- is_(result, orig)
+ address: Mapped[Annotated[Address, "foo"]] = composite(
+ mapped_column(), mapped_column(), mapped_column("zip")
+ )
- def test_pep484_newtypes_as_typemap_keys(
+ self.assert_compile(
+ select(User),
+ 'SELECT "user".id, "user".name, "user".street, '
+ '"user".state, "user".zip FROM "user"',
+ dialect="default",
+ )
+
+ def test_pep593_types_as_typemap_keys(
self, decl_base: Type[DeclarativeBase]
):
- # anno only: global str50, str30, str3050
+ """neat!!!"""
+ # anno only: global str50, str30, opt_str50, opt_str30
- str50 = NewType("str50", str)
- str30 = NewType("str30", str)
- str3050 = NewType("str30", str50)
+ str50 = Annotated[str, 50]
+ str30 = Annotated[str, 30]
+ opt_str50 = Optional[str50]
+ opt_str30 = Optional[str30]
decl_base.registry.update_type_annotation_map(
- {str50: String(50), str30: String(30), str3050: String(150)}
+ {str50: String(50), str30: String(30)}
)
class MyClass(decl_base):
id: Mapped[str50] = mapped_column(primary_key=True)
data_one: Mapped[str30]
- data_two: Mapped[str50]
- data_three: Mapped[Optional[str30]]
- data_four: Mapped[str3050]
+ data_two: Mapped[opt_str30]
+ data_three: Mapped[str50]
+ data_four: Mapped[opt_str50]
+ data_five: Mapped[str]
+ data_six: Mapped[Optional[str]]
eq_(MyClass.__table__.c.data_one.type.length, 30)
is_false(MyClass.__table__.c.data_one.nullable)
+ eq_(MyClass.__table__.c.data_two.type.length, 30)
+ is_true(MyClass.__table__.c.data_two.nullable)
+ eq_(MyClass.__table__.c.data_three.type.length, 50)
- eq_(MyClass.__table__.c.data_two.type.length, 50)
- is_false(MyClass.__table__.c.data_two.nullable)
+ @testing.variation(
+ "alias_type",
+ [
+ "none",
+ "typekeyword",
+ "typekeyword_unpopulated",
+ "typealias",
+ "typekeyword_nested",
+ ],
+ )
+ @testing.requires.python312
+ def test_extract_pep593_from_pep695(
+ self, decl_base: Type[DeclarativeBase], alias_type
+ ):
+ """test #11130"""
+ if alias_type.typekeyword:
+ decl_base.registry.update_type_annotation_map(
+ {strtypalias_keyword: VARCHAR(33)} # noqa: F821
+ )
+ if alias_type.typekeyword_nested:
+ decl_base.registry.update_type_annotation_map(
+ {strtypalias_keyword_nested: VARCHAR(42)} # noqa: F821
+ )
- eq_(MyClass.__table__.c.data_three.type.length, 30)
- is_true(MyClass.__table__.c.data_three.nullable)
+ class MyClass(decl_base):
+ __tablename__ = "my_table"
- eq_(MyClass.__table__.c.data_four.type.length, 150)
- is_false(MyClass.__table__.c.data_four.nullable)
+ id: Mapped[int] = mapped_column(primary_key=True)
- def test_newtype_missing_from_map(self, decl_base):
- # anno only: global str50
+ if alias_type.typekeyword or alias_type.typekeyword_unpopulated:
+ data_one: Mapped[strtypalias_keyword] # noqa: F821
+ elif alias_type.typealias:
+ data_one: Mapped[strtypalias_ta] # noqa: F821
+ elif alias_type.none:
+ data_one: Mapped[strtypalias_plain] # noqa: F821
+ elif alias_type.typekeyword_nested:
+ data_one: Mapped[strtypalias_keyword_nested] # noqa: F821
+ else:
+ alias_type.fail()
- str50 = NewType("str50", str)
+ table = MyClass.__table__
+ assert table is not None
- if compat.py310:
- text = ".*str50"
+ if alias_type.typekeyword_nested:
+ # a nested annotation is not supported
+ eq_(MyClass.data_one.expression.info, {})
else:
- # NewTypes before 3.10 had a very bad repr
- # <function NewType.<locals>.new_type at 0x...>
- text = ".*NewType.*"
+ eq_(MyClass.data_one.expression.info, {"hi": "there"})
- with expect_deprecated(
- f"Matching the provided NewType '{text}' on its "
- "resolved value without matching it in the "
- "type_annotation_map is deprecated; add this type to the "
- "type_annotation_map to allow it to match explicitly.",
+ if alias_type.typekeyword:
+ eq_(MyClass.data_one.type.length, 33)
+ elif alias_type.typekeyword_nested:
+ eq_(MyClass.data_one.type.length, 42)
+ else:
+ eq_(MyClass.data_one.type.length, None)
+
+ @testing.requires.python312
+ def test_no_recursive_pep593_from_pep695(
+ self, decl_base: Type[DeclarativeBase]
+ ):
+ def declare():
+ class MyClass(decl_base):
+ __tablename__ = "my_table"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+
+ data_one: Mapped[_RecursivePep695Pep593] # noqa: F821
+
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ r"Could not locate SQLAlchemy Core type when resolving for Python "
+ r"type "
+ r"indicated by '_RecursivePep695Pep593' inside the Mapped\[\] "
+ r"annotation for the 'data_one' attribute; none of "
+ r"'_RecursivePep695Pep593', "
+ r"'typing.Annotated\[_TypingStrPep695, .*\]', '_TypingStrPep695' "
+ r"are resolvable by the registry",
):
+ declare()
+
+ @testing.variation("in_map", [True, False])
+ @testing.variation("alias_type", ["plain", "pep695"])
+ @testing.requires.python312
+ def test_generic_typealias_pep593(
+ self, decl_base: Type[DeclarativeBase], alias_type: Variation, in_map
+ ):
+
+ if in_map:
+ decl_base.registry.update_type_annotation_map(
+ {
+ _GenericPep593TypeAlias[str]: VARCHAR(33),
+ _GenericPep593Pep695[str]: VARCHAR(33),
+ }
+ )
- class MyClass(decl_base):
- __tablename__ = "my_table"
+ class MyClass(decl_base):
+ __tablename__ = "my_table"
- id: Mapped[int] = mapped_column(primary_key=True)
- data_one: Mapped[str50]
+ id: Mapped[int] = mapped_column(primary_key=True)
- is_true(isinstance(MyClass.data_one.type, String))
+ if alias_type.plain:
+ data_one: Mapped[_GenericPep593TypeAlias[str]] # noqa: F821
+ elif alias_type.pep695:
+ data_one: Mapped[_GenericPep593Pep695[str]] # noqa: F821
+ else:
+ alias_type.fail()
+
+ eq_(MyClass.data_one.expression.info, {"hi": "there"})
+ if in_map:
+ eq_(MyClass.data_one.expression.type.length, 33)
+ else:
+ eq_(MyClass.data_one.expression.type.length, None)
def test_extract_base_type_from_pep593(
self, decl_base: Type[DeclarativeBase]
eq_(A_1.label.property.columns[0].table, A.__table__)
eq_(A_2.label.property.columns[0].table, A.__table__)
- @testing.variation(
- "union",
- [
- "union",
- ("pep604", requires.python310),
- "union_null",
- ("pep604_null", requires.python310),
- ],
- )
- def test_unions(self, union):
- # anno only: global UnionType
- our_type = Numeric(10, 2)
-
- if union.union:
- UnionType = Union[float, Decimal]
- elif union.union_null:
- UnionType = Union[float, Decimal, None]
- elif union.pep604:
- UnionType = float | Decimal
- elif union.pep604_null:
- UnionType = float | Decimal | None
- else:
- union.fail()
- class Base(DeclarativeBase):
- type_annotation_map = {UnionType: our_type}
+class TypeResolutionTests(fixtures.TestBase, testing.AssertsCompiledSQL):
+ __dialect__ = "default"
- class User(Base):
- __tablename__ = "users"
+ @testing.combinations(
+ (str, types.String),
+ (Decimal, types.Numeric),
+ (float, types.Float),
+ (datetime.datetime, types.DateTime),
+ (uuid.UUID, types.Uuid),
+ argnames="pytype_arg,sqltype",
+ )
+ def test_datatype_lookups(self, decl_base, pytype_arg, sqltype):
+ # anno only: global pytype
+ pytype = pytype_arg
+ class MyClass(decl_base):
+ __tablename__ = "mytable"
id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[Union[float, Decimal]]
- reverse_data: Mapped[Union[Decimal, float]]
-
- optional_data: Mapped[Optional[Union[float, Decimal]]] = (
- mapped_column()
- )
-
- # use Optional directly
- reverse_optional_data: Mapped[Optional[Union[Decimal, float]]] = (
- mapped_column()
- )
-
- # use Union with None, same as Optional but presents differently
- # (Optional object with __origin__ Union vs. Union)
- reverse_u_optional_data: Mapped[Union[Decimal, float, None]] = (
- mapped_column()
- )
-
- refer_union: Mapped[UnionType]
- refer_union_optional: Mapped[Optional[UnionType]]
-
- # py38, 37 does not automatically flatten unions, add extra tests
- # for this. maintain these in order to catch future regressions
- # in the behavior of ``Union``
- unflat_union_optional_data: Mapped[
- Union[Union[Decimal, float, None], None]
- ] = mapped_column()
+ data: Mapped[pytype]
- float_data: Mapped[float] = mapped_column()
- decimal_data: Mapped[Decimal] = mapped_column()
+ assert isinstance(MyClass.__table__.c.data.type, sqltype)
- if compat.py310:
- pep604_data: Mapped[float | Decimal] = mapped_column()
- pep604_reverse: Mapped[Decimal | float] = mapped_column()
- pep604_optional: Mapped[Decimal | float | None] = (
- mapped_column()
- )
- pep604_data_fwd: Mapped["float | Decimal"] = mapped_column()
- pep604_reverse_fwd: Mapped["Decimal | float"] = mapped_column()
- pep604_optional_fwd: Mapped["Decimal | float | None"] = (
- mapped_column()
- )
+ @testing.combinations(
+ (BIGINT(),),
+ (BIGINT,),
+ (Integer().with_variant(BIGINT, "default")),
+ (Integer().with_variant(BIGINT(), "default")),
+ (BIGINT().with_variant(String(), "some_other_dialect")),
+ )
+ def test_type_map_varieties(self, typ):
+ Base = declarative_base(type_annotation_map={int: typ})
- info = [
- ("data", False),
- ("reverse_data", False),
- ("optional_data", True),
- ("reverse_optional_data", True),
- ("reverse_u_optional_data", True),
- ("refer_union", "null" in union.name),
- ("refer_union_optional", True),
- ("unflat_union_optional_data", True),
- ]
- if compat.py310:
- info += [
- ("pep604_data", False),
- ("pep604_reverse", False),
- ("pep604_optional", True),
- ("pep604_data_fwd", False),
- ("pep604_reverse_fwd", False),
- ("pep604_optional_fwd", True),
- ]
+ class MyClass(Base):
+ __tablename__ = "mytable"
- for name, nullable in info:
- col = User.__table__.c[name]
- is_(col.type, our_type, name)
- is_(col.nullable, nullable, name)
+ id: Mapped[int] = mapped_column(primary_key=True)
+ x: Mapped[int]
+ y: Mapped[int] = mapped_column()
+ z: Mapped[int] = mapped_column(typ)
- is_true(isinstance(User.__table__.c.float_data.type, Float))
- ne_(User.__table__.c.float_data.type, our_type)
+ self.assert_compile(
+ CreateTable(MyClass.__table__),
+ "CREATE TABLE mytable (id BIGINT NOT NULL, "
+ "x BIGINT NOT NULL, y BIGINT NOT NULL, z BIGINT NOT NULL, "
+ "PRIMARY KEY (id))",
+ )
- is_true(isinstance(User.__table__.c.decimal_data.type, Numeric))
- ne_(User.__table__.c.decimal_data.type, our_type)
+ def test_dont_ignore_unresolvable(self, decl_base):
+ """test #8888"""
- @testing.variation(
- "union",
- [
- "union",
- ("pep604", requires.python310),
- ("pep695", requires.python312),
- ],
- )
- def test_optional_in_annotation_map(self, union):
- """See issue #11370"""
+ with expect_raises_message(
+ sa_exc.ArgumentError,
+ r"Could not resolve all types within mapped annotation: "
+ r"\".*Mapped\[.*fake.*\]\". Ensure all types are written "
+ r"correctly and are imported within the module in use.",
+ ):
- class Base(DeclarativeBase):
- if union.union:
- type_annotation_map = {_Json: JSON}
- elif union.pep604:
- type_annotation_map = {_JsonPep604: JSON}
- elif union.pep695:
- type_annotation_map = {_JsonPep695: JSON} # noqa: F821
- else:
- union.fail()
+ class A(decl_base):
+ __tablename__ = "a"
- class A(Base):
- __tablename__ = "a"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped["fake"] # noqa
- id: Mapped[int] = mapped_column(primary_key=True)
- if union.union:
- json1: Mapped[_Json]
- json2: Mapped[_Json] = mapped_column(nullable=False)
- elif union.pep604:
- json1: Mapped[_JsonPep604]
- json2: Mapped[_JsonPep604] = mapped_column(nullable=False)
- elif union.pep695:
- json1: Mapped[_JsonPep695] # noqa: F821
- json2: Mapped[_JsonPep695] = mapped_column( # noqa: F821
- nullable=False
- )
- else:
- union.fail()
+ def test_type_dont_mis_resolve_on_superclass(self):
+ """test for #8859.
- is_(A.__table__.c.json1.type._type_affinity, JSON)
- is_(A.__table__.c.json2.type._type_affinity, JSON)
- is_true(A.__table__.c.json1.nullable)
- is_false(A.__table__.c.json2.nullable)
+ For subclasses of a type that's in the map, don't resolve this
+ by default, even though we do a search through __mro__.
- @testing.variation(
- "option",
- [
- "not_optional",
- "optional",
- "optional_fwd_ref",
- "union_none",
- ("pep604", testing.requires.python310),
- ("pep604_fwd_ref", testing.requires.python310),
- ],
- )
- @testing.variation("brackets", ["oneset", "twosets"])
- @testing.combinations(
- "include_mc_type", "derive_from_anno", argnames="include_mc_type"
- )
- def test_optional_styles_nested_brackets(
- self, option, brackets, include_mc_type
- ):
- """composed types test, includes tests that were added later for
- #12207"""
+ """
+ # anno only: global int_sub
- class Base(DeclarativeBase):
- if testing.requires.python310.enabled:
- type_annotation_map = {
- Dict[str, Decimal]: JSON,
- dict[str, Decimal]: JSON,
- Union[List[int], List[str]]: JSON,
- list[int] | list[str]: JSON,
- }
- else:
- type_annotation_map = {
- Dict[str, Decimal]: JSON,
- Union[List[int], List[str]]: JSON,
- }
+ class int_sub(int):
+ pass
- if include_mc_type == "include_mc_type":
- mc = mapped_column(JSON)
- mc2 = mapped_column(JSON)
- else:
- mc = mapped_column()
- mc2 = mapped_column()
+ Base = declarative_base(
+ type_annotation_map={
+ int: Integer,
+ }
+ )
- class A(Base):
- __tablename__ = "a"
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ "Could not locate SQLAlchemy Core type",
+ ):
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[str] = mapped_column()
+ class MyClass(Base):
+ __tablename__ = "mytable"
- if brackets.oneset:
- if option.not_optional:
- json: Mapped[Dict[str, Decimal]] = mapped_column() # type: ignore # noqa: E501
- if testing.requires.python310.enabled:
- json2: Mapped[dict[str, Decimal]] = mapped_column() # type: ignore # noqa: E501
- elif option.optional:
- json: Mapped[Optional[Dict[str, Decimal]]] = mc
- if testing.requires.python310.enabled:
- json2: Mapped[Optional[dict[str, Decimal]]] = mc2
- elif option.optional_fwd_ref:
- json: Mapped["Optional[Dict[str, Decimal]]"] = mc
- if testing.requires.python310.enabled:
- json2: Mapped["Optional[dict[str, Decimal]]"] = mc2
- elif option.union_none:
- json: Mapped[Union[Dict[str, Decimal], None]] = mc
- json2: Mapped[Union[None, Dict[str, Decimal]]] = mc2
- elif option.pep604:
- json: Mapped[dict[str, Decimal] | None] = mc
- if testing.requires.python310.enabled:
- json2: Mapped[None | dict[str, Decimal]] = mc2
- elif option.pep604_fwd_ref:
- json: Mapped["dict[str, Decimal] | None"] = mc
- if testing.requires.python310.enabled:
- json2: Mapped["None | dict[str, Decimal]"] = mc2
- elif brackets.twosets:
- if option.not_optional:
- json: Mapped[Union[List[int], List[str]]] = mapped_column() # type: ignore # noqa: E501
- elif option.optional:
- json: Mapped[Optional[Union[List[int], List[str]]]] = mc
- if testing.requires.python310.enabled:
- json2: Mapped[
- Optional[Union[list[int], list[str]]]
- ] = mc2
- elif option.optional_fwd_ref:
- json: Mapped["Optional[Union[List[int], List[str]]]"] = mc
- if testing.requires.python310.enabled:
- json2: Mapped[
- "Optional[Union[list[int], list[str]]]"
- ] = mc2
- elif option.union_none:
- json: Mapped[Union[List[int], List[str], None]] = mc
- if testing.requires.python310.enabled:
- json2: Mapped[Union[None, list[int], list[str]]] = mc2
- elif option.pep604:
- json: Mapped[list[int] | list[str] | None] = mc
- json2: Mapped[None | list[int] | list[str]] = mc2
- elif option.pep604_fwd_ref:
- json: Mapped["list[int] | list[str] | None"] = mc
- json2: Mapped["None | list[int] | list[str]"] = mc2
- else:
- brackets.fail()
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[int_sub]
- is_(A.__table__.c.json.type._type_affinity, JSON)
- if hasattr(A, "json2"):
- is_(A.__table__.c.json2.type._type_affinity, JSON)
- if option.not_optional:
- is_false(A.__table__.c.json2.nullable)
- else:
- is_true(A.__table__.c.json2.nullable)
+ @testing.variation("in_map", ["yes", "no", "value"])
+ @testing.variation("lookup", ["A", "B", "value"])
+ def test_recursive_pep695_cases(
+ self, decl_base, in_map: Variation, lookup: Variation
+ ):
+ # anno only: global A, B
+ A = TypingTypeAliasType("A", Union[int, float])
+ B = TypingTypeAliasType("B", A)
- if option.not_optional:
- is_false(A.__table__.c.json.nullable)
- else:
- is_true(A.__table__.c.json.nullable)
+ if in_map.yes:
+ decl_base.registry.update_type_annotation_map({A: Numeric(10, 5)})
+ elif in_map.value:
+ decl_base.registry.update_type_annotation_map(
+ {A.__value__: Numeric(10, 5)}
+ )
- @testing.variation("optional", [True, False])
- @testing.variation("provide_type", [True, False])
- @testing.variation("add_to_type_map", [True, False])
- def test_recursive_type(
- self, decl_base, optional, provide_type, add_to_type_map
- ):
- """test #9553"""
+ def declare():
+ class MyClass(decl_base):
+ __tablename__ = "my_table"
+ id: Mapped[int] = mapped_column(primary_key=True)
- global T
+ if lookup.A:
+ data: Mapped[A]
+ elif lookup.B:
+ data: Mapped[B]
+ elif lookup.value:
+ data: Mapped[Union[int, float]]
+ else:
+ lookup.fail()
- T = Dict[str, Optional["T"]]
+ return MyClass
- if not provide_type and not add_to_type_map:
+ if in_map.value and lookup.B:
+ with expect_deprecated(
+ "Matching to pep-695 type 'A' in a recursive fashion"
+ ):
+ MyClass = declare()
+ eq_(MyClass.data.expression.type.precision, 10)
+ elif in_map.no or (in_map.yes and lookup.value):
with expect_raises_message(
- sa_exc.ArgumentError,
- r"Could not locate SQLAlchemy.*" r".*ForwardRef\('T'\).*",
+ orm_exc.MappedAnnotationError,
+ "Could not locate SQLAlchemy Core type when resolving "
+ "for Python type indicated by",
):
+ declare()
+ else:
+ MyClass = declare()
+ eq_(MyClass.data.expression.type.precision, 10)
- class TypeTest(decl_base):
- __tablename__ = "my_table"
+ @testing.variation(
+ "dict_key", ["typing", ("plain", testing.requires.python310)]
+ )
+ def test_type_dont_mis_resolve_on_non_generic(self, dict_key):
+ """test for #8859.
- id: Mapped[int] = mapped_column(primary_key=True)
- if optional:
- type_test: Mapped[Optional[T]] = mapped_column()
- else:
- type_test: Mapped[T] = mapped_column()
+ For a specific generic type with arguments, don't do any MRO
+ lookup.
- return
+ """
- else:
- if add_to_type_map:
- decl_base.registry.update_type_annotation_map({T: JSON()})
+ Base = declarative_base(
+ type_annotation_map={
+ dict: String,
+ }
+ )
- class TypeTest(decl_base):
- __tablename__ = "my_table"
+ with expect_raises_message(
+ sa_exc.ArgumentError, "Could not locate SQLAlchemy Core type"
+ ):
+
+ class MyClass(Base):
+ __tablename__ = "mytable"
id: Mapped[int] = mapped_column(primary_key=True)
- if add_to_type_map:
- if optional:
- type_test: Mapped[Optional[T]] = mapped_column()
- else:
- type_test: Mapped[T] = mapped_column()
- else:
- if optional:
- type_test: Mapped[Optional[T]] = mapped_column(JSON())
- else:
- type_test: Mapped[T] = mapped_column(JSON())
+ if dict_key.plain:
+ data: Mapped[dict[str, str]]
+ elif dict_key.typing:
+ data: Mapped[Dict[str, str]]
- if optional:
- is_(TypeTest.__table__.c.type_test.nullable, True)
- else:
- is_(TypeTest.__table__.c.type_test.nullable, False)
+ def test_type_secondary_resolution(self):
+ class MyString(String):
+ def _resolve_for_python_type(
+ self, python_type, matched_type, matched_on_flattened
+ ):
+ return String(length=42)
- self.assert_compile(
- select(TypeTest),
- "SELECT my_table.id, my_table.type_test FROM my_table",
- )
+ Base = declarative_base(type_annotation_map={str: MyString})
- def test_missing_mapped_lhs(self, decl_base):
- with expect_annotation_syntax_error("User.name"):
+ class MyClass(Base):
+ __tablename__ = "mytable"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[str]
+
+ is_true(isinstance(MyClass.__table__.c.data.type, String))
+ eq_(MyClass.__table__.c.data.type.length, 42)
+
+ def test_construct_lhs_type_missing(self, decl_base):
+ # anno only: global MyClass
+
+ class MyClass:
+ pass
+
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ "Could not locate SQLAlchemy Core type when resolving for Python "
+ r"type indicated by '.*class .*MyClass.*' inside the "
+ r"Mapped\[\] annotation for the 'data' attribute; the type "
+ "object is not resolvable by the registry",
+ ):
class User(decl_base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
- name: str = mapped_column() # type: ignore
+ data: Mapped[MyClass] = mapped_column()
- def test_construct_lhs_separate_name(self, decl_base):
- class User(decl_base):
- __tablename__ = "users"
+ @testing.variation(
+ "argtype",
+ [
+ "type",
+ ("column", testing.requires.python310),
+ ("mapped_column", testing.requires.python310),
+ "column_class",
+ "ref_to_type",
+ ("ref_to_column", testing.requires.python310),
+ ],
+ )
+ def test_construct_lhs_sqlalchemy_type(self, decl_base, argtype):
+ """test for #12329.
- id: Mapped[int] = mapped_column(primary_key=True)
- name: Mapped[str] = mapped_column()
- data: Mapped[Optional[str]] = mapped_column("the_data")
+ of note here are all the different messages we have for when the
+ wrong thing is put into Mapped[], and in fact in #12329 we added
+ another one.
- self.assert_compile(
- select(User.data), "SELECT users.the_data FROM users"
- )
- is_true(User.__table__.c.the_data.nullable)
+ This is a lot of different messages, but at the same time they
+ occur at different places in the interpretation of types. If
+ we were to centralize all these messages, we'd still likely end up
+ doing distinct messages for each scenario, so instead we added
+ a new ArgumentError subclass MappedAnnotationError that provides
+ some commonality to all of these cases.
- def test_construct_works_in_expr(self, decl_base):
- class User(decl_base):
- __tablename__ = "users"
- id: Mapped[int] = mapped_column(primary_key=True)
+ """
+ expect_future_annotations = "annotations" in globals()
+
+ if argtype.type:
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ # properties.py -> _init_column_for_annotation, type is
+ # a SQL type
+ "The type provided inside the 'data' attribute Mapped "
+ "annotation is the SQLAlchemy type .*BigInteger.*. Expected "
+ "a Python type instead",
+ ):
+
+ class User(decl_base):
+ __tablename__ = "users"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[BigInteger] = mapped_column()
+
+ elif argtype.column:
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ # util.py -> _extract_mapped_subtype
+ (
+ re.escape(
+ "Could not interpret annotation "
+ "Mapped[Column('q', BigInteger)]."
+ )
+ if expect_future_annotations
+ # properties.py -> _init_column_for_annotation, object is
+ # not a SQL type or a python type, it's just some object
+ else re.escape(
+ "The object provided inside the 'data' attribute "
+ "Mapped annotation is not a Python type, it's the "
+ "object Column('q', BigInteger(), table=None). "
+ "Expected a Python type."
+ )
+ ),
+ ):
+
+ class User(decl_base):
+ __tablename__ = "users"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[Column("q", BigInteger)] = ( # noqa: F821
+ mapped_column()
+ )
- class Address(decl_base):
- __tablename__ = "addresses"
+ elif argtype.mapped_column:
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ # properties.py -> _init_column_for_annotation, object is
+ # not a SQL type or a python type, it's just some object
+ # interestingly, this raises at the same point for both
+ # future annotations mode and legacy annotations mode
+ r"The object provided inside the 'data' attribute "
+ "Mapped annotation is not a Python type, it's the object "
+ r"\<sqlalchemy.orm.properties.MappedColumn.*\>. "
+ "Expected a Python type.",
+ ):
- id: Mapped[int] = mapped_column(primary_key=True)
- user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
+ class User(decl_base):
+ __tablename__ = "users"
- user = relationship(User, primaryjoin=user_id == User.id)
+ id: Mapped[int] = mapped_column(primary_key=True)
+ big_integer: Mapped[int] = mapped_column()
+ data: Mapped[big_integer] = mapped_column()
- self.assert_compile(
- select(Address.user_id, User.id).join(Address.user),
- "SELECT addresses.user_id, users.id FROM addresses "
- "JOIN users ON addresses.user_id = users.id",
- )
+ elif argtype.column_class:
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ # properties.py -> _init_column_for_annotation, type is not
+ # a SQL type
+ "Could not locate SQLAlchemy Core type when resolving for "
+ "Python type indicated by "
+ r"'.*class .*.Column.*' inside the "
+ r"Mapped\[\] annotation for the 'data' attribute; the "
+ "type object is not resolvable by the registry",
+ ):
- def test_construct_works_as_polymorphic_on(self, decl_base):
- class User(decl_base):
- __tablename__ = "users"
+ class User(decl_base):
+ __tablename__ = "users"
- id: Mapped[int] = mapped_column(primary_key=True)
- type: Mapped[str] = mapped_column()
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[Column] = mapped_column()
- __mapper_args__ = {"polymorphic_on": type}
+ elif argtype.ref_to_type:
+ mytype = BigInteger
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ (
+ # decl_base.py -> _exract_mappable_attributes
+ re.escape(
+ "Could not resolve all types within mapped "
+ 'annotation: "Mapped[mytype]"'
+ )
+ if expect_future_annotations
+ # properties.py -> _init_column_for_annotation, type is
+ # a SQL type
+ else re.escape(
+ "The type provided inside the 'data' attribute Mapped "
+ "annotation is the SQLAlchemy type "
+ "<class 'sqlalchemy.sql.sqltypes.BigInteger'>. "
+ "Expected a Python type instead"
+ )
+ ),
+ ):
- decl_base.registry.configure()
- is_(User.__table__.c.type, User.__mapper__.polymorphic_on)
+ class User(decl_base):
+ __tablename__ = "users"
- def test_construct_works_as_version_id_col(self, decl_base):
- class User(decl_base):
- __tablename__ = "users"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[mytype] = mapped_column()
- id: Mapped[int] = mapped_column(primary_key=True)
- version_id: Mapped[int] = mapped_column()
+ elif argtype.ref_to_column:
+ mycol = Column("q", BigInteger)
- __mapper_args__ = {"version_id_col": version_id}
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ # decl_base.py -> _exract_mappable_attributes
+ (
+ re.escape(
+ "Could not resolve all types within mapped "
+ 'annotation: "Mapped[mycol]"'
+ )
+ if expect_future_annotations
+ else
+ # properties.py -> _init_column_for_annotation, object is
+ # not a SQL type or a python type, it's just some object
+ re.escape(
+ "The object provided inside the 'data' attribute "
+ "Mapped "
+ "annotation is not a Python type, it's the object "
+ "Column('q', BigInteger(), table=None). "
+ "Expected a Python type."
+ )
+ ),
+ ):
- decl_base.registry.configure()
- is_(User.__table__.c.version_id, User.__mapper__.version_id_col)
+ class User(decl_base):
+ __tablename__ = "users"
- def test_construct_works_in_deferred(self, decl_base):
- class User(decl_base):
- __tablename__ = "users"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[mycol] = mapped_column()
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[str] = deferred(mapped_column())
+ else:
+ argtype.fail()
- self.assert_compile(select(User), "SELECT users.id FROM users")
- self.assert_compile(
- select(User).options(undefer(User.data)),
- "SELECT users.id, users.data FROM users",
+ def test_plain_typealias_as_typemap_keys(
+ self, decl_base: Type[DeclarativeBase]
+ ):
+ decl_base.registry.update_type_annotation_map(
+ {_UnionTypeAlias: JSON, _StrTypeAlias: String(30)}
)
- def test_deferred_kw(self, decl_base):
- class User(decl_base):
- __tablename__ = "users"
-
+ class Test(decl_base):
+ __tablename__ = "test"
id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[str] = mapped_column(deferred=True)
+ data: Mapped[_StrTypeAlias]
+ structure: Mapped[_UnionTypeAlias]
- self.assert_compile(select(User), "SELECT users.id FROM users")
- self.assert_compile(
- select(User).options(undefer(User.data)),
- "SELECT users.id, users.data FROM users",
- )
+ eq_(Test.__table__.c.data.type.length, 30)
+ is_(Test.__table__.c.structure.type._type_affinity, JSON)
- @testing.combinations(
- (str, types.String),
- (Decimal, types.Numeric),
- (float, types.Float),
- (datetime.datetime, types.DateTime),
- (uuid.UUID, types.Uuid),
- argnames="pytype_arg,sqltype",
+ @testing.variation(
+ "option",
+ [
+ "plain",
+ "union",
+ "union_604",
+ "union_null",
+ "union_null_604",
+ "optional",
+ "optional_union",
+ "optional_union_604",
+ "union_newtype",
+ "union_null_newtype",
+ "union_695",
+ "union_null_695",
+ ],
)
- def test_datatype_lookups(self, decl_base, pytype_arg, sqltype):
- # anno only: global pytype
- pytype = pytype_arg
+ @testing.variation("in_map", ["yes", "no", "value"])
+ @testing.requires.python312
+ def test_pep695_behavior(self, decl_base, in_map, option):
+ """Issue #11955; later issue #12829"""
- class MyClass(decl_base):
- __tablename__ = "mytable"
- id: Mapped[int] = mapped_column(primary_key=True)
+ # anno only: global tat
- data: Mapped[pytype]
+ if option.plain:
+ tat = TypeAliasType("tat", str)
+ elif option.union:
+ tat = TypeAliasType("tat", Union[str, int])
+ elif option.union_604:
+ tat = TypeAliasType("tat", str | int)
+ elif option.union_null:
+ tat = TypeAliasType("tat", Union[str, int, None])
+ elif option.union_null_604:
+ tat = TypeAliasType("tat", str | int | None)
+ elif option.optional:
+ tat = TypeAliasType("tat", Optional[str])
+ elif option.optional_union:
+ tat = TypeAliasType("tat", Optional[Union[str, int]])
+ elif option.optional_union_604:
+ tat = TypeAliasType("tat", Optional[str | int])
+ elif option.union_newtype:
+ # this seems to be illegal for typing but "works"
+ tat = NewType("tat", Union[str, int])
+ elif option.union_null_newtype:
+ # this seems to be illegal for typing but "works"
+ tat = NewType("tat", Union[str, int, None])
+ elif option.union_695:
+ tat = TypeAliasType("tat", str | int)
+ elif option.union_null_695:
+ tat = TypeAliasType("tat", str | int | None)
+ else:
+ option.fail()
- assert isinstance(MyClass.__table__.c.data.type, sqltype)
+ is_newtype = "newtype" in option.name
+ if in_map.yes:
+ decl_base.registry.update_type_annotation_map({tat: String(99)})
+ elif in_map.value and not is_newtype:
+ decl_base.registry.update_type_annotation_map(
+ {tat.__value__: String(99)}
+ )
- def test_dont_ignore_unresolvable(self, decl_base):
- """test #8888"""
+ def declare():
+ class Test(decl_base):
+ __tablename__ = "test"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[tat]
- with expect_raises_message(
- sa_exc.ArgumentError,
- r"Could not resolve all types within mapped annotation: "
- r"\".*Mapped\[.*fake.*\]\". Ensure all types are written "
- r"correctly and are imported within the module in use.",
- ):
+ return Test.__table__.c.data
+
+ if in_map.yes or (in_map.value and not is_newtype):
+ col = declare()
+ # String(99) inside the type_map
+ is_true(isinstance(col.type, String))
+ eq_(col.type.length, 99)
+ nullable = "null" in option.name or "optional" in option.name
+ eq_(col.nullable, nullable)
+ elif option.plain or option.optional:
+ col = declare()
+ # plain string from default lookup
+ is_true(isinstance(col.type, String))
+ eq_(col.type.length, None)
+ nullable = "null" in option.name or "optional" in option.name
+ eq_(col.nullable, nullable)
+ else:
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ r"Could not locate SQLAlchemy Core type when resolving "
+ r"for Python type "
+ r"indicated by '.*tat' inside the Mapped\[\] "
+ r"annotation for the 'data' attribute;",
+ ):
+ declare()
+ return
+
+ @testing.variation(
+ "type_",
+ [
+ "str_extension",
+ "str_typing",
+ "generic_extension",
+ "generic_typing",
+ "generic_typed_extension",
+ "generic_typed_typing",
+ ],
+ )
+ @testing.requires.python312
+ def test_pep695_typealias_as_typemap_keys(
+ self, decl_base: Type[DeclarativeBase], type_
+ ):
+ """test #10807, #12829"""
+
+ decl_base.registry.update_type_annotation_map(
+ {
+ _UnionPep695: JSON,
+ _StrPep695: String(30),
+ _TypingStrPep695: String(30),
+ _GenericPep695: String(30),
+ _TypingGenericPep695: String(30),
+ _GenericPep695Typed: String(30),
+ _TypingGenericPep695Typed: String(30),
+ }
+ )
- class A(decl_base):
- __tablename__ = "a"
+ class Test(decl_base):
+ __tablename__ = "test"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ if type_.str_extension:
+ data: Mapped[_StrPep695]
+ elif type_.str_typing:
+ data: Mapped[_TypingStrPep695]
+ elif type_.generic_extension:
+ data: Mapped[_GenericPep695]
+ elif type_.generic_typing:
+ data: Mapped[_TypingGenericPep695]
+ elif type_.generic_typed_extension:
+ data: Mapped[_GenericPep695Typed]
+ elif type_.generic_typed_typing:
+ data: Mapped[_TypingGenericPep695Typed]
+ else:
+ type_.fail()
+ structure: Mapped[_UnionPep695]
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped["fake"] # noqa
+ eq_(Test.__table__.c.data.type._type_affinity, String)
+ eq_(Test.__table__.c.data.type.length, 30)
+ is_(Test.__table__.c.structure.type._type_affinity, JSON)
- def test_type_dont_mis_resolve_on_superclass(self):
- """test for #8859.
+ def test_pep484_newtypes_as_typemap_keys(
+ self, decl_base: Type[DeclarativeBase]
+ ):
+ # anno only: global str50, str30, str3050
- For subclasses of a type that's in the map, don't resolve this
- by default, even though we do a search through __mro__.
+ str50 = NewType("str50", str)
+ str30 = NewType("str30", str)
+ str3050 = NewType("str30", str50)
- """
- # anno only: global int_sub
+ decl_base.registry.update_type_annotation_map(
+ {str50: String(50), str30: String(30), str3050: String(150)}
+ )
- class int_sub(int):
- pass
+ class MyClass(decl_base):
+ __tablename__ = "my_table"
- Base = declarative_base(
- type_annotation_map={
- int: Integer,
- }
- )
+ id: Mapped[str50] = mapped_column(primary_key=True)
+ data_one: Mapped[str30]
+ data_two: Mapped[str50]
+ data_three: Mapped[Optional[str30]]
+ data_four: Mapped[str3050]
- with expect_raises_message(
- orm_exc.MappedAnnotationError,
- "Could not locate SQLAlchemy Core type",
- ):
+ eq_(MyClass.__table__.c.data_one.type.length, 30)
+ is_false(MyClass.__table__.c.data_one.nullable)
- class MyClass(Base):
- __tablename__ = "mytable"
+ eq_(MyClass.__table__.c.data_two.type.length, 50)
+ is_false(MyClass.__table__.c.data_two.nullable)
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[int_sub]
+ eq_(MyClass.__table__.c.data_three.type.length, 30)
+ is_true(MyClass.__table__.c.data_three.nullable)
- @testing.variation(
- "dict_key", ["typing", ("plain", testing.requires.python310)]
- )
- def test_type_dont_mis_resolve_on_non_generic(self, dict_key):
- """test for #8859.
+ eq_(MyClass.__table__.c.data_four.type.length, 150)
+ is_false(MyClass.__table__.c.data_four.nullable)
- For a specific generic type with arguments, don't do any MRO
- lookup.
+ def test_newtype_missing_from_map(self, decl_base):
+ # anno only: global str50
- """
+ str50 = NewType("str50", str)
- Base = declarative_base(
- type_annotation_map={
- dict: String,
- }
- )
+ if compat.py310:
+ text = ".*str50"
+ else:
+ # NewTypes before 3.10 had a very bad repr
+ # <function NewType.<locals>.new_type at 0x...>
+ text = ".*NewType.*"
- with expect_raises_message(
- sa_exc.ArgumentError, "Could not locate SQLAlchemy Core type"
+ with expect_deprecated(
+ f"Matching the provided NewType '{text}' on its "
+ "resolved value without matching it in the "
+ "type_annotation_map is deprecated; add this type to the "
+ "type_annotation_map to allow it to match explicitly.",
):
- class MyClass(Base):
- __tablename__ = "mytable"
+ class MyClass(decl_base):
+ __tablename__ = "my_table"
id: Mapped[int] = mapped_column(primary_key=True)
+ data_one: Mapped[str50]
- if dict_key.plain:
- data: Mapped[dict[str, str]]
- elif dict_key.typing:
- data: Mapped[Dict[str, str]]
-
- def test_type_secondary_resolution(self):
- class MyString(String):
- def _resolve_for_python_type(
- self, python_type, matched_type, matched_on_flattened
- ):
- return String(length=42)
-
- Base = declarative_base(type_annotation_map={str: MyString})
-
- class MyClass(Base):
- __tablename__ = "mytable"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[str]
-
- is_true(isinstance(MyClass.__table__.c.data.type, String))
- eq_(MyClass.__table__.c.data.type.length, 42)
+ is_true(isinstance(MyClass.data_one.type, String))
-class EnumOrLiteralTypeMapTest(fixtures.TestBase, testing.AssertsCompiledSQL):
+class ResolveToEnumTest(fixtures.TestBase, testing.AssertsCompiledSQL):
__dialect__ = "default"
@testing.variation("use_explicit_name", [True, False])
is_true(isinstance(Foo.__table__.c.status.type, JSON))
+ @testing.variation(
+ "type_",
+ [
+ "literal",
+ "literal_typing",
+ "recursive",
+ "not_literal",
+ "not_literal_typing",
+ "generic",
+ "generic_typing",
+ "generic_typed",
+ "generic_typed_typing",
+ ],
+ )
+ @testing.combinations(True, False, argnames="in_map")
+ @testing.requires.python312
+ def test_pep695_literal_defaults_to_enum(self, decl_base, type_, in_map):
+ """test #11305."""
+
+ def declare():
+ class Foo(decl_base):
+ __tablename__ = "footable"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ if type_.recursive:
+ status: Mapped[_RecursiveLiteral695] # noqa: F821
+ elif type_.literal:
+ status: Mapped[_Literal695] # noqa: F821
+ elif type_.literal_typing:
+ status: Mapped[_TypingLiteral695] # noqa: F821
+ elif type_.not_literal:
+ status: Mapped[_StrPep695] # noqa: F821
+ elif type_.not_literal_typing:
+ status: Mapped[_TypingStrPep695] # noqa: F821
+ elif type_.generic:
+ status: Mapped[_GenericPep695] # noqa: F821
+ elif type_.generic_typing:
+ status: Mapped[_TypingGenericPep695] # noqa: F821
+ elif type_.generic_typed:
+ status: Mapped[_GenericPep695Typed] # noqa: F821
+ elif type_.generic_typed_typing:
+ status: Mapped[_TypingGenericPep695Typed] # noqa: F821
+ else:
+ type_.fail()
+
+ return Foo
+
+ if in_map:
+ decl_base.registry.update_type_annotation_map(
+ {
+ _Literal695: Enum(enum.Enum), # noqa: F821
+ _TypingLiteral695: Enum(enum.Enum), # noqa: F821
+ _RecursiveLiteral695: Enum(enum.Enum), # noqa: F821
+ _StrPep695: Enum(enum.Enum), # noqa: F821
+ _TypingStrPep695: Enum(enum.Enum), # noqa: F821
+ _GenericPep695: Enum(enum.Enum), # noqa: F821
+ _TypingGenericPep695: Enum(enum.Enum), # noqa: F821
+ _GenericPep695Typed: Enum(enum.Enum), # noqa: F821
+ _TypingGenericPep695Typed: Enum(enum.Enum), # noqa: F821
+ }
+ )
+ if type_.recursive:
+ with expect_deprecated(
+ "Mapping recursive TypeAliasType '.+' that resolve to "
+ "literal to generate an Enum is deprecated. SQLAlchemy "
+ "2.1 will not support this use case. Please avoid using "
+ "recursing TypeAliasType",
+ ):
+ Foo = declare()
+ elif type_.literal or type_.literal_typing:
+ Foo = declare()
+ else:
+ with expect_raises_message(
+ exc.ArgumentError,
+ "Can't associate TypeAliasType '.+' to an Enum "
+ "since it's not a direct alias of a Literal. Only "
+ "aliases in this form `type my_alias = Literal.'a', "
+ "'b'.` are supported when generating Enums.",
+ ):
+ declare()
+ elif type_.literal or type_.literal_typing:
+ Foo = declare()
+ col = Foo.__table__.c.status
+ is_true(isinstance(col.type, Enum))
+ eq_(col.type.enums, ["to-do", "in-progress", "done"])
+ is_(col.type.native_enum, False)
+ elif type_.not_literal or type_.not_literal_typing:
+ Foo = declare()
+ col = Foo.__table__.c.status
+ is_true(isinstance(col.type, String))
+ elif type_.recursive:
+ with expect_deprecated(
+ "Matching to pep-695 type '_Literal695' in a "
+ "recursive fashion "
+ "without the recursed type being present in the "
+ "type_annotation_map is deprecated; add this type or its "
+ "recursed value to the type_annotation_map to allow it to "
+ "match explicitly."
+ ):
+ Foo = declare()
+ else:
+ with expect_raises_message(
+ orm_exc.MappedAnnotationError,
+ r"Could not locate SQLAlchemy Core type when resolving "
+ r"for Python type "
+ r"indicated by '.+' inside the Mapped\[\] "
+ r"annotation for the 'status' attribute",
+ ):
+ declare()
+ return
+
class MixinTest(fixtures.TestBase, testing.AssertsCompiledSQL):
__dialect__ = "default"
mapped_column(), mapped_column(), mapped_column("zip")
)
- def test_extract_from_pep593(self, decl_base):
- # anno only: global Address
-
- @dataclasses.dataclass
- class Address:
- street: str
- state: str
- zip_: str
-
- class User(decl_base):
- __tablename__ = "user"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- name: Mapped[str] = mapped_column()
-
- address: Mapped[Annotated[Address, "foo"]] = composite(
- mapped_column(), mapped_column(), mapped_column("zip")
- )
-
- self.assert_compile(
- select(User),
- 'SELECT "user".id, "user".name, "user".street, '
- '"user".state, "user".zip FROM "user"',
- dialect="default",
- )
-
def test_cls_not_composite_compliant(self, decl_base):
# anno only: global Address