summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2022-07-15 12:53:37 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2022-07-16 16:28:11 -0400
commit9a37ebdf99d0e3fcca53ca8f00f101fe475a1b01 (patch)
tree112107d938b959316253b3988132a2109947889f /lib/sqlalchemy
parent6104c163eb58e35e46b0bb6a237e824ec1ee1d15 (diff)
downloadsqlalchemy-9a37ebdf99d0e3fcca53ca8f00f101fe475a1b01.tar.gz
update ORM declarative docs for new features
I screwed up a rebase or something so this was temporarily in Ic51a12de3358f3a451bd7cf3542b375569499fc1 Change-Id: I847ee1336381221c0112b67854df022edf596b25
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/ext/mutable.py80
-rw-r--r--lib/sqlalchemy/orm/__init__.py12
-rw-r--r--lib/sqlalchemy/orm/_orm_constructors.py338
-rw-r--r--lib/sqlalchemy/orm/base.py12
-rw-r--r--lib/sqlalchemy/orm/decl_api.py434
-rw-r--r--lib/sqlalchemy/orm/mapped_collection.py19
-rw-r--r--lib/sqlalchemy/orm/mapper.py12
-rw-r--r--lib/sqlalchemy/orm/properties.py16
-rw-r--r--lib/sqlalchemy/testing/requirements.py6
9 files changed, 476 insertions, 453 deletions
diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py
index 6c26c9266..1d6cf8a85 100644
--- a/lib/sqlalchemy/ext/mutable.py
+++ b/lib/sqlalchemy/ext/mutable.py
@@ -236,19 +236,19 @@ a geometric "point", and is introduced in :ref:`mapper_composite`.
As is the case with :class:`.Mutable`, the user-defined composite class
subclasses :class:`.MutableComposite` as a mixin, and detects and delivers
change events to its parents via the :meth:`.MutableComposite.changed` method.
-In the case of a composite class, the detection is usually via the usage of
-Python descriptors (i.e. ``@property``), or alternatively via the special
-Python method ``__setattr__()``. Below we expand upon the ``Point`` class
-introduced in :ref:`mapper_composite` to subclass :class:`.MutableComposite`
-and to also route attribute set events via ``__setattr__`` to the
-:meth:`.MutableComposite.changed` method::
+In the case of a composite class, the detection is usually via the usage of the
+special Python method ``__setattr__()``. In the example below, we expand upon the ``Point``
+class introduced in :ref:`mapper_composite` to include
+:class:`.MutableComposite` in its bases and to route attribute set events via
+``__setattr__`` to the :meth:`.MutableComposite.changed` method::
+ import dataclasses
from sqlalchemy.ext.mutable import MutableComposite
+ @dataclasses.dataclass
class Point(MutableComposite):
- def __init__(self, x, y):
- self.x = x
- self.y = y
+ x: int
+ y: int
def __setattr__(self, key, value):
"Intercept set events"
@@ -259,16 +259,6 @@ and to also route attribute set events via ``__setattr__`` to the
# alert all parents to the change
self.changed()
- def __composite_values__(self):
- return self.x, self.y
-
- def __eq__(self, other):
- return isinstance(other, Point) and \
- other.x == self.x and \
- other.y == self.y
-
- def __ne__(self, other):
- return not self.__eq__(other)
The :class:`.MutableComposite` class makes use of class mapping events to
automatically establish listeners for any usage of :func:`_orm.composite` that
@@ -276,38 +266,45 @@ specifies our ``Point`` type. Below, when ``Point`` is mapped to the ``Vertex``
class, listeners are established which will route change events from ``Point``
objects to each of the ``Vertex.start`` and ``Vertex.end`` attributes::
- from sqlalchemy.orm import composite, mapper
- from sqlalchemy import Table, Column
-
- vertices = Table('vertices', metadata,
- Column('id', Integer, primary_key=True),
- Column('x1', Integer),
- Column('y1', Integer),
- Column('x2', Integer),
- Column('y2', Integer),
- )
+ from sqlalchemy.orm import DeclarativeBase, Mapped
+ from sqlalchemy.orm import composite, mapped_column
- class Vertex:
+ class Base(DeclarativeBase):
pass
- mapper(Vertex, vertices, properties={
- 'start': composite(Point, vertices.c.x1, vertices.c.y1),
- 'end': composite(Point, vertices.c.x2, vertices.c.y2)
- })
+
+ class Vertex(Base):
+ __tablename__ = "vertices"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+
+ start: Mapped[Point] = composite(mapped_column("x1"), mapped_column("y1"))
+ end: Mapped[Point] = composite(mapped_column("x2"), mapped_column("y2"))
+
+ def __repr__(self):
+ return f"Vertex(start={self.start}, end={self.end})"
Any in-place changes to the ``Vertex.start`` or ``Vertex.end`` members
-will flag the attribute as "dirty" on the parent object::
+will flag the attribute as "dirty" on the parent object:
- >>> from sqlalchemy.orm import Session
+.. sourcecode:: python+sql
- >>> sess = Session()
+ >>> from sqlalchemy.orm import Session
+ >>> sess = Session(engine)
>>> v1 = Vertex(start=Point(3, 4), end=Point(12, 15))
>>> sess.add(v1)
- >>> sess.commit()
+ {sql}>>> sess.flush()
+ BEGIN (implicit)
+ INSERT INTO vertices (x1, y1, x2, y2) VALUES (?, ?, ?, ?)
+ [...] (3, 4, 12, 15)
- >>> v1.end.x = 8
+ {stop}>>> v1.end.x = 8
>>> assert v1 in sess.dirty
True
+ {sql}>>> sess.commit()
+ UPDATE vertices SET x2=? WHERE vertices.id = ?
+ [...] (8, 1)
+ COMMIT
Coercing Mutable Composites
---------------------------
@@ -319,6 +316,7 @@ Overriding the :meth:`.MutableBase.coerce` method is essentially equivalent
to using a :func:`.validates` validation routine for all attributes which
make use of the custom composite type::
+ @dataclasses.dataclass
class Point(MutableComposite):
# other Point methods
# ...
@@ -341,6 +339,7 @@ to define a ``__getstate__`` that doesn't include the ``_parents`` dictionary.
Below we define both a ``__getstate__`` and a ``__setstate__`` that package up
the minimal form of our ``Point`` class::
+ @dataclasses.dataclass
class Point(MutableComposite):
# ...
@@ -354,7 +353,8 @@ As with :class:`.Mutable`, the :class:`.MutableComposite` augments the
pickling process of the parent's object-relational state so that the
:meth:`MutableBase._parents` collection is restored to all ``Point`` objects.
-"""
+""" # noqa: E501
+
from collections import defaultdict
import weakref
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py
index 539cf2600..cda58d6a5 100644
--- a/lib/sqlalchemy/orm/__init__.py
+++ b/lib/sqlalchemy/orm/__init__.py
@@ -87,10 +87,14 @@ from .interfaces import PropComparator as PropComparator
from .interfaces import UserDefinedOption as UserDefinedOption
from .loading import merge_frozen_result as merge_frozen_result
from .loading import merge_result as merge_result
-from .mapped_collection import attribute_mapped_collection
-from .mapped_collection import column_mapped_collection
-from .mapped_collection import mapped_collection
-from .mapped_collection import MappedCollection
+from .mapped_collection import (
+ attribute_mapped_collection as attribute_mapped_collection,
+)
+from .mapped_collection import (
+ column_mapped_collection as column_mapped_collection,
+)
+from .mapped_collection import mapped_collection as mapped_collection
+from .mapped_collection import MappedCollection as MappedCollection
from .mapper import configure_mappers as configure_mappers
from .mapper import Mapper as Mapper
from .mapper import reconstructor as reconstructor
diff --git a/lib/sqlalchemy/orm/_orm_constructors.py b/lib/sqlalchemy/orm/_orm_constructors.py
index ea95f1420..fe87d19dc 100644
--- a/lib/sqlalchemy/orm/_orm_constructors.py
+++ b/lib/sqlalchemy/orm/_orm_constructors.py
@@ -115,7 +115,9 @@ def mapped_column(
Union[bool, Literal[SchemaConst.NULL_UNSPECIFIED]]
] = SchemaConst.NULL_UNSPECIFIED,
primary_key: Optional[bool] = False,
- deferred: bool = False,
+ deferred: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ deferred_group: Optional[str] = None,
+ deferred_raiseload: bool = False,
name: Optional[str] = None,
type_: Optional[_TypeEngineArgument[Any]] = None,
autoincrement: Union[bool, Literal["auto", "ignore_fk"]] = "auto",
@@ -133,137 +135,37 @@ def mapped_column(
comment: Optional[str] = None,
**dialect_kwargs: Any,
) -> MappedColumn[Any]:
- r"""construct a new ORM-mapped :class:`_schema.Column` construct.
+ r"""declare a new ORM-mapped :class:`_schema.Column` construct
+ for use within :ref:`Declarative Table <orm_declarative_table>`
+ configuration.
The :func:`_orm.mapped_column` function provides an ORM-aware and
Python-typing-compatible construct which is used with
:ref:`declarative <orm_declarative_mapping>` mappings to indicate an
attribute that's mapped to a Core :class:`_schema.Column` object. It
provides the equivalent feature as mapping an attribute to a
- :class:`_schema.Column` object directly when using declarative.
+ :class:`_schema.Column` object directly when using Declarative,
+ specifically when using :ref:`Declarative Table <orm_declarative_table>`
+ configuration.
.. versionadded:: 2.0
:func:`_orm.mapped_column` is normally used with explicit typing along with
- the :class:`_orm.Mapped` mapped attribute type, where it can derive the SQL
- type and nullability for the column automatically, such as::
-
- from typing import Optional
-
- from sqlalchemy.orm import Mapped
- from sqlalchemy.orm import mapped_column
-
- class User(Base):
- __tablename__ = 'user'
-
- id: Mapped[int] = mapped_column(primary_key=True)
- name: Mapped[str] = mapped_column()
- options: Mapped[Optional[str]] = mapped_column()
-
- In the above example, the ``int`` and ``str`` types are inferred by the
- Declarative mapping system to indicate use of the :class:`_types.Integer`
- and :class:`_types.String` datatypes, and the presence of ``Optional`` or
- not indicates whether or not each non-primary-key column is to be
- ``nullable=True`` or ``nullable=False``.
-
- The above example, when interpreted within a Declarative class, will result
- in a table named ``"user"`` which is equivalent to the following::
-
- from sqlalchemy import Integer
- from sqlalchemy import String
- from sqlalchemy import Table
-
- Table(
- 'user',
- Base.metadata,
- Column("id", Integer, primary_key=True),
- Column("name", String, nullable=False),
- Column("options", String, nullable=True),
- )
-
- The :func:`_orm.mapped_column` construct accepts the same arguments as
- that of :class:`_schema.Column` directly, including optional "name"
- and "type" fields, so the above mapping can be stated more explicitly
- as::
+ the :class:`_orm.Mapped` annotation type, where it can derive the SQL
+ type and nullability for the column based on what's present within the
+ :class:`_orm.Mapped` annotation. It also may be used without annotations
+ as a drop-in replacement for how :class:`_schema.Column` is used in
+ Declarative mappings in SQLAlchemy 1.x style.
- from typing import Optional
+ For usage examples of :func:`_orm.mapped_column`, see the documentation
+ at :ref:`orm_declarative_table`.
- from sqlalchemy import Integer
- from sqlalchemy import String
- from sqlalchemy.orm import Mapped
- from sqlalchemy.orm import mapped_column
-
- class User(Base):
- __tablename__ = 'user'
+ .. seealso::
- id: Mapped[int] = mapped_column("id", Integer, primary_key=True)
- name: Mapped[str] = mapped_column("name", String, nullable=False)
- options: Mapped[Optional[str]] = mapped_column(
- "name", String, nullable=True
- )
+ :ref:`orm_declarative_table` - complete documentation
- Arguments passed to :func:`_orm.mapped_column` always supersede those which
- would be derived from the type annotation and/or attribute name. To state
- the above mapping with more specific datatypes for ``id`` and ``options``,
- and a different column name for ``name``, looks like::
-
- from sqlalchemy import BigInteger
-
- class User(Base):
- __tablename__ = 'user'
-
- id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True)
- name: Mapped[str] = mapped_column("user_name")
- options: Mapped[Optional[str]] = mapped_column(String(50))
-
- Where again, datatypes and nullable parameters that can be automatically
- derived may be omitted.
-
- The datatypes passed to :class:`_orm.Mapped` are mapped to SQL
- :class:`_types.TypeEngine` types with the following default mapping::
-
- _type_map = {
- int: Integer(),
- float: Float(),
- bool: Boolean(),
- decimal.Decimal: Numeric(),
- dt.date: Date(),
- dt.datetime: DateTime(),
- dt.time: Time(),
- dt.timedelta: Interval(),
- util.NoneType: NULLTYPE,
- bytes: LargeBinary(),
- str: String(),
- }
-
- The above mapping may be expanded to include any combination of Python
- datatypes to SQL types by using the
- :paramref:`_orm.registry.type_annotation_map` parameter to
- :class:`_orm.registry`, or as the attribute ``type_annotation_map`` upon
- the :class:`_orm.DeclarativeBase` base class.
-
- Finally, :func:`_orm.mapped_column` is implicitly used by the Declarative
- mapping system for any :class:`_orm.Mapped` annotation that has no
- attribute value set up. This is much in the way that Python dataclasses
- allow the ``field()`` construct to be optional, only needed when additional
- parameters should be associated with the field. Using this functionality,
- our original mapping can be stated even more succinctly as::
-
- from typing import Optional
-
- from sqlalchemy.orm import Mapped
- from sqlalchemy.orm import mapped_column
-
- class User(Base):
- __tablename__ = 'user'
-
- id: Mapped[int] = mapped_column(primary_key=True)
- name: Mapped[str]
- options: Mapped[Optional[str]]
-
- Above, the ``name`` and ``options`` columns will be evaluated as
- ``Column("name", String, nullable=False)`` and
- ``Column("options", String, nullable=True)``, respectively.
+ :ref:`whatsnew_20_orm_declarative_typing` - migration notes for
+ Declarative mappings using 1.x style mappings
:param __name: String name to give to the :class:`_schema.Column`. This
is an optional, positional only argument that if present must be the
@@ -293,17 +195,54 @@ def mapped_column(
ORM declarative process, and is not part of the :class:`_schema.Column`
itself; instead, it indicates that this column should be "deferred" for
loading as though mapped by :func:`_orm.deferred`.
- :param default: This keyword argument, if present, is passed along to the
- :class:`_schema.Column` constructor as the value of the
- :paramref:`_schema.Column.default` parameter. However, as
- :paramref:`_orm.mapped_column.default` is also consumed as a dataclasses
- directive, the :paramref:`_orm.mapped_column.insert_default` parameter
- should be used instead in a dataclasses context.
+
+ .. seealso::
+
+ :ref:`deferred`
+
+ :param deferred_group: Implies :paramref:`_orm.mapped_column.deferred`
+ to ``True``, and set the :paramref:`_orm.deferred.group` parameter.
+ :param deferred_raiseload: Implies :paramref:`_orm.mapped_column.deferred`
+ to ``True``, and set the :paramref:`_orm.deferred.raiseload` parameter.
+
+ :param default: Passed directly to the
+ :paramref:`_schema.Column.default` parameter if the
+ :paramref:`_orm.mapped_column.insert_default` parameter is not present.
+ Additionally, when used with :ref:`orm_declarative_native_dataclasses`,
+ indicates a default Python value that should be applied to the keyword
+ constructor within the generated ``__init__()`` method.
+
+ Note that in the case of dataclass generation when
+ :paramref:`_orm.mapped_column.insert_default` is not present, this means
+ the :paramref:`_orm.mapped_column.default` value is used in **two**
+ places, both the ``__init__()`` method as well as the
+ :paramref:`_schema.Column.default` parameter. While this behavior may
+ change in a future release, for the moment this tends to "work out"; a
+ default of ``None`` will mean that the :class:`_schema.Column` gets no
+ default generator, whereas a default that refers to a non-``None`` Python
+ or SQL expression value will be assigned up front on the object when
+ ``__init__()`` is called, which is the same value that the Core
+ :class:`_sql.Insert` construct would use in any case, leading to the same
+ end result.
+
:param insert_default: Passed directly to the
:paramref:`_schema.Column.default` parameter; will supersede the value
of :paramref:`_orm.mapped_column.default` when present, however
:paramref:`_orm.mapped_column.default` will always apply to the
constructor default for a dataclasses mapping.
+
+ :param init: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the mapped attribute should be part of the ``__init__()``
+ method as generated by the dataclass process.
+ :param repr: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the mapped attribute should be part of the ``__repr__()``
+ method as generated by the dataclass process.
+ :param default_factory: Specific to
+ :ref:`orm_declarative_native_dataclasses`,
+ specifies a default-value generation function that will take place
+ as part of the ``__init__()``
+ method as generated by the dataclass process.
+
:param \**kw: All remaining keyword argments are passed through to the
constructor for the :class:`_schema.Column`.
@@ -341,6 +280,8 @@ def mapped_column(
comment=comment,
system=system,
deferred=deferred,
+ deferred_group=deferred_group,
+ deferred_raiseload=deferred_raiseload,
**dialect_kwargs,
)
@@ -569,6 +510,18 @@ def composite(
:param info: Optional data dictionary which will be populated into the
:attr:`.MapperProperty.info` attribute of this object.
+ :param init: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the mapped attribute should be part of the ``__init__()``
+ method as generated by the dataclass process.
+ :param repr: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the mapped attribute should be part of the ``__repr__()``
+ method as generated by the dataclass process.
+ :param default_factory: Specific to
+ :ref:`orm_declarative_native_dataclasses`,
+ specifies a default-value generation function that will take place
+ as part of the ``__init__()``
+ method as generated by the dataclass process.
+
"""
if __kw:
raise _no_kw()
@@ -825,87 +778,50 @@ def relationship(
"""Provide a relationship between two mapped classes.
This corresponds to a parent-child or associative table relationship.
- The constructed class is an instance of
- :class:`.Relationship`.
-
- A typical :func:`_orm.relationship`, used in a classical mapping::
-
- mapper(Parent, properties={
- 'children': relationship(Child)
- })
-
- Some arguments accepted by :func:`_orm.relationship`
- optionally accept a
- callable function, which when called produces the desired value.
- The callable is invoked by the parent :class:`_orm.Mapper` at "mapper
- initialization" time, which happens only when mappers are first used,
- and is assumed to be after all mappings have been constructed. This
- can be used to resolve order-of-declaration and other dependency
- issues, such as if ``Child`` is declared below ``Parent`` in the same
- file::
-
- mapper(Parent, properties={
- "children":relationship(lambda: Child,
- order_by=lambda: Child.id)
- })
-
- When using the :ref:`declarative_toplevel` extension, the Declarative
- initializer allows string arguments to be passed to
- :func:`_orm.relationship`. These string arguments are converted into
- callables that evaluate the string as Python code, using the
- Declarative class-registry as a namespace. This allows the lookup of
- related classes to be automatic via their string name, and removes the
- need for related classes to be imported into the local module space
- before the dependent classes have been declared. It is still required
- that the modules in which these related classes appear are imported
- anywhere in the application at some point before the related mappings
- are actually used, else a lookup error will be raised when the
- :func:`_orm.relationship`
- attempts to resolve the string reference to the
- related class. An example of a string- resolved class is as
- follows::
-
- from sqlalchemy.ext.declarative import declarative_base
-
- Base = declarative_base()
-
- class Parent(Base):
- __tablename__ = 'parent'
- id = Column(Integer, primary_key=True)
- children = relationship("Child", order_by="Child.id")
+ The constructed class is an instance of :class:`.Relationship`.
.. seealso::
- :ref:`relationship_config_toplevel` - Full introductory and
- reference documentation for :func:`_orm.relationship`.
+ :ref:`tutorial_orm_related_objects` - tutorial introduction
+ to :func:`_orm.relationship` in the :ref:`unified_tutorial`
- :ref:`tutorial_orm_related_objects` - ORM tutorial introduction.
+ :ref:`relationship_config_toplevel` - narrative documentation
:param argument:
- A mapped class, or actual :class:`_orm.Mapper` instance,
- representing
- the target of the relationship.
+ This parameter refers to the class that is to be related. It
+ accepts several forms, including a direct reference to the target
+ class itself, the :class:`_orm.Mapper` instance for the target class,
+ a Python callable / lambda that will return a reference to the
+ class or :class:`_orm.Mapper` when called, and finally a string
+ name for the class, which will be resolved from the
+ :class:`_orm.registry` in use in order to locate the class, e.g.::
- :paramref:`_orm.relationship.argument`
- may also be passed as a callable
- function which is evaluated at mapper initialization time, and may
- be passed as a string name when using Declarative.
+ class SomeClass(Base):
+ # ...
- .. warning:: Prior to SQLAlchemy 1.3.16, this value is interpreted
- using Python's ``eval()`` function.
- **DO NOT PASS UNTRUSTED INPUT TO THIS STRING**.
- See :ref:`declarative_relationship_eval` for details on
- declarative evaluation of :func:`_orm.relationship` arguments.
+ related = relationship("RelatedClass")
+
+ The :paramref:`_orm.relationship.argument` may also be omitted from the
+ :func:`_orm.relationship` construct entirely, and instead placed inside
+ a :class:`_orm.Mapped` annotation on the left side, which should
+ include a Python collection type if the relationship is expected
+ to be a collection, such as::
+
+ class SomeClass(Base):
+ # ...
- .. versionchanged 1.3.16::
+ related_items: Mapped[List["RelatedItem"]] = relationship()
- The string evaluation of the main "argument" no longer accepts an
- open ended Python expression, instead only accepting a string
- class name or dotted package-qualified name.
+ Or for a many-to-one or one-to-one relationship::
+
+ class SomeClass(Base):
+ # ...
+
+ related_item: Mapped["RelatedItem"] = relationship()
.. seealso::
- :ref:`declarative_configuring_relationships` - further detail
+ :ref:`orm_declarative_properties` - further detail
on relationship configuration when using Declarative.
:param secondary:
@@ -1530,13 +1446,17 @@ def relationship(
A boolean that indicates if this property should be loaded as a
list or a scalar. In most cases, this value is determined
automatically by :func:`_orm.relationship` at mapper configuration
- time, based on the type and direction
+ time. When using explicit :class:`_orm.Mapped` annotations,
+ :paramref:`_orm.relationship.uselist` may be derived from the
+ whether or not the annotation within :class:`_orm.Mapped` contains
+ a collection class.
+ Otherwise, :paramref:`_orm.relationship.uselist` may be derived from
+ the type and direction
of the relationship - one to many forms a list, many to one
forms a scalar, many to many is a list. If a scalar is desired
where normally a list would be present, such as a bi-directional
- one-to-one relationship, set :paramref:`_orm.relationship.uselist`
- to
- False.
+ one-to-one relationship, use an appropriate :class:`_orm.Mapped`
+ annotation or set :paramref:`_orm.relationship.uselist` to False.
The :paramref:`_orm.relationship.uselist`
flag is also available on an
@@ -1552,8 +1472,8 @@ def relationship(
.. seealso::
:ref:`relationships_one_to_one` - Introduction to the "one to
- one" relationship pattern, which is typically when the
- :paramref:`_orm.relationship.uselist` flag is needed.
+ one" relationship pattern, which is typically when an alternate
+ setting for :paramref:`_orm.relationship.uselist` is involved.
:param viewonly=False:
When set to ``True``, the relationship is used only for loading
@@ -1622,6 +1542,19 @@ def relationship(
.. versionadded:: 1.3
+ :param init: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the mapped attribute should be part of the ``__init__()``
+ method as generated by the dataclass process.
+ :param repr: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the mapped attribute should be part of the ``__repr__()``
+ method as generated by the dataclass process.
+ :param default_factory: Specific to
+ :ref:`orm_declarative_native_dataclasses`,
+ specifies a default-value generation function that will take place
+ as part of the ``__init__()``
+ method as generated by the dataclass process.
+
+
"""
return Relationship(
@@ -1927,6 +1860,10 @@ def deferred(
r"""Indicate a column-based mapped attribute that by default will
not load unless accessed.
+ When using :func:`_orm.mapped_column`, the same functionality as
+ that of :func:`_orm.deferred` construct is provided by using the
+ :paramref:`_orm.mapped_column.deferred` parameter.
+
:param \*columns: columns to be mapped. This is typically a single
:class:`_schema.Column` object,
however a collection is supported in order
@@ -1937,9 +1874,6 @@ def deferred(
.. versionadded:: 1.4
- .. seealso::
-
- :ref:`deferred_raiseload`
Additional arguments are the same as that of :func:`_orm.column_property`.
@@ -1947,6 +1881,8 @@ def deferred(
:ref:`deferred`
+ :ref:`deferred_raiseload`
+
"""
return ColumnProperty(
column,
diff --git a/lib/sqlalchemy/orm/base.py b/lib/sqlalchemy/orm/base.py
index 63f873fd0..fa653a472 100644
--- a/lib/sqlalchemy/orm/base.py
+++ b/lib/sqlalchemy/orm/base.py
@@ -714,6 +714,18 @@ class Mapped(ORMDescriptor[_T], roles.TypedColumnsClauseRole[_T], TypingOnly):
checkers such as pylance and mypy so that ORM-mapped attributes
are correctly typed.
+ The most prominent use of :class:`_orm.Mapped` is in
+ the :ref:`Declarative Mapping <orm_explicit_declarative_base>` form
+ of :class:`_orm.Mapper` configuration, where used explicitly it drives
+ the configuration of ORM attributes such as :func:`_orm.mapped_class`
+ and :func:`_orm.relationship`.
+
+ .. seealso::
+
+ :ref:`orm_explicit_declarative_base`
+
+ :ref:`orm_declarative_table`
+
.. tip::
The :class:`_orm.Mapped` class represents attributes that are handled
diff --git a/lib/sqlalchemy/orm/decl_api.py b/lib/sqlalchemy/orm/decl_api.py
index 05d6dacfb..7249698c0 100644
--- a/lib/sqlalchemy/orm/decl_api.py
+++ b/lib/sqlalchemy/orm/decl_api.py
@@ -17,6 +17,7 @@ from typing import Callable
from typing import ClassVar
from typing import Dict
from typing import FrozenSet
+from typing import Generic
from typing import Iterator
from typing import Mapping
from typing import Optional
@@ -79,8 +80,11 @@ if TYPE_CHECKING:
from .interfaces import MapperProperty
from .state import InstanceState # noqa
from ..sql._typing import _TypeEngineArgument
+
_T = TypeVar("_T", bound=Any)
+_TT = TypeVar("_TT", bound=Any)
+
# it's not clear how to have Annotated, Union objects etc. as keys here
# from a typing perspective so just leave it open ended for now
_TypeAnnotationMapType = Mapping[Any, "_TypeEngineArgument[Any]"]
@@ -228,100 +232,10 @@ def synonym_for(
return decorate
-class declared_attr(interfaces._MappedAttribute[_T]):
- """Mark a class-level method as representing the definition of
- a mapped property or special declarative member name.
-
- :class:`_orm.declared_attr` is typically applied as a decorator to a class
- level method, turning the attribute into a scalar-like property that can be
- invoked from the uninstantiated class. The Declarative mapping process
- looks for these :class:`_orm.declared_attr` callables as it scans classes,
- and assumes any attribute marked with :class:`_orm.declared_attr` will be a
- callable that will produce an object specific to the Declarative mapping or
- table configuration.
-
- :class:`_orm.declared_attr` is usually applicable to mixins, to define
- relationships that are to be applied to different implementors of the
- class. It is also used to define :class:`_schema.Column` objects that
- include the :class:`_schema.ForeignKey` construct, as these cannot be
- easily reused across different mappings. The example below illustrates
- both::
-
- class ProvidesUser:
- "A mixin that adds a 'user' relationship to classes."
-
- @declared_attr
- def user_id(self):
- return Column(ForeignKey("user_account.id"))
-
- @declared_attr
- def user(self):
- return relationship("User")
-
- :class:`_orm.declared_attr` can also be applied to mapped classes, such as
- to provide a "polymorphic" scheme for inheritance::
-
- class Employee(Base):
- id = Column(Integer, primary_key=True)
- type = Column(String(50), nullable=False)
-
- @declared_attr
- def __tablename__(cls):
- return cls.__name__.lower()
-
- @declared_attr
- def __mapper_args__(cls):
- if cls.__name__ == 'Employee':
- return {
- "polymorphic_on":cls.type,
- "polymorphic_identity":"Employee"
- }
- else:
- return {"polymorphic_identity":cls.__name__}
-
- To use :class:`_orm.declared_attr` inside of a Python dataclass
- as discussed at :ref:`orm_declarative_dataclasses_declarative_table`,
- it may be placed directly inside the field metadata using a lambda::
-
- @dataclass
- class AddressMixin:
- __sa_dataclass_metadata_key__ = "sa"
-
- user_id: int = field(
- init=False, metadata={"sa": declared_attr(lambda: Column(ForeignKey("user.id")))}
- )
- user: User = field(
- init=False, metadata={"sa": declared_attr(lambda: relationship(User))}
- )
-
- :class:`_orm.declared_attr` also may be omitted from this form using a
- lambda directly, as in::
-
- user: User = field(
- init=False, metadata={"sa": lambda: relationship(User)}
- )
-
- .. seealso::
-
- :ref:`orm_mixins_toplevel` - illustrates how to use Declarative Mixins
- which is the primary use case for :class:`_orm.declared_attr`
-
- :ref:`orm_declarative_dataclasses_mixin` - illustrates special forms
- for use with Python dataclasses
-
- """ # noqa: E501
-
- if typing.TYPE_CHECKING:
-
- def __set__(self, instance: Any, value: Any) -> None:
- ...
-
- def __delete__(self, instance: Any) -> None:
- ...
-
+class _declared_attr_common:
def __init__(
self,
- fn: _DeclaredAttrDecorated[_T],
+ fn: Callable[..., Any],
cascading: bool = False,
):
# suppport
@@ -341,21 +255,7 @@ class declared_attr(interfaces._MappedAttribute[_T]):
def _collect_return_annotation(self) -> Optional[Type[Any]]:
return util.get_annotations(self.fget).get("return")
- # this is the Mapped[] API where at class descriptor get time we want
- # the type checker to see InstrumentedAttribute[_T]. However the
- # callable function prior to mapping in fact calls the given
- # declarative function that does not return InstrumentedAttribute
- @overload
- def __get__(self, instance: None, owner: Any) -> InstrumentedAttribute[_T]:
- ...
-
- @overload
- def __get__(self, instance: object, owner: Any) -> _T:
- ...
-
- def __get__(
- self, instance: Optional[object], owner: Any
- ) -> Union[InstrumentedAttribute[_T], _T]:
+ def __get__(self, instance: Optional[object], owner: Any) -> Any:
# the declared_attr needs to make use of a cache that exists
# for the span of the declarative scan_attributes() phase.
# to achieve this we look at the class manager that's configured.
@@ -394,70 +294,171 @@ class declared_attr(interfaces._MappedAttribute[_T]):
reg[self] = obj = self.fget(cls)
return obj # type: ignore
- @hybridmethod
- def _stateful(cls, **kw: Any) -> _stateful_declared_attr[_T]:
- return _stateful_declared_attr(**kw)
- @hybridproperty
- def cascading(cls) -> _stateful_declared_attr[_T]:
- """Mark a :class:`.declared_attr` as cascading.
-
- This is a special-use modifier which indicates that a column
- or MapperProperty-based declared attribute should be configured
- distinctly per mapped subclass, within a mapped-inheritance scenario.
-
- .. warning::
-
- The :attr:`.declared_attr.cascading` modifier has several
- limitations:
-
- * The flag **only** applies to the use of :class:`.declared_attr`
- on declarative mixin classes and ``__abstract__`` classes; it
- currently has no effect when used on a mapped class directly.
-
- * The flag **only** applies to normally-named attributes, e.g.
- not any special underscore attributes such as ``__tablename__``.
- On these attributes it has **no** effect.
-
- * The flag currently **does not allow further overrides** down
- the class hierarchy; if a subclass tries to override the
- attribute, a warning is emitted and the overridden attribute
- is skipped. This is a limitation that it is hoped will be
- resolved at some point.
-
- Below, both MyClass as well as MySubClass will have a distinct
- ``id`` Column object established::
-
- class HasIdMixin:
- @declared_attr.cascading
- def id(cls):
- if has_inherited_table(cls):
- return Column(
- ForeignKey('myclass.id'), primary_key=True
- )
- else:
- return Column(Integer, primary_key=True)
-
- class MyClass(HasIdMixin, Base):
- __tablename__ = 'myclass'
- # ...
+class _declared_directive(_declared_attr_common, Generic[_T]):
+ # see mapping_api.rst for docstring
- class MySubClass(MyClass):
- ""
- # ...
+ if typing.TYPE_CHECKING:
- The behavior of the above configuration is that ``MySubClass``
- will refer to both its own ``id`` column as well as that of
- ``MyClass`` underneath the attribute named ``some_id``.
+ def __init__(
+ self,
+ fn: Callable[..., _T],
+ cascading: bool = False,
+ ):
+ ...
- .. seealso::
+ def __get__(self, instance: Optional[object], owner: Any) -> _T:
+ ...
+
+ def __set__(self, instance: Any, value: Any) -> None:
+ ...
- :ref:`declarative_inheritance`
+ def __delete__(self, instance: Any) -> None:
+ ...
- :ref:`mixin_inheritance_columns`
+ def __call__(self, fn: Callable[..., _TT]) -> _declared_directive[_TT]:
+ # extensive fooling of mypy underway...
+ ...
- """
+class declared_attr(interfaces._MappedAttribute[_T], _declared_attr_common):
+ """Mark a class-level method as representing the definition of
+ a mapped property or Declarative directive.
+
+ :class:`_orm.declared_attr` is typically applied as a decorator to a class
+ level method, turning the attribute into a scalar-like property that can be
+ invoked from the uninstantiated class. The Declarative mapping process
+ looks for these :class:`_orm.declared_attr` callables as it scans classes,
+ and assumes any attribute marked with :class:`_orm.declared_attr` will be a
+ callable that will produce an object specific to the Declarative mapping or
+ table configuration.
+
+ :class:`_orm.declared_attr` is usually applicable to
+ :ref:`mixins <orm_mixins_toplevel>`, to define relationships that are to be
+ applied to different implementors of the class. It may also be used to
+ define dynamically generated column expressions and other Declarative
+ attributes.
+
+ Example::
+
+ class ProvidesUserMixin:
+ "A mixin that adds a 'user' relationship to classes."
+
+ user_id: Mapped[int] = mapped_column(ForeignKey("user_table.id"))
+
+ @declared_attr
+ def user(cls) -> Mapped["User"]:
+ return relationship("User")
+
+ When used with Declarative directives such as ``__tablename__``, the
+ :meth:`_orm.declared_attr.directive` modifier may be used which indicates
+ to :pep:`484` typing tools that the given method is not dealing with
+ :class:`_orm.Mapped` attributes::
+
+ class CreateTableName:
+ @declared_attr.directive
+ def __tablename__(cls) -> str:
+ return cls.__name__.lower()
+
+ :class:`_orm.declared_attr` can also be applied directly to mapped
+ classes, to allow for attributes that dynamically configure themselves
+ on subclasses when using mapped inheritance schemes. Below
+ illustrates :class:`_orm.declared_attr` to create a dynamic scheme
+ for generating the :paramref:`_orm.Mapper.polymorphic_identity` parameter
+ for subclasses::
+
+ class Employee(Base):
+ __tablename__ = 'employee'
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ type: Mapped[str] = mapped_column(String(50))
+
+ @declared_attr.directive
+ def __mapper_args__(cls) -> dict[str, Any]:
+ if cls.__name__ == 'Employee':
+ return {
+ "polymorphic_on":cls.type,
+ "polymorphic_identity":"Employee"
+ }
+ else:
+ return {"polymorphic_identity":cls.__name__}
+
+ class Engineer(Employee):
+ pass
+
+ :class:`_orm.declared_attr` supports decorating functions that are
+ explicitly decorated with ``@classmethod``. This is never necessary from a
+ runtime perspective, however may be needed in order to support :pep:`484`
+ typing tools that don't otherwise recognize the decorated function as
+ having class-level behaviors for the ``cls`` parameter::
+
+ class SomethingMixin:
+ x: Mapped[int]
+ y: Mapped[int]
+
+ @declared_attr
+ @classmethod
+ def x_plus_y(cls) -> Mapped[int]:
+ return column_property(cls.x + cls.y)
+
+ .. versionadded:: 2.0 - :class:`_orm.declared_attr` can accommodate a
+ function decorated with ``@classmethod`` to help with :pep:`484`
+ integration where needed.
+
+
+ .. seealso::
+
+ :ref:`orm_mixins_toplevel` - Declarative Mixin documentation with
+ background on use patterns for :class:`_orm.declared_attr`.
+
+ """ # noqa: E501
+
+ if typing.TYPE_CHECKING:
+
+ def __init__(
+ self,
+ fn: _DeclaredAttrDecorated[_T],
+ cascading: bool = False,
+ ):
+ ...
+
+ def __set__(self, instance: Any, value: Any) -> None:
+ ...
+
+ def __delete__(self, instance: Any) -> None:
+ ...
+
+ # this is the Mapped[] API where at class descriptor get time we want
+ # the type checker to see InstrumentedAttribute[_T]. However the
+ # callable function prior to mapping in fact calls the given
+ # declarative function that does not return InstrumentedAttribute
+ @overload
+ def __get__(
+ self, instance: None, owner: Any
+ ) -> InstrumentedAttribute[_T]:
+ ...
+
+ @overload
+ def __get__(self, instance: object, owner: Any) -> _T:
+ ...
+
+ def __get__(
+ self, instance: Optional[object], owner: Any
+ ) -> Union[InstrumentedAttribute[_T], _T]:
+ ...
+
+ @hybridmethod
+ def _stateful(cls, **kw: Any) -> _stateful_declared_attr[_T]:
+ return _stateful_declared_attr(**kw)
+
+ @hybridproperty
+ def directive(cls) -> _declared_directive[Any]:
+ # see mapping_api.rst for docstring
+ return _declared_directive # type: ignore
+
+ @hybridproperty
+ def cascading(cls) -> _stateful_declared_attr[_T]:
+ # see mapping_api.rst for docstring
return cls._stateful(cascading=True)
@@ -557,45 +558,17 @@ def _setup_declarative_base(cls: Type[Any]) -> None:
cls.metadata = cls.registry.metadata # type: ignore
-class DeclarativeBaseNoMeta(inspection.Inspectable[Mapper[Any]]):
- """Same as :class:`_orm.DeclarativeBase`, but does not use a metaclass
- to intercept new attributes.
-
- The :class:`_orm.DeclarativeBaseNoMeta` base may be used when use of
- custom metaclasses is desirable.
-
- .. versionadded:: 2.0
-
-
- """
-
- registry: ClassVar[_RegistryType]
- _sa_registry: ClassVar[_RegistryType]
- metadata: ClassVar[MetaData]
- __mapper__: ClassVar[Mapper[Any]]
- __table__: Optional[FromClause]
-
- if typing.TYPE_CHECKING:
-
- def __init__(self, **kw: Any):
- ...
-
- def __init_subclass__(cls) -> None:
- if DeclarativeBaseNoMeta in cls.__bases__:
- _setup_declarative_base(cls)
- else:
- cls._sa_registry.map_declaratively(cls)
-
-
class MappedAsDataclass(metaclass=DCTransformDeclarative):
"""Mixin class to indicate when mapping this class, also convert it to be
a dataclass.
.. seealso::
- :meth:`_orm.registry.mapped_as_dataclass`
+ :ref:`orm_declarative_native_dataclasses` - complete background
+ on SQLAlchemy native dataclass mapping
.. versionadded:: 2.0
+
"""
def __init_subclass__(
@@ -636,7 +609,6 @@ class DeclarativeBase(
):
"""Base class used for declarative class definitions.
-
The :class:`_orm.DeclarativeBase` allows for the creation of new
declarative bases in such a way that is compatible with type checkers::
@@ -689,7 +661,14 @@ class DeclarativeBase(
:paramref:`_orm.registry.type_annotation_map`.
:param registry: supply a pre-existing :class:`_orm.registry` directly.
- .. versionadded:: 2.0
+ .. versionadded:: 2.0 Added :class:`.DeclarativeBase`, so that declarative
+ base classes may be constructed in such a way that is also recognized
+ by :pep:`484` type checkers. As a result, :class:`.DeclarativeBase`
+ and other subclassing-oriented APIs should be seen as
+ superseding previous "class returned by a function" APIs, namely
+ :func:`_orm.declarative_base` and :meth:`_orm.registry.generate_base`,
+ where the base class returned cannot be recognized by type checkers
+ without using plugins.
"""
@@ -698,10 +677,11 @@ class DeclarativeBase(
_sa_registry: ClassVar[_RegistryType]
metadata: ClassVar[MetaData]
+ __name__: ClassVar[str]
__mapper__: ClassVar[Mapper[Any]]
__table__: ClassVar[Optional[FromClause]]
- __tablename__: ClassVar[Optional[str]]
+ __tablename__: ClassVar[Any]
def __init__(self, **kw: Any):
...
@@ -713,6 +693,39 @@ class DeclarativeBase(
_as_declarative(cls._sa_registry, cls, cls.__dict__)
+class DeclarativeBaseNoMeta(inspection.Inspectable[Mapper[Any]]):
+ """Same as :class:`_orm.DeclarativeBase`, but does not use a metaclass
+ to intercept new attributes.
+
+ The :class:`_orm.DeclarativeBaseNoMeta` base may be used when use of
+ custom metaclasses is desirable.
+
+ .. versionadded:: 2.0
+
+
+ """
+
+ if typing.TYPE_CHECKING:
+ registry: ClassVar[_RegistryType]
+ _sa_registry: ClassVar[_RegistryType]
+ metadata: ClassVar[MetaData]
+
+ __name__: ClassVar[str]
+ __mapper__: ClassVar[Mapper[Any]]
+ __table__: ClassVar[Optional[FromClause]]
+
+ __tablename__: ClassVar[Any]
+
+ def __init__(self, **kw: Any):
+ ...
+
+ def __init_subclass__(cls) -> None:
+ if DeclarativeBaseNoMeta in cls.__bases__:
+ _setup_declarative_base(cls)
+ else:
+ cls._sa_registry.map_declaratively(cls)
+
+
def add_mapped_attribute(
target: Type[_O], key: str, attr: MapperProperty[Any]
) -> None:
@@ -751,6 +764,12 @@ def declarative_base(
information provided declaratively in the class and any subclasses
of the class.
+ .. versionchanged:: 2.0 Note that the :func:`_orm.declarative_base`
+ function is superseded by the new :class:`_orm.DeclarativeBase` class,
+ which generates a new "base" class using subclassing, rather than
+ return value of a function. This allows an approach that is compatible
+ with :pep:`484` typing tools.
+
The :func:`_orm.declarative_base` function is a shorthand version
of using the :meth:`_orm.registry.generate_base`
method. That is, the following::
@@ -818,8 +837,13 @@ def declarative_base(
to produce column types based on annotations within the
:class:`_orm.Mapped` type.
+
.. versionadded:: 2.0
+ .. seealso::
+
+ :ref:`orm_declarative_mapped_column_type_map`
+
:param metaclass:
Defaults to :class:`.DeclarativeMeta`. A metaclass or __metaclass__
compatible callable to use as the meta type of the generated
@@ -928,6 +952,11 @@ class registry:
.. versionadded:: 2.0
+ .. seealso::
+
+ :ref:`orm_declarative_mapped_column_type_map`
+
+
"""
lcl_metadata = metadata or MetaData()
@@ -1176,6 +1205,13 @@ class registry:
__init__ = mapper_registry.constructor
+ .. versionchanged:: 2.0 Note that the
+ :meth:`_orm.registry.generate_base` method is superseded by the new
+ :class:`_orm.DeclarativeBase` class, which generates a new "base"
+ class using subclassing, rather than return value of a function.
+ This allows an approach that is compatible with :pep:`484` typing
+ tools.
+
The :meth:`_orm.registry.generate_base` method provides the
implementation for the :func:`_orm.declarative_base` function, which
creates the :class:`_orm.registry` and base class all at once.
@@ -1282,7 +1318,9 @@ class registry:
.. seealso::
- :meth:`_orm.registry.mapped`
+ :ref:`orm_declarative_native_dataclasses` - complete background
+ on SQLAlchemy native dataclass mapping
+
.. versionadded:: 2.0
diff --git a/lib/sqlalchemy/orm/mapped_collection.py b/lib/sqlalchemy/orm/mapped_collection.py
index d1057ca5f..f34083c91 100644
--- a/lib/sqlalchemy/orm/mapped_collection.py
+++ b/lib/sqlalchemy/orm/mapped_collection.py
@@ -167,12 +167,21 @@ def mapped_collection(
class MappedCollection(Dict[_KT, _VT]):
- """A basic dictionary-based collection class.
+ """Base for ORM mapped dictionary classes.
+
+ Extends the ``dict`` type with additional methods needed by SQLAlchemy ORM
+ collection classes. Use of :class:`_orm.MappedCollection` is most directly
+ by using the :func:`.attribute_mapped_collection` or
+ :func:`.column_mapped_collection` class factories.
+ :class:`_orm.MappedCollection` may also serve as the base for user-defined
+ custom dictionary classes.
+
+ .. seealso::
+
+ :ref:`orm_dictionary_collection`
+
+ :ref:`orm_custom_collection`
- Extends dict with the minimal bag semantics that collection
- classes require. ``set`` and ``remove`` are implemented in terms
- of a keying function: any callable that takes an object and
- returns an object for use as a dictionary key.
"""
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 0241a3123..769b1b623 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -353,12 +353,16 @@ class Mapper(
:param exclude_properties: A list or set of string column names to
be excluded from mapping.
- See :ref:`include_exclude_cols` for an example.
+ .. seealso::
+
+ :ref:`include_exclude_cols`
:param include_properties: An inclusive list or set of string column
names to map.
- See :ref:`include_exclude_cols` for an example.
+ .. seealso::
+
+ :ref:`include_exclude_cols`
:param inherits: A mapped class or the corresponding
:class:`_orm.Mapper`
@@ -542,8 +546,8 @@ class Mapper(
)
__mapper_args__ = {
- "polymorphic_on":employee_type,
- "polymorphic_identity":"employee"
+ "polymorphic_on": "employee_type",
+ "polymorphic_identity": "employee"
}
When setting ``polymorphic_on`` to reference an
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index c5f50d7b4..051b6df8b 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -486,11 +486,16 @@ class MappedColumn(
"foreign_keys",
"_has_nullable",
"deferred",
+ "deferred_group",
+ "deferred_raiseload",
"_attribute_options",
"_has_dataclass_arguments",
)
deferred: bool
+ deferred_raiseload: bool
+ deferred_group: Optional[str]
+
column: Column[_T]
foreign_keys: Optional[Set[ForeignKey]]
_attribute_options: _AttributeOptions
@@ -514,7 +519,14 @@ class MappedColumn(
kw["default"] = kw.pop("insert_default", None)
- self.deferred = kw.pop("deferred", False)
+ self.deferred_group = kw.pop("deferred_group", None)
+ self.deferred_raiseload = kw.pop("deferred_raiseload", None)
+ self.deferred = kw.pop("deferred", _NoArg.NO_ARG)
+ if self.deferred is _NoArg.NO_ARG:
+ self.deferred = bool(
+ self.deferred_group or self.deferred_raiseload
+ )
+
self.column = cast("Column[_T]", Column(*arg, **kw))
self.foreign_keys = self.column.foreign_keys
self._has_nullable = "nullable" in kw and kw.get("nullable") not in (
@@ -545,6 +557,8 @@ class MappedColumn(
return ColumnProperty(
self.column,
deferred=True,
+ group=self.deferred_group,
+ raiseload=self.deferred_raiseload,
attribute_options=self._attribute_options,
)
else:
diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py
index c0a2bcf65..cb955ff3d 100644
--- a/lib/sqlalchemy/testing/requirements.py
+++ b/lib/sqlalchemy/testing/requirements.py
@@ -1395,6 +1395,12 @@ class SuiteRequirements(Requirements):
)
@property
+ def python39(self):
+ return exclusions.only_if(
+ lambda: util.py39, "Python 3.9 or above required"
+ )
+
+ @property
def cpython(self):
return exclusions.only_if(
lambda: util.cpython, "cPython interpreter needed"