summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authormike bayer <mike_mp@zzzcomputing.com>2022-11-30 14:04:34 +0000
committerGerrit Code Review <gerrit@ci3.zzzcomputing.com>2022-11-30 14:04:34 +0000
commit1057b47bca2522e45d9621a709d033aa4fb88888 (patch)
tree4ca4cd649b2f4f5f068051a7c2d74815e9d52f1a /lib/sqlalchemy
parent7857a1de32169858367446d11089c34f8daee957 (diff)
parent3e3e3ab0d46b8912649afc7c3eb63b76c19d93fe (diff)
downloadsqlalchemy-1057b47bca2522e45d9621a709d033aa4fb88888.tar.gz
Merge "annotated / DC forms for association proxy" into main
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/ext/associationproxy.py223
-rw-r--r--lib/sqlalchemy/ext/mutable.py47
-rw-r--r--lib/sqlalchemy/orm/_orm_constructors.py3
-rw-r--r--lib/sqlalchemy/orm/decl_base.py102
-rw-r--r--lib/sqlalchemy/orm/interfaces.py38
-rw-r--r--lib/sqlalchemy/orm/relationships.py7
6 files changed, 260 insertions, 160 deletions
diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py
index f4adf3d29..15193e563 100644
--- a/lib/sqlalchemy/ext/associationproxy.py
+++ b/lib/sqlalchemy/ext/associationproxy.py
@@ -19,6 +19,7 @@ import operator
import typing
from typing import AbstractSet
from typing import Any
+from typing import Callable
from typing import cast
from typing import Collection
from typing import Dict
@@ -51,8 +52,12 @@ from ..orm import InspectionAttrExtensionType
from ..orm import interfaces
from ..orm import ORMDescriptor
from ..orm.base import SQLORMOperations
+from ..orm.interfaces import _AttributeOptions
+from ..orm.interfaces import _DCAttributeOptions
+from ..orm.interfaces import _DEFAULT_ATTRIBUTE_OPTIONS
from ..sql import operators
from ..sql import or_
+from ..sql.base import _NoArg
from ..util.typing import Literal
from ..util.typing import Protocol
from ..util.typing import Self
@@ -76,7 +81,20 @@ _VT = TypeVar("_VT", bound=Any)
def association_proxy(
- target_collection: str, attr: str, **kw: Any
+ target_collection: str,
+ attr: str,
+ *,
+ creator: Optional[_CreatorProtocol] = None,
+ getset_factory: Optional[_GetSetFactoryProtocol] = None,
+ proxy_factory: Optional[_ProxyFactoryProtocol] = None,
+ proxy_bulk_set: Optional[_ProxyBulkSetProtocol] = None,
+ info: Optional[_InfoType] = None,
+ cascade_scalar_deletes: bool = False,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ default: Optional[Any] = _NoArg.NO_ARG,
+ default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
) -> AssociationProxy[Any]:
r"""Return a Python property implementing a view of a target
attribute which references an attribute on members of the
@@ -89,47 +107,120 @@ def association_proxy(
the collection type of the target (list, dict or set), or, in the case of
a one to one relationship, a simple scalar value.
- :param target_collection: Name of the attribute we'll proxy to.
- This attribute is typically mapped by
+ :param target_collection: Name of the attribute that is the immediate
+ target. This attribute is typically mapped by
:func:`~sqlalchemy.orm.relationship` to link to a target collection, but
can also be a many-to-one or non-scalar relationship.
- :param attr: Attribute on the associated instance or instances we'll
- proxy for.
+ :param attr: Attribute on the associated instance or instances that
+ are available on instances of the target object.
- For example, given a target collection of [obj1, obj2], a list created
- by this proxy property would look like [getattr(obj1, *attr*),
- getattr(obj2, *attr*)]
+ :param creator: optional.
- If the relationship is one-to-one or otherwise uselist=False, then
- simply: getattr(obj, *attr*)
+ Defines custom behavior when new items are added to the proxied
+ collection.
- :param creator: optional.
+ By default, adding new items to the collection will trigger a
+ construction of an instance of the target object, passing the given
+ item as a positional argument to the target constructor. For cases
+ where this isn't sufficient, :paramref:`.association_proxy.creator`
+ can supply a callable that will construct the object in the
+ appropriate way, given the item that was passed.
+
+ For list- and set- oriented collections, a single argument is
+ passed to the callable. For dictionary oriented collections, two
+ arguments are passed, corresponding to the key and value.
+
+ The :paramref:`.association_proxy.creator` callable is also invoked
+ for scalar (i.e. many-to-one, one-to-one) relationships. If the
+ current value of the target relationship attribute is ``None``, the
+ callable is used to construct a new object. If an object value already
+ exists, the given attribute value is populated onto that object.
+
+ .. seealso::
+
+ :ref:`associationproxy_creator`
+
+ :param cascade_scalar_deletes: when True, indicates that setting
+ the proxied value to ``None``, or deleting it via ``del``, should
+ also remove the source object. Only applies to scalar attributes.
+ Normally, removing the proxied target will not remove the proxy
+ source, as this object may have other state that is still to be
+ kept.
+
+ .. versionadded:: 1.3
+
+ .. seealso::
+
+ :ref:`cascade_scalar_deletes` - complete usage example
+
+ :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.
+
+ .. versionadded:: 2.0.0b4
+
+ :param repr: Specific to :ref:`orm_declarative_native_dataclasses`,
+ specifies if the attribute established by this :class:`.AssociationProxy`
+ should be part of the ``__repr__()`` method as generated by the dataclass
+ process.
+
+ .. versionadded:: 2.0.0b4
+
+ :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.
+
+ .. versionadded:: 2.0.0b4
+
+ :param kw_only: Specific to :ref:`orm_declarative_native_dataclasses`,
+ indicates if this field should be marked as keyword-only when generating
+ the ``__init__()`` method as generated by the dataclass process.
+
+ .. versionadded:: 2.0.0b4
- When new items are added to this proxied collection, new instances of
- the class collected by the target collection will be created. For list
- and set collections, the target class constructor will be called with
- the 'value' for the new instance. For dict types, two arguments are
- passed: key and value.
+ :param info: optional, will be assigned to
+ :attr:`.AssociationProxy.info` if present.
- If you want to construct instances differently, supply a *creator*
- function that takes arguments as above and returns instances.
- For scalar relationships, creator() will be called if the target is None.
- If the target is present, set operations are proxied to setattr() on the
- associated object.
+ The following additional parameters involve injection of custom behaviors
+ within the :class:`.AssociationProxy` object and are for advanced use
+ only:
- If you have an associated object with multiple attributes, you may set
- up multiple association proxies mapping to different attributes. See
- the unit tests for examples, and for examples of how creator() functions
- can be used to construct the scalar relationship on-demand in this
- situation.
+ :param getset_factory: Optional. Proxied attribute access is
+ automatically handled by routines that get and set values based on
+ the `attr` argument for this proxy.
+
+ If you would like to customize this behavior, you may supply a
+ `getset_factory` callable that produces a tuple of `getter` and
+ `setter` functions. The factory is called with two arguments, the
+ abstract type of the underlying collection and this proxy instance.
+
+ :param proxy_factory: Optional. The type of collection to emulate is
+ determined by sniffing the target collection. If your collection
+ type can't be determined by duck typing or you'd like to use a
+ different collection implementation, you may supply a factory
+ function to produce those collections. Only applicable to
+ non-scalar relationships.
+
+ :param proxy_bulk_set: Optional, use with proxy_factory.
- :param \*\*kw: Passes along any other keyword arguments to
- :class:`.AssociationProxy`.
"""
- return AssociationProxy(target_collection, attr, **kw)
+ return AssociationProxy(
+ target_collection,
+ attr,
+ creator=creator,
+ getset_factory=getset_factory,
+ proxy_factory=proxy_factory,
+ proxy_bulk_set=proxy_bulk_set,
+ info=info,
+ cascade_scalar_deletes=cascade_scalar_deletes,
+ attribute_options=_AttributeOptions(
+ init, repr, default, default_factory, kw_only
+ ),
+ )
class AssociationProxyExtensionType(InspectionAttrExtensionType):
@@ -247,6 +338,7 @@ _SelfAssociationProxy = TypeVar(
class AssociationProxy(
interfaces.InspectionAttrInfo,
ORMDescriptor[_T],
+ _DCAttributeOptions,
_AssociationProxyProtocol[_T],
):
"""A descriptor that presents a read/write view of an object attribute."""
@@ -258,73 +350,22 @@ class AssociationProxy(
self,
target_collection: str,
attr: str,
+ *,
creator: Optional[_CreatorProtocol] = None,
getset_factory: Optional[_GetSetFactoryProtocol] = None,
proxy_factory: Optional[_ProxyFactoryProtocol] = None,
proxy_bulk_set: Optional[_ProxyBulkSetProtocol] = None,
info: Optional[_InfoType] = None,
cascade_scalar_deletes: bool = False,
+ attribute_options: Optional[_AttributeOptions] = None,
):
"""Construct a new :class:`.AssociationProxy`.
- The :func:`.association_proxy` function is provided as the usual
- entrypoint here, though :class:`.AssociationProxy` can be instantiated
- and/or subclassed directly.
-
- :param target_collection: Name of the collection we'll proxy to,
- usually created with :func:`_orm.relationship`.
-
- :param attr: Attribute on the collected instances we'll proxy
- for. For example, given a target collection of [obj1, obj2], a
- list created by this proxy property would look like
- [getattr(obj1, attr), getattr(obj2, attr)]
-
- :param creator: Optional. When new items are added to this proxied
- collection, new instances of the class collected by the target
- collection will be created. For list and set collections, the
- target class constructor will be called with the 'value' for the
- new instance. For dict types, two arguments are passed:
- key and value.
-
- If you want to construct instances differently, supply a 'creator'
- function that takes arguments as above and returns instances.
-
- :param cascade_scalar_deletes: when True, indicates that setting
- the proxied value to ``None``, or deleting it via ``del``, should
- also remove the source object. Only applies to scalar attributes.
- Normally, removing the proxied target will not remove the proxy
- source, as this object may have other state that is still to be
- kept.
-
- .. versionadded:: 1.3
-
- .. seealso::
+ The :class:`.AssociationProxy` object is typically constructed using
+ the :func:`.association_proxy` constructor function. See the
+ description of :func:`.association_proxy` for a description of all
+ parameters.
- :ref:`cascade_scalar_deletes` - complete usage example
-
- :param getset_factory: Optional. Proxied attribute access is
- automatically handled by routines that get and set values based on
- the `attr` argument for this proxy.
-
- If you would like to customize this behavior, you may supply a
- `getset_factory` callable that produces a tuple of `getter` and
- `setter` functions. The factory is called with two arguments, the
- abstract type of the underlying collection and this proxy instance.
-
- :param proxy_factory: Optional. The type of collection to emulate is
- determined by sniffing the target collection. If your collection
- type can't be determined by duck typing or you'd like to use a
- different collection implementation, you may supply a factory
- function to produce those collections. Only applicable to
- non-scalar relationships.
-
- :param proxy_bulk_set: Optional, use with proxy_factory. See
- the _set() method for details.
-
- :param info: optional, will be assigned to
- :attr:`.AssociationProxy.info` if present.
-
- .. versionadded:: 1.0.9
"""
self.target_collection = target_collection
@@ -343,6 +384,16 @@ class AssociationProxy(
if info:
self.info = info # type: ignore
+ if (
+ attribute_options
+ and attribute_options != _DEFAULT_ATTRIBUTE_OPTIONS
+ ):
+ self._has_dataclass_arguments = True
+ self._attribute_options = attribute_options
+ else:
+ self._has_dataclass_arguments = False
+ self._attribute_options = _DEFAULT_ATTRIBUTE_OPTIONS
+
@overload
def __get__(
self: _SelfAssociationProxy, instance: Any, owner: Literal[None]
diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py
index f9ed17efc..242f5ee8f 100644
--- a/lib/sqlalchemy/ext/mutable.py
+++ b/lib/sqlalchemy/ext/mutable.py
@@ -111,34 +111,27 @@ Above, :meth:`~.Mutable.as_mutable` returns an instance of ``JSONEncodedDict``
attributes which are mapped against this type. Below we establish a simple
mapping against the ``my_data`` table::
- from sqlalchemy import mapper
+ from sqlalchemy.orm import DeclarativeBase
+ from sqlalchemy.orm import Mapped
+ from sqlalchemy.orm import mapped_column
- class MyDataClass:
+ class Base(DeclarativeBase):
pass
- # associates mutation listeners with MyDataClass.data
- mapper(MyDataClass, my_data)
+ class MyDataClass(Base):
+ __tablename__ = 'my_data'
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[dict[str, str]] = mapped_column(MutableDict.as_mutable(JSONEncodedDict))
The ``MyDataClass.data`` member will now be notified of in place changes
to its value.
-There's no difference in usage when using declarative::
-
- from sqlalchemy.ext.declarative import declarative_base
-
- Base = declarative_base()
-
- class MyDataClass(Base):
- __tablename__ = 'my_data'
- id = Column(Integer, primary_key=True)
- data = Column(MutableDict.as_mutable(JSONEncodedDict))
-
Any in-place changes to the ``MyDataClass.data`` member
will flag the attribute as "dirty" on the parent object::
>>> from sqlalchemy.orm import Session
- >>> sess = Session()
+ >>> sess = Session(some_engine)
>>> m1 = MyDataClass(data={'value1':'foo'})
>>> sess.add(m1)
>>> sess.commit()
@@ -154,12 +147,19 @@ of ``JSONEncodedDict`` in one step, using
of ``MutableDict`` in all mappings unconditionally, without
the need to declare it individually::
+ from sqlalchemy.orm import DeclarativeBase
+ from sqlalchemy.orm import Mapped
+ from sqlalchemy.orm import mapped_column
+
MutableDict.associate_with(JSONEncodedDict)
+ class Base(DeclarativeBase):
+ pass
+
class MyDataClass(Base):
__tablename__ = 'my_data'
- id = Column(Integer, primary_key=True)
- data = Column(JSONEncodedDict)
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[dict[str, str]] = mapped_column(JSONEncodedDict)
Supporting Pickling
@@ -208,15 +208,18 @@ an event when a mutable scalar emits a change event. This event handler
is called when the :func:`.attributes.flag_modified` function is called
from within the mutable extension::
- from sqlalchemy.ext.declarative import declarative_base
+ from sqlalchemy.orm import DeclarativeBase
+ from sqlalchemy.orm import Mapped
+ from sqlalchemy.orm import mapped_column
from sqlalchemy import event
- Base = declarative_base()
+ class Base(DeclarativeBase):
+ pass
class MyDataClass(Base):
__tablename__ = 'my_data'
- id = Column(Integer, primary_key=True)
- data = Column(MutableDict.as_mutable(JSONEncodedDict))
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[dict[str, str]] = mapped_column(MutableDict.as_mutable(JSONEncodedDict))
@event.listens_for(MyDataClass.data, "modified")
def modified_json(instance, initiator):
diff --git a/lib/sqlalchemy/orm/_orm_constructors.py b/lib/sqlalchemy/orm/_orm_constructors.py
index c4abb1c8e..2450d1e83 100644
--- a/lib/sqlalchemy/orm/_orm_constructors.py
+++ b/lib/sqlalchemy/orm/_orm_constructors.py
@@ -29,6 +29,7 @@ from .properties import MappedColumn
from .properties import MappedSQLExpression
from .query import AliasOption
from .relationships import _RelationshipArgumentType
+from .relationships import _RelationshipSecondaryArgument
from .relationships import Relationship
from .relationships import RelationshipProperty
from .session import Session
@@ -736,7 +737,7 @@ def with_loader_criteria(
def relationship(
argument: Optional[_RelationshipArgumentType[Any]] = None,
- secondary: Optional[Union[FromClause, str]] = None,
+ secondary: Optional[_RelationshipSecondaryArgument] = None,
*,
uselist: Optional[bool] = None,
collection_class: Optional[
diff --git a/lib/sqlalchemy/orm/decl_base.py b/lib/sqlalchemy/orm/decl_base.py
index 21e3c3344..1e716e687 100644
--- a/lib/sqlalchemy/orm/decl_base.py
+++ b/lib/sqlalchemy/orm/decl_base.py
@@ -44,6 +44,7 @@ from .base import InspectionAttr
from .descriptor_props import CompositeProperty
from .descriptor_props import SynonymProperty
from .interfaces import _AttributeOptions
+from .interfaces import _DCAttributeOptions
from .interfaces import _IntrospectsAnnotations
from .interfaces import _MappedAttribute
from .interfaces import _MapsColumns
@@ -1262,6 +1263,8 @@ class _ClassScanMapperConfig(_MapperConfig):
or self.is_dataclass_prior_to_mapping
)
+ look_for_dataclass_things = bool(self.dataclass_setup_arguments)
+
for k in list(collected_attributes):
if k in _include_dunders:
@@ -1304,15 +1307,21 @@ class _ClassScanMapperConfig(_MapperConfig):
"accidentally placed at the end of the line?" % k
)
continue
- elif not isinstance(value, (Column, MapperProperty, _MapsColumns)):
+ elif look_for_dataclass_things and isinstance(
+ value, dataclasses.Field
+ ):
+ # we collected a dataclass Field; dataclasses would have
+ # set up the correct state on the class
+ continue
+ elif not isinstance(value, (Column, _DCAttributeOptions)):
# using @declared_attr for some object that
- # isn't Column/MapperProperty; remove from the clsdict_view
+ # isn't Column/MapperProperty/_DCAttributeOptions; remove
+ # from the clsdict_view
# and place the evaluated value onto the class.
- if not k.startswith("__"):
- collected_attributes.pop(k)
- self._warn_for_decl_attributes(cls, k, value)
- if not late_mapped:
- setattr(cls, k, value)
+ collected_attributes.pop(k)
+ self._warn_for_decl_attributes(cls, k, value)
+ if not late_mapped:
+ setattr(cls, k, value)
continue
# we expect to see the name 'metadata' in some valid cases;
# however at this point we see it's assigned to something trying
@@ -1372,38 +1381,59 @@ class _ClassScanMapperConfig(_MapperConfig):
# by util._extract_mapped_subtype before we got here.
assert expect_annotations_wo_mapped
- if (
- isinstance(value, (MapperProperty, _MapsColumns))
- and value._has_dataclass_arguments
- and not self.dataclass_setup_arguments
- ):
- if isinstance(value, MapperProperty):
- argnames = [
- "init",
- "default_factory",
- "repr",
- "default",
- ]
- else:
- argnames = ["init", "default_factory", "repr"]
+ if isinstance(value, _DCAttributeOptions):
+
+ if (
+ value._has_dataclass_arguments
+ and not look_for_dataclass_things
+ ):
+ if isinstance(value, MapperProperty):
+ argnames = [
+ "init",
+ "default_factory",
+ "repr",
+ "default",
+ ]
+ else:
+ argnames = ["init", "default_factory", "repr"]
+
+ args = {
+ a
+ for a in argnames
+ if getattr(
+ value._attribute_options, f"dataclasses_{a}"
+ )
+ is not _NoArg.NO_ARG
+ }
- args = {
- a
- for a in argnames
- if getattr(
- value._attribute_options, f"dataclasses_{a}"
+ raise exc.ArgumentError(
+ f"Attribute '{k}' on class {cls} includes "
+ f"dataclasses argument(s): "
+ f"{', '.join(sorted(repr(a) for a in args))} but "
+ f"class does not specify "
+ "SQLAlchemy native dataclass configuration."
)
- is not _NoArg.NO_ARG
- }
- raise exc.ArgumentError(
- f"Attribute '{k}' on class {cls} includes dataclasses "
- f"argument(s): "
- f"{', '.join(sorted(repr(a) for a in args))} but "
- f"class does not specify "
- "SQLAlchemy native dataclass configuration."
- )
- our_stuff[k] = value
+ if not isinstance(value, (MapperProperty, _MapsColumns)):
+ # filter for _DCAttributeOptions objects that aren't
+ # MapperProperty / mapped_column(). Currently this
+ # includes AssociationProxy. pop it from the things
+ # we're going to map and set it up as a descriptor
+ # on the class.
+ collected_attributes.pop(k)
+
+ # Assoc Prox (or other descriptor object that may
+ # use _DCAttributeOptions) is usually here, except if
+ # 1. we're a
+ # dataclass, dataclasses would have removed the
+ # attr here or 2. assoc proxy is coming from a
+ # superclass, we want it to be direct here so it
+ # tracks state or 3. assoc prox comes from
+ # declared_attr, uncommon case
+ setattr(cls, k, value)
+ continue
+
+ our_stuff[k] = value # type: ignore
def _extract_declared_columns(self) -> None:
our_stuff = self.properties
diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py
index ff003f654..3d2f9708f 100644
--- a/lib/sqlalchemy/orm/interfaces.py
+++ b/lib/sqlalchemy/orm/interfaces.py
@@ -230,7 +230,7 @@ class _AttributeOptions(NamedTuple):
for this attribute.
"""
- if isinstance(elem, (MapperProperty, _MapsColumns)):
+ if isinstance(elem, _DCAttributeOptions):
dc_field = elem._attribute_options._as_dataclass_field()
return (key, annotation, dc_field)
@@ -260,16 +260,36 @@ _DEFAULT_ATTRIBUTE_OPTIONS = _AttributeOptions(
)
-class _MapsColumns(_MappedAttribute[_T]):
- """interface for declarative-capable construct that delivers one or more
- Column objects to the declarative process to be part of a Table.
+class _DCAttributeOptions:
+ """mixin for descriptors or configurational objects that include dataclass
+ field options.
+
+ This includes :class:`.MapperProperty`, :class:`._MapsColumn` within
+ the ORM, but also includes :class:`.AssociationProxy` within ext.
+ Can in theory be used for other descriptors that serve a similar role
+ as association proxy. (*maybe* hybrids, not sure yet.)
+
"""
__slots__ = ()
_attribute_options: _AttributeOptions
+ """behavioral options for ORM-enabled Python attributes
+
+ .. versionadded:: 2.0
+
+ """
+
_has_dataclass_arguments: bool
+
+class _MapsColumns(_DCAttributeOptions, _MappedAttribute[_T]):
+ """interface for declarative-capable construct that delivers one or more
+ Column objects to the declarative process to be part of a Table.
+ """
+
+ __slots__ = ()
+
@property
def mapper_property_to_assign(self) -> Optional[MapperProperty[_T]]:
"""return a MapperProperty to be assigned to the declarative mapping"""
@@ -296,6 +316,7 @@ class _MapsColumns(_MappedAttribute[_T]):
@inspection._self_inspects
class MapperProperty(
HasCacheKey,
+ _DCAttributeOptions,
_MappedAttribute[_T],
InspectionAttrInfo,
util.MemoizedSlots,
@@ -358,13 +379,6 @@ class MapperProperty(
doc: Optional[str]
"""optional documentation string"""
- _attribute_options: _AttributeOptions
- """behavioral options for ORM-enabled Python attributes
-
- .. versionadded:: 2.0
-
- """
-
info: _InfoType
"""Info dictionary associated with the object, allowing user-defined
data to be associated with this :class:`.InspectionAttr`.
@@ -386,8 +400,6 @@ class MapperProperty(
"""
- _has_dataclass_arguments: bool
-
def _memoized_attr_info(self) -> _InfoType:
"""Info dictionary associated with the object, allowing user-defined
data to be associated with this :class:`.InspectionAttr`.
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index 443801b32..73d11e880 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -160,6 +160,9 @@ _LazyLoadArgumentType = Literal[
_RelationshipJoinConditionArgument = Union[
str, _ColumnExpressionArgument[bool]
]
+_RelationshipSecondaryArgument = Union[
+ "FromClause", str, Callable[[], "FromClause"]
+]
_ORMOrderByArgument = Union[
Literal[False],
str,
@@ -269,7 +272,7 @@ class _RelationshipArgs(NamedTuple):
"""
secondary: _RelationshipArg[
- Optional[Union[FromClause, str]],
+ Optional[_RelationshipSecondaryArgument],
Optional[FromClause],
]
primaryjoin: _RelationshipArg[
@@ -352,7 +355,7 @@ class RelationshipProperty(
def __init__(
self,
argument: Optional[_RelationshipArgumentType[_T]] = None,
- secondary: Optional[Union[FromClause, str]] = None,
+ secondary: Optional[_RelationshipSecondaryArgument] = None,
*,
uselist: Optional[bool] = None,
collection_class: Optional[