This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# orm/util.py
|
||||
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
|
||||
# Copyright (C) 2005-2023 the SQLAlchemy authors and contributors
|
||||
# <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
@@ -35,7 +35,6 @@ import weakref
|
||||
|
||||
from . import attributes # noqa
|
||||
from . import exc
|
||||
from . import exc as orm_exc
|
||||
from ._typing import _O
|
||||
from ._typing import insp_is_aliased_class
|
||||
from ._typing import insp_is_mapper
|
||||
@@ -43,7 +42,6 @@ from ._typing import prop_is_relationship
|
||||
from .base import _class_to_mapper as _class_to_mapper
|
||||
from .base import _MappedAnnotationBase
|
||||
from .base import _never_set as _never_set # noqa: F401
|
||||
from .base import _none_only_set as _none_only_set # noqa: F401
|
||||
from .base import _none_set as _none_set # noqa: F401
|
||||
from .base import attribute_str as attribute_str # noqa: F401
|
||||
from .base import class_mapper as class_mapper
|
||||
@@ -87,12 +85,14 @@ from ..sql.elements import KeyedColumnElement
|
||||
from ..sql.selectable import FromClause
|
||||
from ..util.langhelpers import MemoizedSlots
|
||||
from ..util.typing import de_stringify_annotation as _de_stringify_annotation
|
||||
from ..util.typing import (
|
||||
de_stringify_union_elements as _de_stringify_union_elements,
|
||||
)
|
||||
from ..util.typing import eval_name_only as _eval_name_only
|
||||
from ..util.typing import fixup_container_fwd_refs
|
||||
from ..util.typing import get_origin
|
||||
from ..util.typing import is_origin_of_cls
|
||||
from ..util.typing import Literal
|
||||
from ..util.typing import Protocol
|
||||
from ..util.typing import typing_get_origin
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ._typing import _EntityType
|
||||
@@ -121,6 +121,7 @@ if typing.TYPE_CHECKING:
|
||||
from ..sql.selectable import Selectable
|
||||
from ..sql.visitors import anon_map
|
||||
from ..util.typing import _AnnotationScanType
|
||||
from ..util.typing import ArgsTypeProcotol
|
||||
|
||||
_T = TypeVar("_T", bound=Any)
|
||||
|
||||
@@ -137,6 +138,7 @@ all_cascades = frozenset(
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
_de_stringify_partial = functools.partial(
|
||||
functools.partial,
|
||||
locals_=util.immutabledict(
|
||||
@@ -161,7 +163,8 @@ class _DeStringifyAnnotation(Protocol):
|
||||
*,
|
||||
str_cleanup_fn: Optional[Callable[[str, str], str]] = None,
|
||||
include_generic: bool = False,
|
||||
) -> Type[Any]: ...
|
||||
) -> Type[Any]:
|
||||
...
|
||||
|
||||
|
||||
de_stringify_annotation = cast(
|
||||
@@ -169,8 +172,27 @@ de_stringify_annotation = cast(
|
||||
)
|
||||
|
||||
|
||||
class _DeStringifyUnionElements(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
cls: Type[Any],
|
||||
annotation: ArgsTypeProcotol,
|
||||
originating_module: str,
|
||||
*,
|
||||
str_cleanup_fn: Optional[Callable[[str, str], str]] = None,
|
||||
) -> Type[Any]:
|
||||
...
|
||||
|
||||
|
||||
de_stringify_union_elements = cast(
|
||||
_DeStringifyUnionElements,
|
||||
_de_stringify_partial(_de_stringify_union_elements),
|
||||
)
|
||||
|
||||
|
||||
class _EvalNameOnly(Protocol):
|
||||
def __call__(self, name: str, module_name: str) -> Any: ...
|
||||
def __call__(self, name: str, module_name: str) -> Any:
|
||||
...
|
||||
|
||||
|
||||
eval_name_only = cast(_EvalNameOnly, _de_stringify_partial(_eval_name_only))
|
||||
@@ -228,7 +250,7 @@ class CascadeOptions(FrozenSet[str]):
|
||||
values.clear()
|
||||
values.discard("all")
|
||||
|
||||
self = super().__new__(cls, values)
|
||||
self = super().__new__(cls, values) # type: ignore
|
||||
self.save_update = "save-update" in values
|
||||
self.delete = "delete" in values
|
||||
self.refresh_expire = "refresh-expire" in values
|
||||
@@ -237,7 +259,9 @@ class CascadeOptions(FrozenSet[str]):
|
||||
self.delete_orphan = "delete-orphan" in values
|
||||
|
||||
if self.delete_orphan and not self.delete:
|
||||
util.warn("The 'delete-orphan' cascade option requires 'delete'.")
|
||||
util.warn(
|
||||
"The 'delete-orphan' cascade " "option requires 'delete'."
|
||||
)
|
||||
return self
|
||||
|
||||
def __repr__(self):
|
||||
@@ -454,7 +478,9 @@ def identity_key(
|
||||
|
||||
E.g.::
|
||||
|
||||
>>> row = engine.execute(text("select * from table where a=1 and b=2")).first()
|
||||
>>> row = engine.execute(\
|
||||
text("select * from table where a=1 and b=2")\
|
||||
).first()
|
||||
>>> identity_key(MyClass, row=row)
|
||||
(<class '__main__.MyClass'>, (1, 2), None)
|
||||
|
||||
@@ -465,7 +491,7 @@ def identity_key(
|
||||
|
||||
.. versionadded:: 1.2 added identity_token
|
||||
|
||||
""" # noqa: E501
|
||||
"""
|
||||
if class_ is not None:
|
||||
mapper = class_mapper(class_)
|
||||
if row is None:
|
||||
@@ -643,9 +669,9 @@ class AliasedClass(
|
||||
|
||||
# find all pairs of users with the same name
|
||||
user_alias = aliased(User)
|
||||
session.query(User, user_alias).join(
|
||||
(user_alias, User.id > user_alias.id)
|
||||
).filter(User.name == user_alias.name)
|
||||
session.query(User, user_alias).\
|
||||
join((user_alias, User.id > user_alias.id)).\
|
||||
filter(User.name == user_alias.name)
|
||||
|
||||
:class:`.AliasedClass` is also capable of mapping an existing mapped
|
||||
class to an entirely new selectable, provided this selectable is column-
|
||||
@@ -669,7 +695,6 @@ class AliasedClass(
|
||||
using :func:`_sa.inspect`::
|
||||
|
||||
from sqlalchemy import inspect
|
||||
|
||||
my_alias = aliased(MyClass)
|
||||
insp = inspect(my_alias)
|
||||
|
||||
@@ -730,16 +755,12 @@ class AliasedClass(
|
||||
insp,
|
||||
alias,
|
||||
name,
|
||||
(
|
||||
with_polymorphic_mappers
|
||||
if with_polymorphic_mappers
|
||||
else mapper.with_polymorphic_mappers
|
||||
),
|
||||
(
|
||||
with_polymorphic_discriminator
|
||||
if with_polymorphic_discriminator is not None
|
||||
else mapper.polymorphic_on
|
||||
),
|
||||
with_polymorphic_mappers
|
||||
if with_polymorphic_mappers
|
||||
else mapper.with_polymorphic_mappers,
|
||||
with_polymorphic_discriminator
|
||||
if with_polymorphic_discriminator is not None
|
||||
else mapper.polymorphic_on,
|
||||
base_alias,
|
||||
use_mapper_path,
|
||||
adapt_on_names,
|
||||
@@ -950,9 +971,9 @@ class AliasedInsp(
|
||||
|
||||
self._weak_entity = weakref.ref(entity)
|
||||
self.mapper = mapper
|
||||
self.selectable = self.persist_selectable = self.local_table = (
|
||||
selectable
|
||||
)
|
||||
self.selectable = (
|
||||
self.persist_selectable
|
||||
) = self.local_table = selectable
|
||||
self.name = name
|
||||
self.polymorphic_on = polymorphic_on
|
||||
self._base_alias = weakref.ref(_base_alias or self)
|
||||
@@ -1047,7 +1068,6 @@ class AliasedInsp(
|
||||
aliased: bool = False,
|
||||
innerjoin: bool = False,
|
||||
adapt_on_names: bool = False,
|
||||
name: Optional[str] = None,
|
||||
_use_mapper_path: bool = False,
|
||||
) -> AliasedClass[_O]:
|
||||
primary_mapper = _class_to_mapper(base)
|
||||
@@ -1068,7 +1088,6 @@ class AliasedInsp(
|
||||
return AliasedClass(
|
||||
base,
|
||||
selectable,
|
||||
name=name,
|
||||
with_polymorphic_mappers=mappers,
|
||||
adapt_on_names=adapt_on_names,
|
||||
with_polymorphic_discriminator=polymorphic_on,
|
||||
@@ -1210,7 +1229,8 @@ class AliasedInsp(
|
||||
self,
|
||||
obj: _CE,
|
||||
key: Optional[str] = None,
|
||||
) -> _CE: ...
|
||||
) -> _CE:
|
||||
...
|
||||
|
||||
else:
|
||||
_orm_adapt_element = _adapt_element
|
||||
@@ -1360,10 +1380,7 @@ class LoaderCriteriaOption(CriteriaOption):
|
||||
def __init__(
|
||||
self,
|
||||
entity_or_base: _EntityType[Any],
|
||||
where_criteria: Union[
|
||||
_ColumnExpressionArgument[bool],
|
||||
Callable[[Any], _ColumnExpressionArgument[bool]],
|
||||
],
|
||||
where_criteria: _ColumnExpressionArgument[bool],
|
||||
loader_only: bool = False,
|
||||
include_aliases: bool = False,
|
||||
propagate_to_loaders: bool = True,
|
||||
@@ -1522,7 +1539,7 @@ GenericAlias = type(List[Any])
|
||||
def _inspect_generic_alias(
|
||||
class_: Type[_O],
|
||||
) -> Optional[Mapper[_O]]:
|
||||
origin = cast("Type[_O]", get_origin(class_))
|
||||
origin = cast("Type[_O]", typing_get_origin(class_))
|
||||
return _inspect_mc(origin)
|
||||
|
||||
|
||||
@@ -1566,7 +1583,7 @@ class Bundle(
|
||||
|
||||
_propagate_attrs: _PropagateAttrsType = util.immutabledict()
|
||||
|
||||
proxy_set = util.EMPTY_SET
|
||||
proxy_set = util.EMPTY_SET # type: ignore
|
||||
|
||||
exprs: List[_ColumnsClauseElement]
|
||||
|
||||
@@ -1579,7 +1596,8 @@ class Bundle(
|
||||
|
||||
bn = Bundle("mybundle", MyClass.x, MyClass.y)
|
||||
|
||||
for row in session.query(bn).filter(bn.c.x == 5).filter(bn.c.y == 4):
|
||||
for row in session.query(bn).filter(
|
||||
bn.c.x == 5).filter(bn.c.y == 4):
|
||||
print(row.mybundle.x, row.mybundle.y)
|
||||
|
||||
:param name: name of the bundle.
|
||||
@@ -1588,7 +1606,7 @@ class Bundle(
|
||||
can be returned as a "single entity" outside of any enclosing tuple
|
||||
in the same manner as a mapped entity.
|
||||
|
||||
""" # noqa: E501
|
||||
"""
|
||||
self.name = self._label = name
|
||||
coerced_exprs = [
|
||||
coercions.expect(
|
||||
@@ -1643,24 +1661,24 @@ class Bundle(
|
||||
|
||||
Nesting of bundles is also supported::
|
||||
|
||||
b1 = Bundle(
|
||||
"b1",
|
||||
Bundle("b2", MyClass.a, MyClass.b),
|
||||
Bundle("b3", MyClass.x, MyClass.y),
|
||||
)
|
||||
b1 = Bundle("b1",
|
||||
Bundle('b2', MyClass.a, MyClass.b),
|
||||
Bundle('b3', MyClass.x, MyClass.y)
|
||||
)
|
||||
|
||||
q = sess.query(b1).filter(b1.c.b2.c.a == 5).filter(b1.c.b3.c.y == 9)
|
||||
q = sess.query(b1).filter(
|
||||
b1.c.b2.c.a == 5).filter(b1.c.b3.c.y == 9)
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`.Bundle.c`
|
||||
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
c: ReadOnlyColumnCollection[str, KeyedColumnElement[Any]]
|
||||
"""An alias for :attr:`.Bundle.columns`."""
|
||||
|
||||
def _clone(self, **kw):
|
||||
def _clone(self):
|
||||
cloned = self.__class__.__new__(self.__class__)
|
||||
cloned.__dict__.update(self.__dict__)
|
||||
return cloned
|
||||
@@ -1721,24 +1739,25 @@ class Bundle(
|
||||
|
||||
from sqlalchemy.orm import Bundle
|
||||
|
||||
|
||||
class DictBundle(Bundle):
|
||||
def create_row_processor(self, query, procs, labels):
|
||||
"Override create_row_processor to return values as dictionaries"
|
||||
'Override create_row_processor to return values as
|
||||
dictionaries'
|
||||
|
||||
def proc(row):
|
||||
return dict(zip(labels, (proc(row) for proc in procs)))
|
||||
|
||||
return dict(
|
||||
zip(labels, (proc(row) for proc in procs))
|
||||
)
|
||||
return proc
|
||||
|
||||
A result from the above :class:`_orm.Bundle` will return dictionary
|
||||
values::
|
||||
|
||||
bn = DictBundle("mybundle", MyClass.data1, MyClass.data2)
|
||||
for row in session.execute(select(bn)).where(bn.c.data1 == "d1"):
|
||||
print(row.mybundle["data1"], row.mybundle["data2"])
|
||||
bn = DictBundle('mybundle', MyClass.data1, MyClass.data2)
|
||||
for row in session.execute(select(bn)).where(bn.c.data1 == 'd1'):
|
||||
print(row.mybundle['data1'], row.mybundle['data2'])
|
||||
|
||||
""" # noqa: E501
|
||||
"""
|
||||
keyed_tuple = result_tuple(labels, [() for l in labels])
|
||||
|
||||
def proc(row: Row[Any]) -> Any:
|
||||
@@ -1921,7 +1940,7 @@ class _ORMJoin(expression.Join):
|
||||
self.onclause,
|
||||
isouter=self.isouter,
|
||||
_left_memo=self._left_memo,
|
||||
_right_memo=other._left_memo._path_registry,
|
||||
_right_memo=other._left_memo,
|
||||
)
|
||||
|
||||
return _ORMJoin(
|
||||
@@ -1964,6 +1983,7 @@ def with_parent(
|
||||
|
||||
stmt = select(Address).where(with_parent(some_user, User.addresses))
|
||||
|
||||
|
||||
The SQL rendered is the same as that rendered when a lazy loader
|
||||
would fire off from the given parent on that attribute, meaning
|
||||
that the appropriate state is taken from the parent object in
|
||||
@@ -1976,7 +1996,9 @@ def with_parent(
|
||||
|
||||
a1 = aliased(Address)
|
||||
a2 = aliased(Address)
|
||||
stmt = select(a1, a2).where(with_parent(u1, User.addresses.of_type(a2)))
|
||||
stmt = select(a1, a2).where(
|
||||
with_parent(u1, User.addresses.of_type(a2))
|
||||
)
|
||||
|
||||
The above use is equivalent to using the
|
||||
:func:`_orm.with_parent.from_entity` argument::
|
||||
@@ -2001,7 +2023,7 @@ def with_parent(
|
||||
|
||||
.. versionadded:: 1.2
|
||||
|
||||
""" # noqa: E501
|
||||
"""
|
||||
prop_t: RelationshipProperty[Any]
|
||||
|
||||
if isinstance(prop, str):
|
||||
@@ -2095,13 +2117,14 @@ def _entity_corresponds_to_use_path_impl(
|
||||
someoption(A).someoption(C.d) # -> fn(A, C) -> False
|
||||
|
||||
a1 = aliased(A)
|
||||
someoption(a1).someoption(A.b) # -> fn(a1, A) -> False
|
||||
someoption(a1).someoption(a1.b) # -> fn(a1, a1) -> True
|
||||
someoption(a1).someoption(A.b) # -> fn(a1, A) -> False
|
||||
someoption(a1).someoption(a1.b) # -> fn(a1, a1) -> True
|
||||
|
||||
wp = with_polymorphic(A, [A1, A2])
|
||||
someoption(wp).someoption(A1.foo) # -> fn(wp, A1) -> False
|
||||
someoption(wp).someoption(wp.A1.foo) # -> fn(wp, wp.A1) -> True
|
||||
|
||||
|
||||
"""
|
||||
if insp_is_aliased_class(given):
|
||||
return (
|
||||
@@ -2128,7 +2151,7 @@ def _entity_isa(given: _InternalEntityType[Any], mapper: Mapper[Any]) -> bool:
|
||||
mapper
|
||||
)
|
||||
elif given.with_polymorphic_mappers:
|
||||
return mapper in given.with_polymorphic_mappers or given.isa(mapper)
|
||||
return mapper in given.with_polymorphic_mappers
|
||||
else:
|
||||
return given.isa(mapper)
|
||||
|
||||
@@ -2210,7 +2233,7 @@ def _cleanup_mapped_str_annotation(
|
||||
|
||||
inner: Optional[Match[str]]
|
||||
|
||||
mm = re.match(r"^([^ \|]+?)\[(.+)\]$", annotation)
|
||||
mm = re.match(r"^(.+?)\[(.+)\]$", annotation)
|
||||
|
||||
if not mm:
|
||||
return annotation
|
||||
@@ -2250,7 +2273,7 @@ def _cleanup_mapped_str_annotation(
|
||||
while True:
|
||||
stack.append(real_symbol if mm is inner else inner.group(1))
|
||||
g2 = inner.group(2)
|
||||
inner = re.match(r"^([^ \|]+?)\[(.+)\]$", g2)
|
||||
inner = re.match(r"^(.+?)\[(.+)\]$", g2)
|
||||
if inner is None:
|
||||
stack.append(g2)
|
||||
break
|
||||
@@ -2272,10 +2295,8 @@ def _cleanup_mapped_str_annotation(
|
||||
# ['Mapped', "'Optional[Dict[str, str]]'"]
|
||||
not re.match(r"""^["'].*["']$""", stack[-1])
|
||||
# avoid further generics like Dict[] such as
|
||||
# ['Mapped', 'dict[str, str] | None'],
|
||||
# ['Mapped', 'list[int] | list[str]'],
|
||||
# ['Mapped', 'Union[list[int], list[str]]'],
|
||||
and not re.search(r"[\[\]]", stack[-1])
|
||||
# ['Mapped', 'dict[str, str] | None']
|
||||
and not re.match(r".*\[.*\]", stack[-1])
|
||||
):
|
||||
stripchars = "\"' "
|
||||
stack[-1] = ", ".join(
|
||||
@@ -2297,7 +2318,7 @@ def _extract_mapped_subtype(
|
||||
is_dataclass_field: bool,
|
||||
expect_mapped: bool = True,
|
||||
raiseerr: bool = True,
|
||||
) -> Optional[Tuple[Union[_AnnotationScanType, str], Optional[type]]]:
|
||||
) -> Optional[Tuple[Union[type, str], Optional[type]]]:
|
||||
"""given an annotation, figure out if it's ``Mapped[something]`` and if
|
||||
so, return the ``something`` part.
|
||||
|
||||
@@ -2307,7 +2328,7 @@ def _extract_mapped_subtype(
|
||||
|
||||
if raw_annotation is None:
|
||||
if required:
|
||||
raise orm_exc.MappedAnnotationError(
|
||||
raise sa_exc.ArgumentError(
|
||||
f"Python typing annotation is required for attribute "
|
||||
f'"{cls.__name__}.{key}" when primary argument(s) for '
|
||||
f'"{attr_cls.__name__}" construct are None or not present'
|
||||
@@ -2315,11 +2336,6 @@ def _extract_mapped_subtype(
|
||||
return None
|
||||
|
||||
try:
|
||||
# destringify the "outside" of the annotation. note we are not
|
||||
# adding include_generic so it will *not* dig into generic contents,
|
||||
# which will remain as ForwardRef or plain str under future annotations
|
||||
# mode. The full destringify happens later when mapped_column goes
|
||||
# to do a full lookup in the registry type_annotations_map.
|
||||
annotated = de_stringify_annotation(
|
||||
cls,
|
||||
raw_annotation,
|
||||
@@ -2327,14 +2343,14 @@ def _extract_mapped_subtype(
|
||||
str_cleanup_fn=_cleanup_mapped_str_annotation,
|
||||
)
|
||||
except _CleanupError as ce:
|
||||
raise orm_exc.MappedAnnotationError(
|
||||
raise sa_exc.ArgumentError(
|
||||
f"Could not interpret annotation {raw_annotation}. "
|
||||
"Check that it uses names that are correctly imported at the "
|
||||
"module level. See chained stack trace for more hints."
|
||||
) from ce
|
||||
except NameError as ne:
|
||||
if raiseerr and "Mapped[" in raw_annotation: # type: ignore
|
||||
raise orm_exc.MappedAnnotationError(
|
||||
raise sa_exc.ArgumentError(
|
||||
f"Could not interpret annotation {raw_annotation}. "
|
||||
"Check that it uses names that are correctly imported at the "
|
||||
"module level. See chained stack trace for more hints."
|
||||
@@ -2363,7 +2379,7 @@ def _extract_mapped_subtype(
|
||||
):
|
||||
return None
|
||||
|
||||
raise orm_exc.MappedAnnotationError(
|
||||
raise sa_exc.ArgumentError(
|
||||
f'Type annotation for "{cls.__name__}.{key}" '
|
||||
"can't be correctly interpreted for "
|
||||
"Annotated Declarative Table form. ORM annotations "
|
||||
@@ -2384,20 +2400,8 @@ def _extract_mapped_subtype(
|
||||
return annotated, None
|
||||
|
||||
if len(annotated.__args__) != 1:
|
||||
raise orm_exc.MappedAnnotationError(
|
||||
raise sa_exc.ArgumentError(
|
||||
"Expected sub-type for Mapped[] annotation"
|
||||
)
|
||||
|
||||
return (
|
||||
# fix dict/list/set args to be ForwardRef, see #11814
|
||||
fixup_container_fwd_refs(annotated.__args__[0]),
|
||||
annotated.__origin__,
|
||||
)
|
||||
|
||||
|
||||
def _mapper_property_as_plain_name(prop: Type[Any]) -> str:
|
||||
if hasattr(prop, "_mapper_property_name"):
|
||||
name = prop._mapper_property_name()
|
||||
else:
|
||||
name = None
|
||||
return util.clsname_as_plain_name(prop, name)
|
||||
return annotated.__args__[0], annotated.__origin__
|
||||
|
||||
Reference in New Issue
Block a user