This commit is contained in:
2025-05-22 20:25:38 +02:00
parent 09f6750c2b
commit ce03fbf12f
529 changed files with 3353 additions and 3312 deletions

View File

@@ -114,7 +114,7 @@ def mapped_column(
primary_key: Optional[bool] = False,
deferred: Union[_NoArg, bool] = _NoArg.NO_ARG,
deferred_group: Optional[str] = None,
deferred_raiseload: bool = False,
deferred_raiseload: Optional[bool] = None,
use_existing_column: bool = False,
name: Optional[str] = None,
type_: Optional[_TypeEngineArgument[Any]] = None,
@@ -132,7 +132,7 @@ def mapped_column(
quote: Optional[bool] = None,
system: bool = False,
comment: Optional[str] = None,
sort_order: int = 0,
sort_order: Union[_NoArg, int] = _NoArg.NO_ARG,
**kw: Any,
) -> MappedColumn[Any]:
r"""declare a new ORM-mapped :class:`_schema.Column` construct

View File

@@ -93,6 +93,7 @@ class _OrmKnownExecutionOptions(_CoreKnownExecutionOptions, total=False):
dml_strategy: DMLStrategyArgument
is_delete_using: bool
is_update_from: bool
render_nulls: bool
OrmExecuteOptionsParameter = Union[
@@ -119,7 +120,7 @@ class _LoaderCallable(Protocol):
def is_orm_option(
opt: ExecutableOption,
) -> TypeGuard[ORMOption]:
return not opt._is_core # type: ignore
return not opt._is_core
def is_user_defined_option(

View File

@@ -120,6 +120,7 @@ if TYPE_CHECKING:
_T = TypeVar("_T")
_T_co = TypeVar("_T_co", bound=Any, covariant=True)
_AllPendingType = Sequence[
@@ -132,10 +133,10 @@ _UNKNOWN_ATTR_KEY = object()
@inspection._self_inspects
class QueryableAttribute(
_DeclarativeMapped[_T],
SQLORMExpression[_T],
_DeclarativeMapped[_T_co],
SQLORMExpression[_T_co],
interfaces.InspectionAttr,
interfaces.PropComparator[_T],
interfaces.PropComparator[_T_co],
roles.JoinTargetRole,
roles.OnClauseRole,
sql_base.Immutable,
@@ -178,13 +179,13 @@ class QueryableAttribute(
is_attribute = True
dispatch: dispatcher[QueryableAttribute[_T]]
dispatch: dispatcher[QueryableAttribute[_T_co]]
class_: _ExternalEntityType[Any]
key: str
parententity: _InternalEntityType[Any]
impl: AttributeImpl
comparator: interfaces.PropComparator[_T]
comparator: interfaces.PropComparator[_T_co]
_of_type: Optional[_InternalEntityType[Any]]
_extra_criteria: Tuple[ColumnElement[bool], ...]
_doc: Optional[str]
@@ -198,7 +199,7 @@ class QueryableAttribute(
class_: _ExternalEntityType[_O],
key: str,
parententity: _InternalEntityType[_O],
comparator: interfaces.PropComparator[_T],
comparator: interfaces.PropComparator[_T_co],
impl: Optional[AttributeImpl] = None,
of_type: Optional[_InternalEntityType[Any]] = None,
extra_criteria: Tuple[ColumnElement[bool], ...] = (),
@@ -314,7 +315,7 @@ class QueryableAttribute(
"""
expression: ColumnElement[_T]
expression: ColumnElement[_T_co]
"""The SQL expression object represented by this
:class:`.QueryableAttribute`.
@@ -334,7 +335,7 @@ class QueryableAttribute(
entity_namespace = self._entity_namespace
assert isinstance(entity_namespace, HasCacheKey)
if self.key is _UNKNOWN_ATTR_KEY: # type: ignore[comparison-overlap]
if self.key is _UNKNOWN_ATTR_KEY:
annotations = {"entity_namespace": entity_namespace}
else:
annotations = {
@@ -376,7 +377,7 @@ class QueryableAttribute(
def _annotations(self) -> _AnnotationDict:
return self.__clause_element__()._annotations
def __clause_element__(self) -> ColumnElement[_T]:
def __clause_element__(self) -> ColumnElement[_T_co]:
return self.expression
@property
@@ -443,18 +444,18 @@ class QueryableAttribute(
extra_criteria=self._extra_criteria,
)
def label(self, name: Optional[str]) -> Label[_T]:
def label(self, name: Optional[str]) -> Label[_T_co]:
return self.__clause_element__().label(name)
def operate(
self, op: OperatorType, *other: Any, **kwargs: Any
) -> ColumnElement[Any]:
return op(self.comparator, *other, **kwargs) # type: ignore[return-value,no-any-return] # noqa: E501
return op(self.comparator, *other, **kwargs) # type: ignore[no-any-return] # noqa: E501
def reverse_operate(
self, op: OperatorType, other: Any, **kwargs: Any
) -> ColumnElement[Any]:
return op(other, self.comparator, **kwargs) # type: ignore[return-value,no-any-return] # noqa: E501
return op(other, self.comparator, **kwargs) # type: ignore[no-any-return] # noqa: E501
def hasparent(
self, state: InstanceState[Any], optimistic: bool = False
@@ -520,16 +521,16 @@ class InstrumentedAttribute(QueryableAttribute[_T]):
# InstrumentedAttribute, while still keeping classlevel
# __doc__ correct
@util.rw_hybridproperty # type: ignore
def __doc__(self) -> Optional[str]: # type: ignore
@util.rw_hybridproperty
def __doc__(self) -> Optional[str]:
return self._doc
@__doc__.setter # type: ignore
def __doc__(self, value: Optional[str]) -> None: # type: ignore
def __doc__(self, value: Optional[str]) -> None:
self._doc = value
@__doc__.classlevel # type: ignore
def __doc__(cls) -> Optional[str]: # type: ignore
def __doc__(cls) -> Optional[str]:
return super().__doc__
def __set__(self, instance: object, value: Any) -> None:
@@ -790,7 +791,7 @@ class AttributeEventToken:
__slots__ = "impl", "op", "parent_token"
def __init__(self, attribute_impl, op):
def __init__(self, attribute_impl: AttributeImpl, op: util.symbol):
self.impl = attribute_impl
self.op = op
self.parent_token = self.impl.parent_token
@@ -833,7 +834,7 @@ class AttributeImpl:
self,
class_: _ExternalEntityType[_O],
key: str,
callable_: _LoaderCallable,
callable_: Optional[_LoaderCallable],
dispatch: _Dispatch[QueryableAttribute[Any]],
trackparent: bool = False,
compare_function: Optional[Callable[..., bool]] = None,
@@ -1940,7 +1941,7 @@ class CollectionAttributeImpl(HasCollectionAdapter, AttributeImpl):
and "None"
or iterable.__class__.__name__
)
wanted = self._duck_typed_as.__name__ # type: ignore
wanted = self._duck_typed_as.__name__
raise TypeError(
"Incompatible collection type: %s is not %s-like"
% (given, wanted)
@@ -2617,7 +2618,7 @@ def register_attribute_impl(
# TODO: this appears to be the WriteOnlyAttributeImpl /
# DynamicAttributeImpl constructor which is hardcoded
impl = cast("Type[WriteOnlyAttributeImpl]", impl_class)(
class_, key, typecallable, dispatch, **kw
class_, key, dispatch, **kw
)
elif uselist:
impl = CollectionAttributeImpl(

View File

@@ -56,6 +56,7 @@ if typing.TYPE_CHECKING:
from ..sql.operators import OperatorType
_T = TypeVar("_T", bound=Any)
_T_co = TypeVar("_T_co", bound=Any, covariant=True)
_O = TypeVar("_O", bound=object)
@@ -678,12 +679,12 @@ class InspectionAttrInfo(InspectionAttr):
return {}
class SQLORMOperations(SQLCoreOperations[_T], TypingOnly):
class SQLORMOperations(SQLCoreOperations[_T_co], TypingOnly):
__slots__ = ()
if typing.TYPE_CHECKING:
def of_type(self, class_: _EntityType[Any]) -> PropComparator[_T]:
def of_type(self, class_: _EntityType[Any]) -> PropComparator[_T_co]:
...
def and_(
@@ -706,7 +707,7 @@ class SQLORMOperations(SQLCoreOperations[_T], TypingOnly):
...
class ORMDescriptor(Generic[_T], TypingOnly):
class ORMDescriptor(Generic[_T_co], TypingOnly):
"""Represent any Python descriptor that provides a SQL expression
construct at the class level."""
@@ -717,26 +718,26 @@ class ORMDescriptor(Generic[_T], TypingOnly):
@overload
def __get__(
self, instance: Any, owner: Literal[None]
) -> ORMDescriptor[_T]:
) -> ORMDescriptor[_T_co]:
...
@overload
def __get__(
self, instance: Literal[None], owner: Any
) -> SQLCoreOperations[_T]:
) -> SQLCoreOperations[_T_co]:
...
@overload
def __get__(self, instance: object, owner: Any) -> _T:
def __get__(self, instance: object, owner: Any) -> _T_co:
...
def __get__(
self, instance: object, owner: Any
) -> Union[ORMDescriptor[_T], SQLCoreOperations[_T], _T]:
) -> Union[ORMDescriptor[_T_co], SQLCoreOperations[_T_co], _T_co]:
...
class _MappedAnnotationBase(Generic[_T], TypingOnly):
class _MappedAnnotationBase(Generic[_T_co], TypingOnly):
"""common class for Mapped and similar ORM container classes.
these are classes that can appear on the left side of an ORM declarative
@@ -749,7 +750,7 @@ class _MappedAnnotationBase(Generic[_T], TypingOnly):
class SQLORMExpression(
SQLORMOperations[_T], SQLColumnExpression[_T], TypingOnly
SQLORMOperations[_T_co], SQLColumnExpression[_T_co], TypingOnly
):
"""A type that may be used to indicate any ORM-level attribute or
object that acts in place of one, in the context of SQL expression
@@ -771,9 +772,9 @@ class SQLORMExpression(
class Mapped(
SQLORMExpression[_T],
ORMDescriptor[_T],
_MappedAnnotationBase[_T],
SQLORMExpression[_T_co],
ORMDescriptor[_T_co],
_MappedAnnotationBase[_T_co],
roles.DDLConstraintColumnRole,
):
"""Represent an ORM mapped attribute on a mapped class.
@@ -819,24 +820,24 @@ class Mapped(
@overload
def __get__(
self, instance: None, owner: Any
) -> InstrumentedAttribute[_T]:
) -> InstrumentedAttribute[_T_co]:
...
@overload
def __get__(self, instance: object, owner: Any) -> _T:
def __get__(self, instance: object, owner: Any) -> _T_co:
...
def __get__(
self, instance: Optional[object], owner: Any
) -> Union[InstrumentedAttribute[_T], _T]:
) -> Union[InstrumentedAttribute[_T_co], _T_co]:
...
@classmethod
def _empty_constructor(cls, arg1: Any) -> Mapped[_T]:
def _empty_constructor(cls, arg1: Any) -> Mapped[_T_co]:
...
def __set__(
self, instance: Any, value: Union[SQLCoreOperations[_T], _T]
self, instance: Any, value: Union[SQLCoreOperations[_T_co], _T_co]
) -> None:
...
@@ -844,7 +845,7 @@ class Mapped(
...
class _MappedAttribute(Generic[_T], TypingOnly):
class _MappedAttribute(Generic[_T_co], TypingOnly):
"""Mixin for attributes which should be replaced by mapper-assigned
attributes.
@@ -853,7 +854,7 @@ class _MappedAttribute(Generic[_T], TypingOnly):
__slots__ = ()
class _DeclarativeMapped(Mapped[_T], _MappedAttribute[_T]):
class _DeclarativeMapped(Mapped[_T_co], _MappedAttribute[_T_co]):
"""Mixin for :class:`.MapperProperty` subclasses that allows them to
be compatible with ORM-annotated declarative mappings.
@@ -878,7 +879,7 @@ class _DeclarativeMapped(Mapped[_T], _MappedAttribute[_T]):
return NotImplemented
class DynamicMapped(_MappedAnnotationBase[_T]):
class DynamicMapped(_MappedAnnotationBase[_T_co]):
"""Represent the ORM mapped attribute type for a "dynamic" relationship.
The :class:`_orm.DynamicMapped` type annotation may be used in an
@@ -918,23 +919,27 @@ class DynamicMapped(_MappedAnnotationBase[_T]):
@overload
def __get__(
self, instance: None, owner: Any
) -> InstrumentedAttribute[_T]:
) -> InstrumentedAttribute[_T_co]:
...
@overload
def __get__(self, instance: object, owner: Any) -> AppenderQuery[_T]:
def __get__(
self, instance: object, owner: Any
) -> AppenderQuery[_T_co]:
...
def __get__(
self, instance: Optional[object], owner: Any
) -> Union[InstrumentedAttribute[_T], AppenderQuery[_T]]:
) -> Union[InstrumentedAttribute[_T_co], AppenderQuery[_T_co]]:
...
def __set__(self, instance: Any, value: typing.Collection[_T]) -> None:
def __set__(
self, instance: Any, value: typing.Collection[_T_co]
) -> None:
...
class WriteOnlyMapped(_MappedAnnotationBase[_T]):
class WriteOnlyMapped(_MappedAnnotationBase[_T_co]):
"""Represent the ORM mapped attribute type for a "write only" relationship.
The :class:`_orm.WriteOnlyMapped` type annotation may be used in an
@@ -970,19 +975,21 @@ class WriteOnlyMapped(_MappedAnnotationBase[_T]):
@overload
def __get__(
self, instance: None, owner: Any
) -> InstrumentedAttribute[_T]:
) -> InstrumentedAttribute[_T_co]:
...
@overload
def __get__(
self, instance: object, owner: Any
) -> WriteOnlyCollection[_T]:
) -> WriteOnlyCollection[_T_co]:
...
def __get__(
self, instance: Optional[object], owner: Any
) -> Union[InstrumentedAttribute[_T], WriteOnlyCollection[_T]]:
) -> Union[InstrumentedAttribute[_T_co], WriteOnlyCollection[_T_co]]:
...
def __set__(self, instance: Any, value: typing.Collection[_T]) -> None:
def __set__(
self, instance: Any, value: typing.Collection[_T_co]
) -> None:
...

View File

@@ -1158,7 +1158,7 @@ class BulkORMInsert(ORMDMLState, InsertDMLState):
execution_options,
) = BulkORMInsert.default_insert_options.from_execution_options(
"_sa_orm_insert_options",
{"dml_strategy", "autoflush", "populate_existing"},
{"dml_strategy", "autoflush", "populate_existing", "render_nulls"},
execution_options,
statement._execution_options,
)
@@ -1438,7 +1438,6 @@ class BulkORMUpdate(BulkUDCompileState, UpdateDMLState):
self._resolved_values = dict(self._resolved_values)
new_stmt = statement._clone()
new_stmt.table = mapper.local_table
# note if the statement has _multi_values, these
# are passed through to the new statement, which will then raise
@@ -1499,9 +1498,7 @@ class BulkORMUpdate(BulkUDCompileState, UpdateDMLState):
# over and over again. so perhaps if it could be RETURNING just
# the elements that were based on a SQL expression and not
# a constant. For now it doesn't quite seem worth it
new_stmt = new_stmt.return_defaults(
*(list(mapper.local_table.primary_key))
)
new_stmt = new_stmt.return_defaults(*new_stmt.table.primary_key)
if toplevel:
new_stmt = self._setup_orm_returning(
@@ -1860,7 +1857,6 @@ class BulkORMDelete(BulkUDCompileState, DeleteDMLState):
)
new_stmt = statement._clone()
new_stmt.table = mapper.local_table
new_crit = cls._adjust_for_extra_criteria(
self.global_attributes, mapper

View File

@@ -253,7 +253,7 @@ class _declared_attr_common:
# which seems to help typing tools interpret the fn as a classmethod
# for situations where needed
if isinstance(fn, classmethod):
fn = fn.__func__ # type: ignore
fn = fn.__func__
self.fget = fn
self._cascading = cascading
@@ -281,11 +281,11 @@ class _declared_attr_common:
"Unmanaged access of declarative attribute %s from "
"non-mapped class %s" % (self.fget.__name__, cls.__name__)
)
return self.fget(cls) # type: ignore
return self.fget(cls)
elif manager.is_mapped:
# the class is mapped, which means we're outside of the declarative
# scan setup, just run the function.
return self.fget(cls) # type: ignore
return self.fget(cls)
# here, we are inside of the declarative scan. use the registry
# that is tracking the values of these attributes.
@@ -297,10 +297,10 @@ class _declared_attr_common:
reg = declarative_scan.declared_attr_reg
if self in reg:
return reg[self] # type: ignore
return reg[self]
else:
reg[self] = obj = self.fget(cls)
return obj # type: ignore
return obj
class _declared_directive(_declared_attr_common, Generic[_T]):
@@ -558,12 +558,12 @@ def _setup_declarative_base(cls: Type[Any]) -> None:
reg = registry(
metadata=metadata, type_annotation_map=type_annotation_map
)
cls.registry = reg # type: ignore
cls.registry = reg
cls._sa_registry = reg # type: ignore
cls._sa_registry = reg
if "metadata" not in cls.__dict__:
cls.metadata = cls.registry.metadata # type: ignore
cls.metadata = cls.registry.metadata
if getattr(cls, "__init__", object.__init__) is object.__init__:
cls.__init__ = cls.registry.constructor
@@ -609,7 +609,7 @@ class MappedAsDataclass(metaclass=DCTransformDeclarative):
current_transforms: _DataclassArguments
if hasattr(cls, "_sa_apply_dc_transforms"):
current = cls._sa_apply_dc_transforms # type: ignore[attr-defined]
current = cls._sa_apply_dc_transforms
_ClassScanMapperConfig._assert_dc_arguments(current)
@@ -1274,7 +1274,7 @@ class registry:
sql_type = sqltypes._type_map_get(pt) # type: ignore # noqa: E501
if sql_type is not None:
sql_type_inst = sqltypes.to_instance(sql_type) # type: ignore
sql_type_inst = sqltypes.to_instance(sql_type)
# ... this additional step will reject most
# type -> supertype matches, such as if we had
@@ -1556,7 +1556,7 @@ class registry:
if hasattr(cls, "__class_getitem__"):
def __class_getitem__(cls: Type[_T], key: str) -> Type[_T]:
def __class_getitem__(cls: Type[_T], key: Any) -> Type[_T]:
# allow generic classes in py3.9+
return cls

View File

@@ -1434,9 +1434,9 @@ class _ClassScanMapperConfig(_MapperConfig):
cls, "_sa_decl_prepare_nocascade", strict=True
)
allow_unmapped_annotations = self.allow_unmapped_annotations
expect_annotations_wo_mapped = (
self.allow_unmapped_annotations
or self.is_dataclass_prior_to_mapping
allow_unmapped_annotations or self.is_dataclass_prior_to_mapping
)
look_for_dataclass_things = bool(self.dataclass_setup_arguments)
@@ -1531,7 +1531,15 @@ class _ClassScanMapperConfig(_MapperConfig):
# Mapped[] etc. were not used. If annotation is None,
# do declarative_scan so that the property can raise
# for required
if mapped_container is not None or annotation is None:
if (
mapped_container is not None
or annotation is None
# issue #10516: need to do declarative_scan even with
# a non-Mapped annotation if we are doing
# __allow_unmapped__, for things like col.name
# assignment
or allow_unmapped_annotations
):
try:
value.declarative_scan(
self,
@@ -1609,7 +1617,7 @@ class _ClassScanMapperConfig(_MapperConfig):
setattr(cls, k, value)
continue
our_stuff[k] = value # type: ignore
our_stuff[k] = value
def _extract_declared_columns(self) -> None:
our_stuff = self.properties
@@ -1979,7 +1987,7 @@ class _DeferredMapperConfig(_ClassScanMapperConfig):
# mypy disallows plain property override of variable
@property # type: ignore
def cls(self) -> Type[Any]: # type: ignore
def cls(self) -> Type[Any]:
return self._cls() # type: ignore
@cls.setter
@@ -1999,7 +2007,7 @@ class _DeferredMapperConfig(_ClassScanMapperConfig):
@classmethod
def raise_unmapped_for_cls(cls, class_: Type[Any]) -> NoReturn:
if hasattr(class_, "_sa_raise_deferred_config"):
class_._sa_raise_deferred_config() # type: ignore
class_._sa_raise_deferred_config()
raise orm_exc.UnmappedClassError(
class_,

View File

@@ -425,7 +425,7 @@ class CompositeProperty(
elif hasattr(self.composite_class, "__composite_values__"):
_composite_getters[
self.composite_class
] = lambda obj: obj.__composite_values__() # type: ignore
] = lambda obj: obj.__composite_values__()
@util.preload_module("sqlalchemy.orm.properties")
@util.preload_module("sqlalchemy.orm.decl_base")
@@ -628,7 +628,7 @@ class CompositeProperty(
proxy_attr = self.parent.class_manager[self.key]
proxy_attr.impl.dispatch = proxy_attr.dispatch # type: ignore
proxy_attr.impl.dispatch._active_history = self.active_history # type: ignore # noqa: E501
proxy_attr.impl.dispatch._active_history = self.active_history
# TODO: need a deserialize hook here
@@ -806,16 +806,16 @@ class CompositeProperty(
def __ne__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
return self._compare(operators.ne, other)
def __lt__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
def __lt__(self, other: Any) -> ColumnElement[bool]:
return self._compare(operators.lt, other)
def __gt__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
def __gt__(self, other: Any) -> ColumnElement[bool]:
return self._compare(operators.gt, other)
def __le__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
def __le__(self, other: Any) -> ColumnElement[bool]:
return self._compare(operators.le, other)
def __ge__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
def __ge__(self, other: Any) -> ColumnElement[bool]:
return self._compare(operators.ge, other)
# what might be interesting would be if we create
@@ -839,8 +839,8 @@ class CompositeProperty(
]
if self._adapt_to_entity:
assert self.adapter is not None
comparisons = [self.adapter(x) for x in comparisons] # type: ignore # noqa: E501
return sql.and_(*comparisons) # type: ignore
comparisons = [self.adapter(x) for x in comparisons]
return sql.and_(*comparisons)
def __str__(self) -> str:
return str(self.parent.class_.__name__) + "." + self.key

View File

@@ -4,7 +4,6 @@
#
# This module is part of SQLAlchemy and is released under
# the MIT License: https://www.opensource.org/licenses/mit-license.php
# mypy: ignore-errors
"""Dynamic collection API.
@@ -23,13 +22,19 @@ from __future__ import annotations
from typing import Any
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
from . import attributes
from . import exc as orm_exc
from . import relationships
from . import util as orm_util
from .base import PassiveFlag
from .query import Query
from .session import object_session
from .writeonly import AbstractCollectionWriter
@@ -39,15 +44,28 @@ from .writeonly import WriteOnlyLoader
from .. import util
from ..engine import result
if TYPE_CHECKING:
from .session import Session
if TYPE_CHECKING:
from . import QueryableAttribute
from .mapper import Mapper
from .relationships import _RelationshipOrderByArg
from .session import Session
from .state import InstanceState
from .util import AliasedClass
from ..event import _Dispatch
from ..sql.elements import ColumnElement
_T = TypeVar("_T", bound=Any)
class DynamicCollectionHistory(WriteOnlyHistory):
def __init__(self, attr, state, passive, apply_to=None):
class DynamicCollectionHistory(WriteOnlyHistory[_T]):
def __init__(
self,
attr: DynamicAttributeImpl,
state: InstanceState[_T],
passive: PassiveFlag,
apply_to: Optional[DynamicCollectionHistory[_T]] = None,
) -> None:
if apply_to:
coll = AppenderQuery(attr, state).autoflush(False)
self.unchanged_items = util.OrderedIdentitySet(coll)
@@ -63,21 +81,21 @@ class DynamicCollectionHistory(WriteOnlyHistory):
class DynamicAttributeImpl(WriteOnlyAttributeImpl):
_supports_dynamic_iteration = True
collection_history_cls = DynamicCollectionHistory
collection_history_cls = DynamicCollectionHistory[Any]
query_class: Type[AppenderMixin[Any]] # type: ignore[assignment]
def __init__(
self,
class_,
key,
typecallable,
dispatch,
target_mapper,
order_by,
query_class=None,
**kw,
):
class_: Union[Type[Any], AliasedClass[Any]],
key: str,
dispatch: _Dispatch[QueryableAttribute[Any]],
target_mapper: Mapper[_T],
order_by: _RelationshipOrderByArg,
query_class: Optional[Type[AppenderMixin[_T]]] = None,
**kw: Any,
) -> None:
attributes.AttributeImpl.__init__(
self, class_, key, typecallable, dispatch, **kw
self, class_, key, None, dispatch, **kw
)
self.target_mapper = target_mapper
if order_by:
@@ -102,21 +120,23 @@ class AppenderMixin(AbstractCollectionWriter[_T]):
"""
query_class = None
query_class: Optional[Type[Query[_T]]] = None
_order_by_clauses: Tuple[ColumnElement[Any], ...]
def __init__(self, attr, state):
Query.__init__(self, attr.target_mapper, None)
def __init__(
self, attr: DynamicAttributeImpl, state: InstanceState[_T]
) -> None:
Query.__init__(
self, # type: ignore[arg-type]
attr.target_mapper,
None,
)
super().__init__(attr, state)
@property
def session(self) -> Session:
def session(self) -> Optional[Session]:
sess = object_session(self.instance)
if (
sess is not None
and self.autoflush
and sess.autoflush
and self.instance in sess
):
if sess is not None and sess.autoflush and self.instance in sess:
sess.flush()
if not orm_util.has_identity(self.instance):
return None
@@ -127,7 +147,7 @@ class AppenderMixin(AbstractCollectionWriter[_T]):
def session(self, session: Session) -> None:
self.sess = session
def _iter(self):
def _iter(self) -> Union[result.ScalarResult[_T], result.Result[_T]]:
sess = self.session
if sess is None:
state = attributes.instance_state(self.instance)
@@ -141,9 +161,9 @@ class AppenderMixin(AbstractCollectionWriter[_T]):
return result.IteratorResult(
result.SimpleResultMetaData([self.attr.class_.__name__]),
self.attr._get_collection_history(
self.attr._get_collection_history( # type: ignore[arg-type]
attributes.instance_state(self.instance),
attributes.PASSIVE_NO_INITIALIZE,
PassiveFlag.PASSIVE_NO_INITIALIZE,
).added_items,
_source_supports_scalars=True,
).scalars()
@@ -155,15 +175,15 @@ class AppenderMixin(AbstractCollectionWriter[_T]):
def __iter__(self) -> Iterator[_T]:
...
def __getitem__(self, index: Any) -> _T:
def __getitem__(self, index: Any) -> Union[_T, List[_T]]:
sess = self.session
if sess is None:
return self.attr._get_collection_history(
attributes.instance_state(self.instance),
attributes.PASSIVE_NO_INITIALIZE,
PassiveFlag.PASSIVE_NO_INITIALIZE,
).indexed(index)
else:
return self._generate(sess).__getitem__(index)
return self._generate(sess).__getitem__(index) # type: ignore[no-any-return] # noqa: E501
def count(self) -> int:
sess = self.session
@@ -171,13 +191,16 @@ class AppenderMixin(AbstractCollectionWriter[_T]):
return len(
self.attr._get_collection_history(
attributes.instance_state(self.instance),
attributes.PASSIVE_NO_INITIALIZE,
PassiveFlag.PASSIVE_NO_INITIALIZE,
).added_items
)
else:
return self._generate(sess).count()
def _generate(self, sess=None):
def _generate(
self,
sess: Optional[Session] = None,
) -> Query[_T]:
# note we're returning an entirely new Query class instance
# here without any assignment capabilities; the class of this
# query is determined by the session.
@@ -259,7 +282,7 @@ class AppenderMixin(AbstractCollectionWriter[_T]):
self._remove_impl(item)
class AppenderQuery(AppenderMixin[_T], Query[_T]):
class AppenderQuery(AppenderMixin[_T], Query[_T]): # type: ignore[misc]
"""A dynamic query that supports basic collection storage operations.
Methods on :class:`.AppenderQuery` include all methods of
@@ -270,7 +293,7 @@ class AppenderQuery(AppenderMixin[_T], Query[_T]):
"""
def mixin_user_query(cls):
def mixin_user_query(cls: Any) -> type[AppenderMixin[Any]]:
"""Return a new class with AppenderQuery functionality layered over."""
name = "Appender" + cls.__name__
return type(name, (AppenderMixin, cls), {"query_class": cls})

View File

@@ -138,7 +138,7 @@ class ClassManager(
def deferred_scalar_loader(self):
return self.expired_attribute_loader
@deferred_scalar_loader.setter # type: ignore[no-redef]
@deferred_scalar_loader.setter
@util.deprecated(
"1.4",
message="The ClassManager.deferred_scalar_loader attribute is now "
@@ -204,7 +204,7 @@ class ClassManager(
init_method: Optional[Callable[..., None]] = None,
) -> None:
if mapper:
self.mapper = mapper # type: ignore[assignment]
self.mapper = mapper #
if registry:
registry._add_manager(self)
if declarative_scan:
@@ -428,7 +428,7 @@ class ClassManager(
for key in list(self.originals):
self.uninstall_member(key)
self.mapper = None # type: ignore
self.mapper = None
self.dispatch = None # type: ignore
self.new_init = None
self.info.clear()
@@ -506,11 +506,11 @@ class ClassManager(
# so that mypy sees that __new__ is present. currently
# it's bound to Any as there were other problems not having
# it that way but these can be revisited
instance = self.class_.__new__(self.class_) # type: ignore
instance = self.class_.__new__(self.class_)
if state is None:
state = self._state_constructor(instance, self)
self._state_setter(instance, state)
return instance # type: ignore[no-any-return]
return instance
def setup_instance(
self, instance: _O, state: Optional[InstanceState[_O]] = None

View File

@@ -107,6 +107,7 @@ if typing.TYPE_CHECKING:
_StrategyKey = Tuple[Any, ...]
_T = TypeVar("_T", bound=Any)
_T_co = TypeVar("_T_co", bound=Any, covariant=True)
_TLS = TypeVar("_TLS", bound="Type[LoaderStrategy]")
@@ -653,7 +654,7 @@ class MapperProperty(
@inspection._self_inspects
class PropComparator(SQLORMOperations[_T], Generic[_T], ColumnOperators):
class PropComparator(SQLORMOperations[_T_co], Generic[_T_co], ColumnOperators):
r"""Defines SQL operations for ORM mapped attributes.
SQLAlchemy allows for operators to
@@ -740,7 +741,7 @@ class PropComparator(SQLORMOperations[_T], Generic[_T], ColumnOperators):
_parententity: _InternalEntityType[Any]
_adapt_to_entity: Optional[AliasedInsp[Any]]
prop: RODescriptorReference[MapperProperty[_T]]
prop: RODescriptorReference[MapperProperty[_T_co]]
def __init__(
self,
@@ -922,9 +923,7 @@ class PropComparator(SQLORMOperations[_T], Generic[_T], ColumnOperators):
"""
return self.operate( # type: ignore
PropComparator.any_op, criterion, **kwargs
)
return self.operate(PropComparator.any_op, criterion, **kwargs)
def has(
self,
@@ -946,9 +945,7 @@ class PropComparator(SQLORMOperations[_T], Generic[_T], ColumnOperators):
"""
return self.operate( # type: ignore
PropComparator.has_op, criterion, **kwargs
)
return self.operate(PropComparator.has_op, criterion, **kwargs)
class StrategizedProperty(MapperProperty[_T]):

View File

@@ -137,40 +137,64 @@ def instances(cursor: CursorResult[Any], context: QueryContext) -> Result[Any]:
"Can't use the ORM yield_per feature in conjunction with unique()"
)
def _not_hashable(datatype):
def go(obj):
raise sa_exc.InvalidRequestError(
"Can't apply uniqueness to row tuple containing value of "
"type %r; this datatype produces non-hashable values"
% datatype
)
def _not_hashable(datatype, *, legacy=False, uncertain=False):
if not legacy:
return go
def go(obj):
if uncertain:
try:
return hash(obj)
except:
pass
if context.load_options._legacy_uniquing:
unique_filters = [
_no_unique
if context.yield_per
else id
if (
ent.use_id_for_hash
or ent._non_hashable_value
or ent._null_column_type
)
else None
for ent in context.compile_state._entities
]
else:
unique_filters = [
_no_unique
if context.yield_per
else _not_hashable(ent.column.type) # type: ignore
if (not ent.use_id_for_hash and ent._non_hashable_value)
else id
if ent.use_id_for_hash
else None
for ent in context.compile_state._entities
]
raise sa_exc.InvalidRequestError(
"Can't apply uniqueness to row tuple containing value of "
f"""type {datatype!r}; {'the values returned appear to be'
if uncertain else 'this datatype produces'} """
"non-hashable values"
)
return go
elif not uncertain:
return id
else:
_use_id = False
def go(obj):
nonlocal _use_id
if not _use_id:
try:
return hash(obj)
except:
pass
# in #10459, we considered using a warning here, however
# as legacy query uses result.unique() in all cases, this
# would lead to too many warning cases.
_use_id = True
return id(obj)
return go
unique_filters = [
_no_unique
if context.yield_per
else _not_hashable(
ent.column.type, # type: ignore
legacy=context.load_options._legacy_uniquing,
uncertain=ent._null_column_type,
)
if (
not ent.use_id_for_hash
and (ent._non_hashable_value or ent._null_column_type)
)
else id
if ent.use_id_for_hash
else None
for ent in context.compile_state._entities
]
row_metadata = SimpleResultMetaData(
labels, extra, _unique_filters=unique_filters
@@ -1246,7 +1270,16 @@ def _load_subclass_via_in(
orig_query = context.query
options = (enable_opt,) + orig_query._with_options + (disable_opt,)
if path.parent:
enable_opt_lcl = enable_opt._prepend_path(path)
disable_opt_lcl = disable_opt._prepend_path(path)
else:
enable_opt_lcl = enable_opt
disable_opt_lcl = disable_opt
options = (
(enable_opt_lcl,) + orig_query._with_options + (disable_opt_lcl,)
)
q2 = q.options(*options)
q2._compile_options = context.compile_state.default_compile_options

View File

@@ -786,7 +786,7 @@ class Mapper(
# interim - polymorphic_on is further refined in
# _configure_polymorphic_setter
self.polymorphic_on = ( # type: ignore
self.polymorphic_on = (
coercions.expect( # type: ignore
roles.ColumnArgumentOrKeyRole,
polymorphic_on,
@@ -964,8 +964,8 @@ class Mapper(
version_id_generator: Optional[Union[Literal[False], Callable[[Any], Any]]]
local_table: FromClause
"""The immediate :class:`_expression.FromClause` which this
:class:`_orm.Mapper` refers towards.
"""The immediate :class:`_expression.FromClause` to which this
:class:`_orm.Mapper` refers.
Typically is an instance of :class:`_schema.Table`, may be any
:class:`.FromClause`.
@@ -1400,7 +1400,7 @@ class Mapper(
self.with_polymorphic = None
if self.with_polymorphic and self.with_polymorphic[1] is not None:
self.with_polymorphic = ( # type: ignore
self.with_polymorphic = (
self.with_polymorphic[0],
coercions.expect(
roles.StrictFromClauseRole,
@@ -1504,7 +1504,7 @@ class Mapper(
manager = instrumentation.register_class(
self.class_,
mapper=self,
expired_attribute_loader=util.partial( # type: ignore
expired_attribute_loader=util.partial(
loading.load_scalar_attributes, self
),
# finalize flag means instrument the __init__ method
@@ -1610,7 +1610,7 @@ class Mapper(
if isinstance(c, str)
else c
for c in (
coercions.expect( # type: ignore
coercions.expect(
roles.DDLConstraintColumnRole,
coerce_pk,
argname="primary_key",
@@ -4097,7 +4097,7 @@ class _OptGetColumnsNotAvailable(Exception):
pass
def configure_mappers():
def configure_mappers() -> None:
"""Initialize the inter-mapper relationships of all mappers that
have been constructed thus far across all :class:`_orm.registry`
collections.

View File

@@ -57,9 +57,9 @@ else:
_SerializedPath = List[Any]
_StrPathToken = str
_PathElementType = Union[
str, "_InternalEntityType[Any]", "MapperProperty[Any]"
_StrPathToken, "_InternalEntityType[Any]", "MapperProperty[Any]"
]
# the representation is in fact
@@ -70,6 +70,8 @@ _PathElementType = Union[
# chopped at odd intervals as well so this is less flexible
_PathRepresentation = Tuple[_PathElementType, ...]
# NOTE: these names are weird since the array is 0-indexed,
# the "_Odd" entries are at 0, 2, 4, etc
_OddPathRepresentation = Sequence["_InternalEntityType[Any]"]
_EvenPathRepresentation = Sequence[Union["MapperProperty[Any]", str]]
@@ -154,6 +156,9 @@ class PathRegistry(HasCacheKey):
def _path_for_compare(self) -> Optional[_PathRepresentation]:
return self.path
def odd_element(self, index: int) -> _InternalEntityType[Any]:
return self.path[index] # type: ignore
def set(self, attributes: Dict[Any, Any], key: Any, value: Any) -> None:
log.debug("set '%s' on path '%s' to '%s'", key, self, value)
attributes[(key, self.natural_path)] = value
@@ -180,7 +185,7 @@ class PathRegistry(HasCacheKey):
return id(self)
@overload
def __getitem__(self, entity: str) -> TokenRegistry:
def __getitem__(self, entity: _StrPathToken) -> TokenRegistry:
...
@overload
@@ -204,7 +209,11 @@ class PathRegistry(HasCacheKey):
def __getitem__(
self,
entity: Union[
str, int, slice, _InternalEntityType[Any], MapperProperty[Any]
_StrPathToken,
int,
slice,
_InternalEntityType[Any],
MapperProperty[Any],
],
) -> Union[
TokenRegistry,
@@ -355,7 +364,7 @@ class CreatesToken(PathRegistry):
is_aliased_class: bool
is_root: bool
def token(self, token: str) -> TokenRegistry:
def token(self, token: _StrPathToken) -> TokenRegistry:
if token.endswith(f":{_WILDCARD_TOKEN}"):
return TokenRegistry(self, token)
elif token.endswith(f":{_DEFAULT_TOKEN}"):
@@ -385,7 +394,7 @@ class RootRegistry(CreatesToken):
) -> Union[TokenRegistry, AbstractEntityRegistry]:
if entity in PathToken._intern:
if TYPE_CHECKING:
assert isinstance(entity, str)
assert isinstance(entity, _StrPathToken)
return TokenRegistry(self, PathToken._intern[entity])
else:
try:
@@ -433,10 +442,10 @@ class TokenRegistry(PathRegistry):
inherit_cache = True
token: str
token: _StrPathToken
parent: CreatesToken
def __init__(self, parent: CreatesToken, token: str):
def __init__(self, parent: CreatesToken, token: _StrPathToken):
token = PathToken.intern(token)
self.token = token
@@ -619,7 +628,7 @@ class PropRegistry(PathRegistry):
self._wildcard_path_loader_key = (
"loader",
parent.natural_path + self.prop._wildcard_token, # type: ignore
parent.natural_path + self.prop._wildcard_token,
)
self._default_path_loader_key = self.prop._default_path_loader_key
self._loader_key = ("loader", self.natural_path)
@@ -721,7 +730,7 @@ class AbstractEntityRegistry(CreatesToken):
@property
def root_entity(self) -> _InternalEntityType[Any]:
return cast("_InternalEntityType[Any]", self.path[0])
return self.odd_element(0)
@property
def entity_path(self) -> PathRegistry:

View File

@@ -1082,7 +1082,7 @@ def _emit_insert_statements(
records = list(records)
if returning_is_required_anyway or (
not hasvalue and len(records) > 1
table.implicit_returning and not hasvalue and len(records) > 1
):
if (
deterministic_results_reqd

View File

@@ -25,6 +25,7 @@ from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
from . import attributes
from . import strategy_options
@@ -446,7 +447,7 @@ class ColumnProperty(
try:
return ce.info # type: ignore
except AttributeError:
return self.prop.info # type: ignore
return self.prop.info
def _memoized_attr_expressions(self) -> Sequence[NamedColumn[Any]]:
"""The full sequence of columns referenced by this
@@ -475,13 +476,13 @@ class ColumnProperty(
def operate(
self, op: OperatorType, *other: Any, **kwargs: Any
) -> ColumnElement[Any]:
return op(self.__clause_element__(), *other, **kwargs) # type: ignore[return-value,no-any-return] # noqa: E501
return op(self.__clause_element__(), *other, **kwargs) # type: ignore[no-any-return] # noqa: E501
def reverse_operate(
self, op: OperatorType, other: Any, **kwargs: Any
) -> ColumnElement[Any]:
col = self.__clause_element__()
return op(col._bind_param(op, other), col, **kwargs) # type: ignore[return-value,no-any-return] # noqa: E501
return op(col._bind_param(op, other), col, **kwargs) # type: ignore[no-any-return] # noqa: E501
def __str__(self) -> str:
if not self.parent or not self.key:
@@ -542,7 +543,7 @@ class MappedColumn(
"_use_existing_column",
)
deferred: bool
deferred: Union[_NoArg, bool]
deferred_raiseload: bool
deferred_group: Optional[str]
@@ -557,17 +558,15 @@ class MappedColumn(
self._use_existing_column = kw.pop("use_existing_column", False)
self._has_dataclass_arguments = False
if attr_opts is not None and attr_opts != _DEFAULT_ATTRIBUTE_OPTIONS:
if attr_opts.dataclasses_default_factory is not _NoArg.NO_ARG:
self._has_dataclass_arguments = True
elif (
attr_opts.dataclasses_init is not _NoArg.NO_ARG
or attr_opts.dataclasses_repr is not _NoArg.NO_ARG
):
self._has_dataclass_arguments = True
self._has_dataclass_arguments = (
attr_opts is not None
and attr_opts != _DEFAULT_ATTRIBUTE_OPTIONS
and any(
attr_opts[i] is not _NoArg.NO_ARG
for i, attr in enumerate(attr_opts._fields)
if attr != "dataclasses_default"
)
)
insert_default = kw.pop("insert_default", _NoArg.NO_ARG)
self._has_insert_default = insert_default is not _NoArg.NO_ARG
@@ -580,12 +579,9 @@ class MappedColumn(
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.active_history = kw.pop("active_history", False)
self._sort_order = kw.pop("sort_order", 0)
self._sort_order = kw.pop("sort_order", _NoArg.NO_ARG)
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 (
@@ -617,10 +613,16 @@ class MappedColumn(
@property
def mapper_property_to_assign(self) -> Optional[MapperProperty[_T]]:
if self.deferred or self.active_history:
effective_deferred = self.deferred
if effective_deferred is _NoArg.NO_ARG:
effective_deferred = bool(
self.deferred_group or self.deferred_raiseload
)
if effective_deferred or self.active_history:
return ColumnProperty(
self.column,
deferred=self.deferred,
deferred=effective_deferred,
group=self.deferred_group,
raiseload=self.deferred_raiseload,
attribute_options=self._attribute_options,
@@ -631,7 +633,14 @@ class MappedColumn(
@property
def columns_to_assign(self) -> List[Tuple[Column[Any], int]]:
return [(self.column, self._sort_order)]
return [
(
self.column,
self._sort_order
if self._sort_order is not _NoArg.NO_ARG
else 0,
)
]
def __clause_element__(self) -> Column[_T]:
return self.column
@@ -639,13 +648,13 @@ class MappedColumn(
def operate(
self, op: OperatorType, *other: Any, **kwargs: Any
) -> ColumnElement[Any]:
return op(self.__clause_element__(), *other, **kwargs) # type: ignore[return-value,no-any-return] # noqa: E501
return op(self.__clause_element__(), *other, **kwargs) # type: ignore[no-any-return] # noqa: E501
def reverse_operate(
self, op: OperatorType, other: Any, **kwargs: Any
) -> ColumnElement[Any]:
col = self.__clause_element__()
return op(col._bind_param(op, other), col, **kwargs) # type: ignore[return-value,no-any-return] # noqa: E501
return op(col._bind_param(op, other), col, **kwargs) # type: ignore[no-any-return] # noqa: E501
def found_in_pep593_annotated(self) -> Any:
# return a blank mapped_column(). This mapped_column()'s
@@ -779,6 +788,65 @@ class MappedColumn(
use_args_from.column._merge(self.column)
sqltype = self.column.type
if (
use_args_from.deferred is not _NoArg.NO_ARG
and self.deferred is _NoArg.NO_ARG
):
self.deferred = use_args_from.deferred
if (
use_args_from.deferred_group is not None
and self.deferred_group is None
):
self.deferred_group = use_args_from.deferred_group
if (
use_args_from.deferred_raiseload is not None
and self.deferred_raiseload is None
):
self.deferred_raiseload = use_args_from.deferred_raiseload
if (
use_args_from._use_existing_column
and not self._use_existing_column
):
self._use_existing_column = True
if use_args_from.active_history:
self.active_history = use_args_from.active_history
if (
use_args_from._sort_order is not None
and self._sort_order is _NoArg.NO_ARG
):
self._sort_order = use_args_from._sort_order
if (
use_args_from.column.key is not None
or use_args_from.column.name is not None
):
util.warn_deprecated(
"Can't use the 'key' or 'name' arguments in "
"Annotated with mapped_column(); this will be ignored",
"2.0.22",
)
if use_args_from._has_dataclass_arguments:
for idx, arg in enumerate(
use_args_from._attribute_options._fields
):
if (
use_args_from._attribute_options[idx]
is not _NoArg.NO_ARG
):
arg = arg.replace("dataclasses_", "")
util.warn_deprecated(
f"Argument '{arg}' is a dataclass argument and "
"cannot be specified within a mapped_column() "
"bundled inside of an Annotated object",
"2.0.22",
)
if sqltype._isnull and not self.column.foreign_keys:
new_sqltype = None

View File

@@ -235,7 +235,9 @@ class Query(
def __init__(
self,
entities: Sequence[_ColumnsClauseArgument[Any]],
entities: Union[
_ColumnsClauseArgument[Any], Sequence[_ColumnsClauseArgument[Any]]
],
session: Optional[Session] = None,
):
"""Construct a :class:`_query.Query` directly.
@@ -274,11 +276,14 @@ class Query(
self._set_entities(entities)
def _set_propagate_attrs(self, values: Mapping[str, Any]) -> Self:
self._propagate_attrs = util.immutabledict(values) # type: ignore
self._propagate_attrs = util.immutabledict(values)
return self
def _set_entities(
self, entities: Iterable[_ColumnsClauseArgument[Any]]
self,
entities: Union[
_ColumnsClauseArgument[Any], Iterable[_ColumnsClauseArgument[Any]]
],
) -> None:
self._raw_columns = [
coercions.expect(
@@ -478,7 +483,7 @@ class Query(
return self
def _clone(self, **kw: Any) -> Self:
return self._generate() # type: ignore
return self._generate()
def _get_select_statement_only(self) -> Select[_T]:
if self._statement is not None:
@@ -1450,7 +1455,7 @@ class Query(
q._set_entities(columns)
if not q.load_options._yield_per:
q.load_options += {"_yield_per": 10}
return iter(q) # type: ignore
return iter(q)
@util.deprecated(
"1.4",

View File

@@ -271,6 +271,9 @@ class _RelationshipArg(Generic[_T1, _T2]):
self.resolved = attr_value
_RelationshipOrderByArg = Union[Literal[False], Tuple[ColumnElement[Any], ...]]
class _RelationshipArgs(NamedTuple):
"""stores user-passed parameters that are resolved at mapper configuration
time.
@@ -289,10 +292,7 @@ class _RelationshipArgs(NamedTuple):
Optional[_RelationshipJoinConditionArgument],
Optional[ColumnElement[Any]],
]
order_by: _RelationshipArg[
_ORMOrderByArgument,
Union[Literal[None, False], Tuple[ColumnElement[Any], ...]],
]
order_by: _RelationshipArg[_ORMOrderByArgument, _RelationshipOrderByArg]
foreign_keys: _RelationshipArg[
Optional[_ORMColCollectionArgument], Set[ColumnElement[Any]]
]
@@ -341,7 +341,7 @@ class RelationshipProperty(
secondaryjoin: Optional[ColumnElement[bool]]
secondary: Optional[FromClause]
_join_condition: JoinCondition
order_by: Union[Literal[False], Tuple[ColumnElement[Any], ...]]
order_by: _RelationshipOrderByArg
_user_defined_foreign_keys: Set[ColumnElement[Any]]
_calculated_foreign_keys: Set[ColumnElement[Any]]
@@ -1614,7 +1614,8 @@ class RelationshipProperty(
@util.memoized_property
def entity(self) -> _InternalEntityType[_T]:
"""Return the target mapped entity, which is an inspect() of the
class or aliased class that is referred towards.
class or aliased class that is referenced by this
:class:`.RelationshipProperty`.
"""
self.parent._check_configure()
@@ -1764,7 +1765,7 @@ class RelationshipProperty(
argument = de_optionalize_union_types(argument)
if hasattr(argument, "__origin__"):
arg_origin = argument.__origin__ # type: ignore
arg_origin = argument.__origin__
if isinstance(arg_origin, type) and issubclass(
arg_origin, abc.Collection
):
@@ -1786,7 +1787,7 @@ class RelationshipProperty(
if argument.__args__: # type: ignore
if isinstance(arg_origin, type) and issubclass(
arg_origin, typing.Mapping # type: ignore
arg_origin, typing.Mapping
):
type_arg = argument.__args__[-1] # type: ignore
else:
@@ -1804,7 +1805,7 @@ class RelationshipProperty(
f"Generic alias {argument} requires an argument"
)
elif hasattr(argument, "__forward_arg__"):
argument = argument.__forward_arg__ # type: ignore
argument = argument.__forward_arg__
argument = resolve_name_to_real_class_name(
argument, originating_module
@@ -1874,7 +1875,7 @@ class RelationshipProperty(
% (self.key, type(resolved_argument))
)
self.entity = entity # type: ignore
self.entity = entity
self.target = self.entity.persist_selectable
def _setup_join_conditions(self) -> None:
@@ -1990,7 +1991,7 @@ class RelationshipProperty(
'and not on the "many" side of a many-to-one or many-to-many '
"relationship. "
"To force this relationship to allow a particular "
'"%(relatedcls)s" object to be referred towards by only '
'"%(relatedcls)s" object to be referenced by only '
'a single "%(clsname)s" object at a time via the '
"%(rel)s relationship, which "
"would allow "

View File

@@ -108,6 +108,7 @@ __all__ = ["scoped_session"]
"begin",
"begin_nested",
"close",
"reset",
"commit",
"connection",
"delete",
@@ -118,6 +119,7 @@ __all__ = ["scoped_session"]
"expunge_all",
"flush",
"get",
"get_one",
"get_bind",
"is_modified",
"bulk_save_objects",
@@ -466,7 +468,8 @@ class scoped_session(Generic[_S]):
:ref:`pysqlite_serializable` - special workarounds required
with the SQLite driver in order for SAVEPOINT to work
correctly.
correctly. For asyncio use cases, see the section
:ref:`aiosqlite_serializable`.
""" # noqa: E501
@@ -491,12 +494,17 @@ class scoped_session(Generic[_S]):
.. tip::
The :meth:`_orm.Session.close` method **does not prevent the
Session from being used again**. The :class:`_orm.Session` itself
does not actually have a distinct "closed" state; it merely means
In the default running mode the :meth:`_orm.Session.close`
method **does not prevent the Session from being used again**.
The :class:`_orm.Session` itself does not actually have a
distinct "closed" state; it merely means
the :class:`_orm.Session` will release all database connections
and ORM objects.
Setting the parameter :paramref:`_orm.Session.close_resets_only`
to ``False`` will instead make the ``close`` final, meaning that
any further action on the session will be forbidden.
.. versionchanged:: 1.4 The :meth:`.Session.close` method does not
immediately create a new :class:`.SessionTransaction` object;
instead, the new :class:`.SessionTransaction` is created only if
@@ -505,13 +513,49 @@ class scoped_session(Generic[_S]):
.. seealso::
:ref:`session_closing` - detail on the semantics of
:meth:`_orm.Session.close`
:meth:`_orm.Session.close` and :meth:`_orm.Session.reset`.
:meth:`_orm.Session.reset` - a similar method that behaves like
``close()`` with the parameter
:paramref:`_orm.Session.close_resets_only` set to ``True``.
""" # noqa: E501
return self._proxied.close()
def reset(self) -> None:
r"""Close out the transactional resources and ORM objects used by this
:class:`_orm.Session`, resetting the session to its initial state.
.. container:: class_bases
Proxied for the :class:`_orm.Session` class on
behalf of the :class:`_orm.scoping.scoped_session` class.
This method provides for same "reset-only" behavior that the
:meth:_orm.Session.close method has provided historically, where the
state of the :class:`_orm.Session` is reset as though the object were
brand new, and ready to be used again.
The method may then be useful for :class:`_orm.Session` objects
which set :paramref:`_orm.Session.close_resets_only` to ``False``,
so that "reset only" behavior is still available from this method.
.. versionadded:: 2.0.22
.. seealso::
:ref:`session_closing` - detail on the semantics of
:meth:`_orm.Session.close` and :meth:`_orm.Session.reset`.
:meth:`_orm.Session.close` - a similar method will additionally
prevent re-use of the Session when the parameter
:paramref:`_orm.Session.close_resets_only` is set to ``False``.
""" # noqa: E501
return self._proxied.reset()
def commit(self) -> None:
r"""Flush pending changes and commit the current transaction.
@@ -1028,6 +1072,56 @@ class scoped_session(Generic[_S]):
bind_arguments=bind_arguments,
)
def get_one(
self,
entity: _EntityBindKey[_O],
ident: _PKIdentityArgument,
*,
options: Optional[Sequence[ORMOption]] = None,
populate_existing: bool = False,
with_for_update: ForUpdateParameter = None,
identity_token: Optional[Any] = None,
execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
bind_arguments: Optional[_BindArguments] = None,
) -> _O:
r"""Return exactly one instance based on the given primary key
identifier, or raise an exception if not found.
.. container:: class_bases
Proxied for the :class:`_orm.Session` class on
behalf of the :class:`_orm.scoping.scoped_session` class.
Raises ``sqlalchemy.orm.exc.NoResultFound`` if the query
selects no rows.
For a detailed documentation of the arguments see the
method :meth:`.Session.get`.
.. versionadded:: 2.0.22
:return: The object instance.
.. seealso::
:meth:`.Session.get` - equivalent method that instead
returns ``None`` if no row was found with the provided primary
key
""" # noqa: E501
return self._proxied.get_one(
entity,
ident,
options=options,
populate_existing=populate_existing,
with_for_update=with_for_update,
identity_token=identity_token,
execution_options=execution_options,
bind_arguments=bind_arguments,
)
def get_bind(
self,
mapper: Optional[_EntityBindKey[_O]] = None,

View File

@@ -83,6 +83,7 @@ from ..sql import roles
from ..sql import Select
from ..sql import TableClause
from ..sql import visitors
from ..sql.base import _NoArg
from ..sql.base import CompileState
from ..sql.schema import Table
from ..sql.selectable import ForUpdateArg
@@ -510,7 +511,8 @@ class ORMExecuteState(util.MemoizedSlots):
"""
return self.bind_arguments.get("mapper", None)
mp: Optional[Mapper[Any]] = self.bind_arguments.get("mapper", None)
return mp
@property
def all_mappers(self) -> Sequence[Mapper[Any]]:
@@ -718,9 +720,14 @@ class ORMExecuteState(util.MemoizedSlots):
"This ORM execution is not against a SELECT statement "
"so there are no load options."
)
return self.execution_options.get(
lo: Union[
context.QueryContext.default_load_options,
Type[context.QueryContext.default_load_options],
] = self.execution_options.get(
"_sa_orm_load_options", context.QueryContext.default_load_options
)
return lo
@property
def update_delete_options(
@@ -737,10 +744,14 @@ class ORMExecuteState(util.MemoizedSlots):
"This ORM execution is not against an UPDATE or DELETE "
"statement so there are no update options."
)
return self.execution_options.get(
uo: Union[
bulk_persistence.BulkUDCompileState.default_update_options,
Type[bulk_persistence.BulkUDCompileState.default_update_options],
] = self.execution_options.get(
"_sa_orm_update_options",
bulk_persistence.BulkUDCompileState.default_update_options,
)
return uo
@property
def _non_compile_orm_options(self) -> Sequence[ORMOption]:
@@ -881,6 +892,12 @@ class SessionTransaction(_StateChange, TransactionalContext):
self.nested = nested = origin is SessionTransactionOrigin.BEGIN_NESTED
self.origin = origin
if session._close_state is _SessionCloseState.CLOSED:
raise sa_exc.InvalidRequestError(
"This Session has been permanently closed and is unable "
"to handle any more transaction requests."
)
if nested:
if not parent:
raise sa_exc.InvalidRequestError(
@@ -1372,6 +1389,12 @@ class SessionTransaction(_StateChange, TransactionalContext):
return self._state not in (COMMITTED, CLOSED)
class _SessionCloseState(Enum):
ACTIVE = 1
CLOSED = 2
CLOSE_IS_RESET = 3
class Session(_SessionClassMethods, EventTarget):
"""Manages persistence operations for ORM-mapped objects.
@@ -1416,6 +1439,7 @@ class Session(_SessionClassMethods, EventTarget):
twophase: bool
join_transaction_mode: JoinTransactionMode
_query_cls: Type[Query[Any]]
_close_state: _SessionCloseState
def __init__(
self,
@@ -1432,6 +1456,7 @@ class Session(_SessionClassMethods, EventTarget):
query_cls: Optional[Type[Query[Any]]] = None,
autocommit: Literal[False] = False,
join_transaction_mode: JoinTransactionMode = "conditional_savepoint",
close_resets_only: Union[bool, _NoArg] = _NoArg.NO_ARG,
):
r"""Construct a new :class:`_orm.Session`.
@@ -1607,8 +1632,9 @@ class Session(_SessionClassMethods, EventTarget):
.. tip:: When using SQLite, the SQLite driver included through
Python 3.11 does not handle SAVEPOINTs correctly in all cases
without workarounds. See the section
:ref:`pysqlite_serializable` for details on current workarounds.
without workarounds. See the sections
:ref:`pysqlite_serializable` and :ref:`aiosqlite_serializable`
for details on current workarounds.
* ``"control_fully"`` - the :class:`_orm.Session` will take
control of the given transaction as its own;
@@ -1639,6 +1665,18 @@ class Session(_SessionClassMethods, EventTarget):
.. versionadded:: 2.0.0rc1
:param close_resets_only: Defaults to ``True``. Determines if
the session should reset itself after calling ``.close()``
or should pass in a no longer usable state, disabling re-use.
.. versionadded:: 2.0.22 added flag ``close_resets_only``.
A future SQLAlchemy version may change the default value of
this flag to ``False``.
.. seealso::
:ref:`session_closing` - Detail on the semantics of
:meth:`_orm.Session.close` and :meth:`_orm.Session.reset`.
""" # noqa
@@ -1671,6 +1709,13 @@ class Session(_SessionClassMethods, EventTarget):
self.autoflush = autoflush
self.expire_on_commit = expire_on_commit
self.enable_baked_queries = enable_baked_queries
# the idea is that at some point NO_ARG will warn that in the future
# the default will switch to close_resets_only=False.
if close_resets_only or close_resets_only is _NoArg.NO_ARG:
self._close_state = _SessionCloseState.CLOSE_IS_RESET
else:
self._close_state = _SessionCloseState.ACTIVE
if (
join_transaction_mode
and join_transaction_mode
@@ -1858,7 +1903,8 @@ class Session(_SessionClassMethods, EventTarget):
:ref:`pysqlite_serializable` - special workarounds required
with the SQLite driver in order for SAVEPOINT to work
correctly.
correctly. For asyncio use cases, see the section
:ref:`aiosqlite_serializable`.
"""
return self.begin(nested=True)
@@ -2393,12 +2439,17 @@ class Session(_SessionClassMethods, EventTarget):
.. tip::
The :meth:`_orm.Session.close` method **does not prevent the
Session from being used again**. The :class:`_orm.Session` itself
does not actually have a distinct "closed" state; it merely means
In the default running mode the :meth:`_orm.Session.close`
method **does not prevent the Session from being used again**.
The :class:`_orm.Session` itself does not actually have a
distinct "closed" state; it merely means
the :class:`_orm.Session` will release all database connections
and ORM objects.
Setting the parameter :paramref:`_orm.Session.close_resets_only`
to ``False`` will instead make the ``close`` final, meaning that
any further action on the session will be forbidden.
.. versionchanged:: 1.4 The :meth:`.Session.close` method does not
immediately create a new :class:`.SessionTransaction` object;
instead, the new :class:`.SessionTransaction` is created only if
@@ -2407,11 +2458,40 @@ class Session(_SessionClassMethods, EventTarget):
.. seealso::
:ref:`session_closing` - detail on the semantics of
:meth:`_orm.Session.close`
:meth:`_orm.Session.close` and :meth:`_orm.Session.reset`.
:meth:`_orm.Session.reset` - a similar method that behaves like
``close()`` with the parameter
:paramref:`_orm.Session.close_resets_only` set to ``True``.
"""
self._close_impl(invalidate=False)
def reset(self) -> None:
"""Close out the transactional resources and ORM objects used by this
:class:`_orm.Session`, resetting the session to its initial state.
This method provides for same "reset-only" behavior that the
:meth:_orm.Session.close method has provided historically, where the
state of the :class:`_orm.Session` is reset as though the object were
brand new, and ready to be used again.
The method may then be useful for :class:`_orm.Session` objects
which set :paramref:`_orm.Session.close_resets_only` to ``False``,
so that "reset only" behavior is still available from this method.
.. versionadded:: 2.0.22
.. seealso::
:ref:`session_closing` - detail on the semantics of
:meth:`_orm.Session.close` and :meth:`_orm.Session.reset`.
:meth:`_orm.Session.close` - a similar method will additionally
prevent re-use of the Session when the parameter
:paramref:`_orm.Session.close_resets_only` is set to ``False``.
"""
self._close_impl(invalidate=False, is_reset=True)
def invalidate(self) -> None:
"""Close this Session, using connection invalidation.
@@ -2448,7 +2528,9 @@ class Session(_SessionClassMethods, EventTarget):
"""
self._close_impl(invalidate=True)
def _close_impl(self, invalidate: bool) -> None:
def _close_impl(self, invalidate: bool, is_reset: bool = False) -> None:
if not is_reset and self._close_state is _SessionCloseState.ACTIVE:
self._close_state = _SessionCloseState.CLOSED
self.expunge_all()
if self._transaction is not None:
for transaction in self._transaction._iterate_self_and_parents():
@@ -3580,6 +3662,57 @@ class Session(_SessionClassMethods, EventTarget):
bind_arguments=bind_arguments,
)
def get_one(
self,
entity: _EntityBindKey[_O],
ident: _PKIdentityArgument,
*,
options: Optional[Sequence[ORMOption]] = None,
populate_existing: bool = False,
with_for_update: ForUpdateParameter = None,
identity_token: Optional[Any] = None,
execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
bind_arguments: Optional[_BindArguments] = None,
) -> _O:
"""Return exactly one instance based on the given primary key
identifier, or raise an exception if not found.
Raises ``sqlalchemy.orm.exc.NoResultFound`` if the query
selects no rows.
For a detailed documentation of the arguments see the
method :meth:`.Session.get`.
.. versionadded:: 2.0.22
:return: The object instance.
.. seealso::
:meth:`.Session.get` - equivalent method that instead
returns ``None`` if no row was found with the provided primary
key
"""
instance = self.get(
entity,
ident,
options=options,
populate_existing=populate_existing,
with_for_update=with_for_update,
identity_token=identity_token,
execution_options=execution_options,
bind_arguments=bind_arguments,
)
if instance is None:
raise sa_exc.NoResultFound(
"No row was found when one was required"
)
return instance
def _get_impl(
self,
entity: _EntityBindKey[_O],

View File

@@ -617,8 +617,8 @@ class InstanceState(interfaces.InspectionAttrInfo, Generic[_O]):
self.class_ = state_dict["class_"]
self.committed_state = state_dict.get("committed_state", {})
self._pending_mutations = state_dict.get("_pending_mutations", {}) # type: ignore # noqa E501
self.parents = state_dict.get("parents", {}) # type: ignore
self._pending_mutations = state_dict.get("_pending_mutations", {})
self.parents = state_dict.get("parents", {})
self.modified = state_dict.get("modified", False)
self.expired = state_dict.get("expired", False)
if "info" in state_dict:

View File

@@ -306,8 +306,9 @@ class ExpressionColumnLoader(ColumnLoader):
**kwargs,
):
columns = None
if loadopt and "expression" in loadopt.local_opts:
columns = [loadopt.local_opts["expression"]]
if loadopt and loadopt._extra_criteria:
columns = loadopt._extra_criteria
elif self._have_default_expression:
columns = self.parent_property.columns
@@ -343,8 +344,8 @@ class ExpressionColumnLoader(ColumnLoader):
):
# look through list of columns represented here
# to see which, if any, is present in the row.
if loadopt and "expression" in loadopt.local_opts:
columns = [loadopt.local_opts["expression"]]
if loadopt and loadopt._extra_criteria:
columns = loadopt._extra_criteria
for col in columns:
if adapter:

View File

@@ -34,6 +34,7 @@ from .attributes import QueryableAttribute
from .base import InspectionAttr
from .interfaces import LoaderOption
from .path_registry import _DEFAULT_TOKEN
from .path_registry import _StrPathToken
from .path_registry import _WILDCARD_TOKEN
from .path_registry import AbstractEntityRegistry
from .path_registry import path_is_property
@@ -77,7 +78,7 @@ if typing.TYPE_CHECKING:
from ..sql.cache_key import CacheKey
_AttrType = Union[str, "QueryableAttribute[Any]"]
_AttrType = Union[Literal["*"], "QueryableAttribute[Any]"]
_WildcardKeyType = Literal["relationship", "column"]
_StrategySpec = Dict[str, Any]
@@ -541,7 +542,12 @@ class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption):
)
def defaultload(self, attr: _AttrType) -> Self:
"""Indicate an attribute should load using its default loader style.
"""Indicate an attribute should load using its predefined loader style.
The behavior of this loading option is to not change the current
loading style of the attribute, meaning that the previously configured
one is used or, if no previous style was selected, the default
loading will be used.
This method is used to link to other loader options further into
a chain of attributes without altering the loader style of the links
@@ -741,7 +747,7 @@ class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption):
)
return self._set_column_strategy(
(key,), {"query_expression": True}, opts={"expression": expression}
(key,), {"query_expression": True}, extra_criteria=(expression,)
)
def selectin_polymorphic(self, classes: Iterable[Type[Any]]) -> Self:
@@ -813,6 +819,7 @@ class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption):
attrs: Tuple[_AttrType, ...],
strategy: Optional[_StrategySpec],
opts: Optional[_OptsType] = None,
extra_criteria: Optional[Tuple[Any, ...]] = None,
) -> Self:
strategy_key = self._coerce_strat(strategy)
@@ -822,6 +829,7 @@ class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption):
_COLUMN_TOKEN,
opts=opts,
attr_group=attrs,
extra_criteria=extra_criteria,
)
return self
@@ -878,6 +886,7 @@ class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption):
attr_group: Optional[_AttrGroupType] = None,
propagate_to_loaders: bool = True,
reconcile_to_other: Optional[bool] = None,
extra_criteria: Optional[Tuple[Any, ...]] = None,
) -> Self:
raise NotImplementedError()
@@ -977,6 +986,7 @@ class Load(_AbstractLoad):
__slots__ = (
"path",
"context",
"additional_source_entities",
)
_traverse_internals = [
@@ -986,11 +996,16 @@ class Load(_AbstractLoad):
visitors.InternalTraversal.dp_has_cache_key_list,
),
("propagate_to_loaders", visitors.InternalTraversal.dp_boolean),
(
"additional_source_entities",
visitors.InternalTraversal.dp_has_cache_key_list,
),
]
_cache_key_traversal = None
path: PathRegistry
context: Tuple[_LoadElement, ...]
additional_source_entities: Tuple[_InternalEntityType[Any], ...]
def __init__(self, entity: _EntityType[Any]):
insp = cast("Union[Mapper[Any], AliasedInsp[Any]]", inspect(entity))
@@ -999,16 +1014,20 @@ class Load(_AbstractLoad):
self.path = insp._path_registry
self.context = ()
self.propagate_to_loaders = False
self.additional_source_entities = ()
def __str__(self) -> str:
return f"Load({self.path[0]})"
@classmethod
def _construct_for_existing_path(cls, path: PathRegistry) -> Load:
def _construct_for_existing_path(
cls, path: AbstractEntityRegistry
) -> Load:
load = cls.__new__(cls)
load.path = path
load.context = ()
load.propagate_to_loaders = False
load.additional_source_entities = ()
return load
def _adapt_cached_option_to_uncached_option(
@@ -1016,6 +1035,13 @@ class Load(_AbstractLoad):
) -> ORMOption:
return self._adjust_for_extra_criteria(context)
def _prepend_path(self, path: PathRegistry) -> Load:
cloned = self._clone()
cloned.context = tuple(
element._prepend_path(path) for element in self.context
)
return cloned
def _adjust_for_extra_criteria(self, context: QueryContext) -> Load:
"""Apply the current bound parameters in a QueryContext to all
occurrences "extra_criteria" stored within this ``Load`` object,
@@ -1029,16 +1055,10 @@ class Load(_AbstractLoad):
found_crit = False
def process(opt: _LoadElement) -> _LoadElement:
if not opt._extra_criteria:
return opt
nonlocal orig_cache_key, replacement_cache_key, found_crit
found_crit = True
# avoid generating cache keys for the queries if we don't
# actually have any extra_criteria options, which is the
# common case
if orig_cache_key is None or replacement_cache_key is None:
orig_cache_key = orig_query._generate_cache_key()
replacement_cache_key = context.query._generate_cache_key()
@@ -1052,8 +1072,12 @@ class Load(_AbstractLoad):
)
for crit in opt._extra_criteria
)
return opt
# avoid generating cache keys for the queries if we don't
# actually have any extra_criteria options, which is the
# common case
new_context = tuple(
process(value._clone()) if value._extra_criteria else value
for value in self.context
@@ -1116,9 +1140,12 @@ class Load(_AbstractLoad):
assert cloned.propagate_to_loaders == self.propagate_to_loaders
if not orm_util._entity_corresponds_to_use_path_impl(
cast("_InternalEntityType[Any]", parent.path[-1]),
cast("_InternalEntityType[Any]", cloned.path[0]),
if not any(
orm_util._entity_corresponds_to_use_path_impl(
elem, cloned.path.odd_element(0)
)
for elem in (parent.path.odd_element(-1),)
+ parent.additional_source_entities
):
if len(cloned.path) > 1:
attrname = cloned.path[1]
@@ -1137,6 +1164,9 @@ class Load(_AbstractLoad):
if cloned.context:
parent.context += cloned.context
parent.additional_source_entities += (
cloned.additional_source_entities
)
@_generative
def options(self, *opts: _AbstractLoad) -> Self:
@@ -1191,6 +1221,7 @@ class Load(_AbstractLoad):
attr_group: Optional[_AttrGroupType] = None,
propagate_to_loaders: bool = True,
reconcile_to_other: Optional[bool] = None,
extra_criteria: Optional[Tuple[Any, ...]] = None,
) -> Self:
# for individual strategy that needs to propagate, set the whole
# Load container to also propagate, so that it shows up in
@@ -1224,9 +1255,14 @@ class Load(_AbstractLoad):
propagate_to_loaders,
attr_group=attr_group,
reconcile_to_other=reconcile_to_other,
extra_criteria=extra_criteria,
)
if load_element:
self.context += (load_element,)
assert opts is not None
self.additional_source_entities += cast(
"Tuple[_InternalEntityType[Any]]", opts["entities"]
)
else:
for attr in attrs:
@@ -1240,6 +1276,7 @@ class Load(_AbstractLoad):
propagate_to_loaders,
attr_group=attr_group,
reconcile_to_other=reconcile_to_other,
extra_criteria=extra_criteria,
)
else:
load_element = _AttributeStrategyLoad.create(
@@ -1251,6 +1288,7 @@ class Load(_AbstractLoad):
propagate_to_loaders,
attr_group=attr_group,
reconcile_to_other=reconcile_to_other,
extra_criteria=extra_criteria,
)
if load_element:
@@ -1314,6 +1352,7 @@ class _WildcardLoad(_AbstractLoad):
attr_group=None,
propagate_to_loaders=True,
reconcile_to_other=None,
extra_criteria=None,
):
assert attrs is not None
attr = attrs[0]
@@ -1330,6 +1369,8 @@ class _WildcardLoad(_AbstractLoad):
if opts:
self.local_opts = util.immutabledict(opts)
assert extra_criteria is None
def options(self, *opts: _AbstractLoad) -> Self:
raise NotImplementedError("Star option does not support sub-options")
@@ -1616,7 +1657,9 @@ class _LoadElement(
return effective_path
def _init_path(self, path, attr, wildcard_key, attr_group, raiseerr):
def _init_path(
self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
):
"""Apply ORM attributes and/or wildcard to an existing path, producing
a new path.
@@ -1668,7 +1711,7 @@ class _LoadElement(
def create(
cls,
path: PathRegistry,
attr: Optional[_AttrType],
attr: Union[_AttrType, _StrPathToken, None],
strategy: Optional[_StrategyKey],
wildcard_key: Optional[_WildcardKeyType],
local_opts: Optional[_OptsType],
@@ -1676,6 +1719,7 @@ class _LoadElement(
raiseerr: bool = True,
attr_group: Optional[_AttrGroupType] = None,
reconcile_to_other: Optional[bool] = None,
extra_criteria: Optional[Tuple[Any, ...]] = None,
) -> _LoadElement:
"""Create a new :class:`._LoadElement` object."""
@@ -1695,7 +1739,9 @@ class _LoadElement(
else:
opt._reconcile_to_other = None
path = opt._init_path(path, attr, wildcard_key, attr_group, raiseerr)
path = opt._init_path(
path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
)
if not path:
return None # type: ignore
@@ -1714,9 +1760,7 @@ class _LoadElement(
return cloned
def _prepend_path_from(
self, parent: Union[Load, _LoadElement]
) -> _LoadElement:
def _prepend_path_from(self, parent: Load) -> _LoadElement:
"""adjust the path of this :class:`._LoadElement` to be
a subpath of that of the given parent :class:`_orm.Load` object's
path.
@@ -1725,22 +1769,30 @@ class _LoadElement(
which is in turn part of the :meth:`_orm.Load.options` method.
"""
if not any(
orm_util._entity_corresponds_to_use_path_impl(
elem,
self.path.odd_element(0),
)
for elem in (parent.path.odd_element(-1),)
+ parent.additional_source_entities
):
raise sa_exc.ArgumentError(
f'Attribute "{self.path[1]}" does not link '
f'from element "{parent.path[-1]}".'
)
return self._prepend_path(parent.path)
def _prepend_path(self, path: PathRegistry) -> _LoadElement:
cloned = self._clone()
assert cloned.strategy == self.strategy
assert cloned.local_opts == self.local_opts
assert cloned.is_class_strategy == self.is_class_strategy
if not orm_util._entity_corresponds_to_use_path_impl(
cast("_InternalEntityType[Any]", parent.path[-1]),
cast("_InternalEntityType[Any]", cloned.path[0]),
):
raise sa_exc.ArgumentError(
f'Attribute "{cloned.path[1]}" does not link '
f'from element "{parent.path[-1]}".'
)
cloned.path = PathRegistry.coerce(parent.path[0:-1] + cloned.path[:])
cloned.path = PathRegistry.coerce(path[0:-1] + cloned.path[:])
return cloned
@@ -1789,7 +1841,7 @@ class _LoadElement(
replacement.local_opts = replacement.local_opts.union(
existing.local_opts
)
replacement._extra_criteria += replacement._extra_criteria
replacement._extra_criteria += existing._extra_criteria
return replacement
elif replacement.path.is_token:
# use 'last one wins' logic for wildcard options. this is also
@@ -1831,7 +1883,9 @@ class _AttributeStrategyLoad(_LoadElement):
is_class_strategy = False
is_token_strategy = False
def _init_path(self, path, attr, wildcard_key, attr_group, raiseerr):
def _init_path(
self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
):
assert attr is not None
self._of_type = None
self._path_with_polymorphic_path = None
@@ -1877,7 +1931,11 @@ class _AttributeStrategyLoad(_LoadElement):
# from an attribute. This appears to have been an artifact of how
# _UnboundLoad / Load interacted together, which was opaque and
# poorly defined.
self._extra_criteria = attr._extra_criteria
if extra_criteria:
assert not attr._extra_criteria
self._extra_criteria = extra_criteria
else:
self._extra_criteria = attr._extra_criteria
if getattr(attr, "_of_type", None):
ac = attr._of_type
@@ -2070,7 +2128,9 @@ class _TokenStrategyLoad(_LoadElement):
is_class_strategy = False
is_token_strategy = True
def _init_path(self, path, attr, wildcard_key, attr_group, raiseerr):
def _init_path(
self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
):
# assert isinstance(attr, str) or attr is None
if attr is not None:
default_token = attr.endswith(_DEFAULT_TOKEN)
@@ -2156,7 +2216,9 @@ class _ClassStrategyLoad(_LoadElement):
__visit_name__ = "class_strategy_load_element"
def _init_path(self, path, attr, wildcard_key, attr_group, raiseerr):
def _init_path(
self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria
):
return path
def _prepare_for_compile_state(
@@ -2447,7 +2509,9 @@ def _raise_for_does_not_link(path, attrname, parent_entity):
(
" Did you mean to use "
f'"{path[-2]}'
f'.of_type({parent_entity_str})"?'
f'.of_type({parent_entity_str})" or "loadopt.options('
f"selectin_polymorphic({path[-2].mapper.class_.__name__}, "
f'[{parent_entity_str}]), ...)" ?'
if not path_is_of_type
and not path[-1].is_aliased_class
and orm_util._entity_corresponds_to(

View File

@@ -45,6 +45,7 @@ from .base import _never_set as _never_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
from .base import DynamicMapped
from .base import InspectionAttr as InspectionAttr
from .base import instance_str as instance_str # noqa: F401
from .base import Mapped
@@ -55,6 +56,7 @@ from .base import ORMDescriptor
from .base import state_attribute_str as state_attribute_str # noqa: F401
from .base import state_class_str as state_class_str # noqa: F401
from .base import state_str as state_str # noqa: F401
from .base import WriteOnlyMapped
from .interfaces import CriteriaOption
from .interfaces import MapperProperty as MapperProperty
from .interfaces import ORMColumnsClauseRole
@@ -100,6 +102,7 @@ if typing.TYPE_CHECKING:
from .context import _MapperEntity
from .context import ORMCompileState
from .mapper import Mapper
from .path_registry import AbstractEntityRegistry
from .query import Query
from .relationships import RelationshipProperty
from ..engine import Row
@@ -137,7 +140,14 @@ all_cascades = frozenset(
_de_stringify_partial = functools.partial(
functools.partial, locals_=util.immutabledict({"Mapped": Mapped})
functools.partial,
locals_=util.immutabledict(
{
"Mapped": Mapped,
"WriteOnlyMapped": WriteOnlyMapped,
"DynamicMapped": DynamicMapped,
}
),
)
# partial is practically useless as we have to write out the whole
@@ -935,7 +945,7 @@ class AliasedInsp(
"""the AliasedClass that refers to this AliasedInsp"""
_target: Union[Type[_O], AliasedClass[_O]]
"""the thing referred towards by the AliasedClass/AliasedInsp.
"""the thing referenced by the AliasedClass/AliasedInsp.
In the vast majority of cases, this is the mapped class. However
it may also be another AliasedClass (alias of alias).
@@ -1124,7 +1134,7 @@ class AliasedInsp(
return self.mapper.class_
@property
def _path_registry(self) -> PathRegistry:
def _path_registry(self) -> AbstractEntityRegistry:
if self._use_mapper_path:
return self.mapper._path_registry
else:
@@ -1204,7 +1214,7 @@ class AliasedInsp(
# IMO mypy should see this one also as returning the same type
# we put into it, but it's not
return (
self._adapter.traverse(expr) # type: ignore
self._adapter.traverse(expr)
._annotate(d)
._set_propagate_attrs(
{"compile_state_plugin": "orm", "plugin_subject": self}
@@ -1397,7 +1407,7 @@ class LoaderCriteriaOption(CriteriaOption):
self.deferred_where_criteria = True
self.where_criteria = lambdas.DeferredLambdaElement(
where_criteria, # type: ignore
where_criteria,
roles.WhereHavingRole,
lambda_args=(_WrapUserEntity(wrap_entity),),
opts=lambdas.LambdaOptions(
@@ -1620,12 +1630,18 @@ class Bundle(
)
@property
def mapper(self) -> Mapper[Any]:
return self.exprs[0]._annotations.get("parentmapper", None)
def mapper(self) -> Optional[Mapper[Any]]:
mp: Optional[Mapper[Any]] = self.exprs[0]._annotations.get(
"parentmapper", None
)
return mp
@property
def entity(self) -> _InternalEntityType[Any]:
return self.exprs[0]._annotations.get("parententity", None)
def entity(self) -> Optional[_InternalEntityType[Any]]:
ie: Optional[_InternalEntityType[Any]] = self.exprs[
0
]._annotations.get("parententity", None)
return ie
@property
def entity_namespace(
@@ -1827,8 +1843,8 @@ class _ORMJoin(expression.Join):
prop = None
on_selectable = None
left_selectable = left_info.selectable
if prop:
left_selectable = left_info.selectable
adapt_from: Optional[FromClause]
if sql_util.clause_is_present(on_selectable, left_selectable):
adapt_from = on_selectable
@@ -1865,25 +1881,25 @@ class _ORMJoin(expression.Join):
self._target_adapter = target_adapter
# we don't use the normal coercions logic for _ORMJoin
# (probably should), so do some gymnastics to get the entity.
# logic here is for #8721, which was a major bug in 1.4
# for almost two years, not reported/fixed until 1.4.43 (!)
if is_selectable(left_info):
parententity = left_selectable._annotations.get(
"parententity", None
)
elif insp_is_mapper(left_info) or insp_is_aliased_class(left_info):
parententity = left_info
else:
parententity = None
# we don't use the normal coercions logic for _ORMJoin
# (probably should), so do some gymnastics to get the entity.
# logic here is for #8721, which was a major bug in 1.4
# for almost two years, not reported/fixed until 1.4.43 (!)
if is_selectable(left_info):
parententity = left_selectable._annotations.get(
"parententity", None
)
elif insp_is_mapper(left_info) or insp_is_aliased_class(left_info):
parententity = left_info
else:
parententity = None
if parententity is not None:
self._annotations = self._annotations.union(
{"parententity": parententity}
)
if parententity is not None:
self._annotations = self._annotations.union(
{"parententity": parententity}
)
augment_onclause = onclause is None and _extra_criteria
augment_onclause = bool(_extra_criteria) and not prop
expression.Join.__init__(self, left, right, onclause, isouter, full)
assert self.onclause is not None
@@ -2169,9 +2185,9 @@ def _getitem(iterable_query: Query[Any], item: Any) -> Any:
res = iterable_query.slice(start, stop)
if step is not None:
return list(res)[None : None : item.step] # type: ignore
return list(res)[None : None : item.step]
else:
return list(res) # type: ignore
return list(res)
else:
if item == -1:
_no_negative_indexes()
@@ -2234,14 +2250,17 @@ def _cleanup_mapped_str_annotation(
"outside of TYPE_CHECKING blocks"
) from ne
try:
if issubclass(obj, _MappedAnnotationBase):
real_symbol = obj.__name__
else:
if obj is typing.ClassVar:
real_symbol = "ClassVar"
else:
try:
if issubclass(obj, _MappedAnnotationBase):
real_symbol = obj.__name__
else:
return annotation
except TypeError:
# avoid isinstance(obj, type) check, just catch TypeError
return annotation
except TypeError:
# avoid isinstance(obj, type) check, just catch TypeError
return annotation
# note: if one of the codepaths above didn't define real_symbol and
# then didn't return, real_symbol raises UnboundLocalError
@@ -2380,9 +2399,9 @@ def _extract_mapped_subtype(
else:
return annotated, None
if len(annotated.__args__) != 1: # type: ignore
if len(annotated.__args__) != 1:
raise sa_exc.ArgumentError(
"Expected sub-type for Mapped[] annotation"
)
return annotated.__args__[0], annotated.__origin__ # type: ignore
return annotated.__args__[0], annotated.__origin__

View File

@@ -4,8 +4,6 @@
#
# This module is part of SQLAlchemy and is released under
# the MIT License: https://www.opensource.org/licenses/mit-license.php
# mypy: ignore-errors
"""Write-only collection API.
@@ -21,12 +19,17 @@ object must be executed each time.
from __future__ import annotations
from typing import Any
from typing import Collection
from typing import Dict
from typing import Generic
from typing import Iterable
from typing import Iterator
from typing import List
from typing import NoReturn
from typing import Optional
from typing import overload
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
@@ -36,9 +39,10 @@ from . import attributes
from . import interfaces
from . import relationships
from . import strategies
from .base import NEVER_SET
from .base import object_mapper
from .base import PassiveFlag
from .relationships import RelationshipDirection
from .base import RelationshipDirection
from .. import exc
from .. import inspect
from .. import log
@@ -53,22 +57,38 @@ from ..sql.dml import Update
from ..util.typing import Literal
if TYPE_CHECKING:
from . import QueryableAttribute
from ._typing import _InstanceDict
from .attributes import _AdaptedCollectionProtocol
from .attributes import AttributeEventToken
from .attributes import CollectionAdapter
from .base import LoaderCallableStatus
from .collections import _AdaptedCollectionProtocol
from .collections import CollectionAdapter
from .mapper import Mapper
from .relationships import _RelationshipOrderByArg
from .state import InstanceState
from .util import AliasedClass
from ..event import _Dispatch
from ..sql.selectable import FromClause
from ..sql.selectable import Select
_T = TypeVar("_T", bound=Any)
class WriteOnlyHistory:
class WriteOnlyHistory(Generic[_T]):
"""Overrides AttributeHistory to receive append/remove events directly."""
def __init__(self, attr, state, passive, apply_to=None):
unchanged_items: util.OrderedIdentitySet
added_items: util.OrderedIdentitySet
deleted_items: util.OrderedIdentitySet
_reconcile_collection: bool
def __init__(
self,
attr: WriteOnlyAttributeImpl,
state: InstanceState[_T],
passive: PassiveFlag,
apply_to: Optional[WriteOnlyHistory[_T]] = None,
) -> None:
if apply_to:
if passive & PassiveFlag.SQL_OK:
raise exc.InvalidRequestError(
@@ -90,18 +110,18 @@ class WriteOnlyHistory:
self._reconcile_collection = False
@property
def added_plus_unchanged(self):
def added_plus_unchanged(self) -> List[_T]:
return list(self.added_items.union(self.unchanged_items))
@property
def all_items(self):
def all_items(self) -> List[_T]:
return list(
self.added_items.union(self.unchanged_items).union(
self.deleted_items
)
)
def as_history(self):
def as_history(self) -> attributes.History:
if self._reconcile_collection:
added = self.added_items.difference(self.unchanged_items)
deleted = self.deleted_items.intersection(self.unchanged_items)
@@ -114,13 +134,13 @@ class WriteOnlyHistory:
)
return attributes.History(list(added), list(unchanged), list(deleted))
def indexed(self, index):
def indexed(self, index: Union[int, slice]) -> Union[List[_T], _T]:
return list(self.added_items)[index]
def add_added(self, value):
def add_added(self, value: _T) -> None:
self.added_items.add(value)
def add_removed(self, value):
def add_removed(self, value: _T) -> None:
if value in self.added_items:
self.added_items.remove(value)
else:
@@ -130,35 +150,41 @@ class WriteOnlyHistory:
class WriteOnlyAttributeImpl(
attributes.HasCollectionAdapter, attributes.AttributeImpl
):
uses_objects = True
default_accepts_scalar_loader = False
supports_population = False
_supports_dynamic_iteration = False
collection = False
dynamic = True
order_by = ()
collection_history_cls = WriteOnlyHistory
uses_objects: bool = True
default_accepts_scalar_loader: bool = False
supports_population: bool = False
_supports_dynamic_iteration: bool = False
collection: bool = False
dynamic: bool = True
order_by: _RelationshipOrderByArg = ()
collection_history_cls: Type[WriteOnlyHistory[Any]] = WriteOnlyHistory
query_class: Type[WriteOnlyCollection[Any]]
def __init__(
self,
class_,
key,
typecallable,
dispatch,
target_mapper,
order_by,
**kw,
class_: Union[Type[Any], AliasedClass[Any]],
key: str,
dispatch: _Dispatch[QueryableAttribute[Any]],
target_mapper: Mapper[_T],
order_by: _RelationshipOrderByArg,
**kw: Any,
):
super().__init__(class_, key, typecallable, dispatch, **kw)
super().__init__(class_, key, None, dispatch, **kw)
self.target_mapper = target_mapper
self.query_class = WriteOnlyCollection
if order_by:
self.order_by = tuple(order_by)
def get(self, state, dict_, passive=attributes.PASSIVE_OFF):
if not passive & attributes.SQL_OK:
def get(
self,
state: InstanceState[Any],
dict_: _InstanceDict,
passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
) -> Union[util.OrderedIdentitySet, WriteOnlyCollection[Any]]:
if not passive & PassiveFlag.SQL_OK:
return self._get_collection_history(
state, attributes.PASSIVE_NO_INITIALIZE
state, PassiveFlag.PASSIVE_NO_INITIALIZE
).added_items
else:
return self.query_class(self, state)
@@ -204,24 +230,34 @@ class WriteOnlyAttributeImpl(
) -> Union[
Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
]:
if not passive & attributes.SQL_OK:
data: Collection[Any]
if not passive & PassiveFlag.SQL_OK:
data = self._get_collection_history(state, passive).added_items
else:
history = self._get_collection_history(state, passive)
data = history.added_plus_unchanged
return DynamicCollectionAdapter(data) # type: ignore
return DynamicCollectionAdapter(data) # type: ignore[return-value]
@util.memoized_property
def _append_token(self):
def _append_token( # type:ignore[override]
self,
) -> attributes.AttributeEventToken:
return attributes.AttributeEventToken(self, attributes.OP_APPEND)
@util.memoized_property
def _remove_token(self):
def _remove_token( # type:ignore[override]
self,
) -> attributes.AttributeEventToken:
return attributes.AttributeEventToken(self, attributes.OP_REMOVE)
def fire_append_event(
self, state, dict_, value, initiator, collection_history=None
):
self,
state: InstanceState[Any],
dict_: _InstanceDict,
value: Any,
initiator: Optional[AttributeEventToken],
collection_history: Optional[WriteOnlyHistory[Any]] = None,
) -> None:
if collection_history is None:
collection_history = self._modified_event(state, dict_)
@@ -234,8 +270,13 @@ class WriteOnlyAttributeImpl(
self.sethasparent(attributes.instance_state(value), state, True)
def fire_remove_event(
self, state, dict_, value, initiator, collection_history=None
):
self,
state: InstanceState[Any],
dict_: _InstanceDict,
value: Any,
initiator: Optional[AttributeEventToken],
collection_history: Optional[WriteOnlyHistory[Any]] = None,
) -> None:
if collection_history is None:
collection_history = self._modified_event(state, dict_)
@@ -247,18 +288,20 @@ class WriteOnlyAttributeImpl(
for fn in self.dispatch.remove:
fn(state, value, initiator or self._remove_token)
def _modified_event(self, state, dict_):
def _modified_event(
self, state: InstanceState[Any], dict_: _InstanceDict
) -> WriteOnlyHistory[Any]:
if self.key not in state.committed_state:
state.committed_state[self.key] = self.collection_history_cls(
self, state, PassiveFlag.PASSIVE_NO_FETCH
)
state._modified_event(dict_, self, attributes.NEVER_SET)
state._modified_event(dict_, self, NEVER_SET)
# this is a hack to allow the entities.ComparableEntity fixture
# to work
dict_[self.key] = True
return state.committed_state[self.key]
return state.committed_state[self.key] # type: ignore[no-any-return]
def set(
self,
@@ -321,25 +364,38 @@ class WriteOnlyAttributeImpl(
collection_history=collection_history,
)
def delete(self, *args, **kwargs):
def delete(self, *args: Any, **kwargs: Any) -> NoReturn:
raise NotImplementedError()
def set_committed_value(self, state, dict_, value):
def set_committed_value(
self, state: InstanceState[Any], dict_: _InstanceDict, value: Any
) -> NoReturn:
raise NotImplementedError(
"Dynamic attributes don't support collection population."
)
def get_history(self, state, dict_, passive=attributes.PASSIVE_NO_FETCH):
def get_history(
self,
state: InstanceState[Any],
dict_: _InstanceDict,
passive: PassiveFlag = PassiveFlag.PASSIVE_NO_FETCH,
) -> attributes.History:
c = self._get_collection_history(state, passive)
return c.as_history()
def get_all_pending(
self, state, dict_, passive=attributes.PASSIVE_NO_INITIALIZE
):
self,
state: InstanceState[Any],
dict_: _InstanceDict,
passive: PassiveFlag = PassiveFlag.PASSIVE_NO_INITIALIZE,
) -> List[Tuple[InstanceState[Any], Any]]:
c = self._get_collection_history(state, passive)
return [(attributes.instance_state(x), x) for x in c.all_items]
def _get_collection_history(self, state, passive):
def _get_collection_history(
self, state: InstanceState[Any], passive: PassiveFlag
) -> WriteOnlyHistory[Any]:
c: WriteOnlyHistory[Any]
if self.key in state.committed_state:
c = state.committed_state[self.key]
else:
@@ -347,7 +403,7 @@ class WriteOnlyAttributeImpl(
self, state, PassiveFlag.PASSIVE_NO_FETCH
)
if state.has_identity and (passive & attributes.INIT_OK):
if state.has_identity and (passive & PassiveFlag.INIT_OK):
return self.collection_history_cls(
self, state, passive, apply_to=c
)
@@ -356,34 +412,34 @@ class WriteOnlyAttributeImpl(
def append(
self,
state,
dict_,
value,
initiator,
passive=attributes.PASSIVE_NO_FETCH,
):
state: InstanceState[Any],
dict_: _InstanceDict,
value: Any,
initiator: Optional[AttributeEventToken],
passive: PassiveFlag = PassiveFlag.PASSIVE_NO_FETCH,
) -> None:
if initiator is not self:
self.fire_append_event(state, dict_, value, initiator)
def remove(
self,
state,
dict_,
value,
initiator,
passive=attributes.PASSIVE_NO_FETCH,
):
state: InstanceState[Any],
dict_: _InstanceDict,
value: Any,
initiator: Optional[AttributeEventToken],
passive: PassiveFlag = PassiveFlag.PASSIVE_NO_FETCH,
) -> None:
if initiator is not self:
self.fire_remove_event(state, dict_, value, initiator)
def pop(
self,
state,
dict_,
value,
initiator,
passive=attributes.PASSIVE_NO_FETCH,
):
state: InstanceState[Any],
dict_: _InstanceDict,
value: Any,
initiator: Optional[AttributeEventToken],
passive: PassiveFlag = PassiveFlag.PASSIVE_NO_FETCH,
) -> None:
self.remove(state, dict_, value, initiator, passive=passive)
@@ -392,7 +448,7 @@ class WriteOnlyAttributeImpl(
class WriteOnlyLoader(strategies.AbstractRelationshipLoader, log.Identified):
impl_class = WriteOnlyAttributeImpl
def init_class_attribute(self, mapper):
def init_class_attribute(self, mapper: Mapper[Any]) -> None:
self.is_class_level = True
if not self.uselist or self.parent_property.direction not in (
interfaces.ONETOMANY,
@@ -404,7 +460,7 @@ class WriteOnlyLoader(strategies.AbstractRelationshipLoader, log.Identified):
"uselist=False." % self.parent_property
)
strategies._register_attribute(
strategies._register_attribute( # type: ignore[no-untyped-call]
self.parent_property,
mapper,
useobject=True,
@@ -418,19 +474,21 @@ class WriteOnlyLoader(strategies.AbstractRelationshipLoader, log.Identified):
class DynamicCollectionAdapter:
"""simplified CollectionAdapter for internal API consistency"""
def __init__(self, data):
data: Collection[Any]
def __init__(self, data: Collection[Any]):
self.data = data
def __iter__(self):
def __iter__(self) -> Iterator[Any]:
return iter(self.data)
def _reset_empty(self):
def _reset_empty(self) -> None:
pass
def __len__(self):
def __len__(self) -> int:
return len(self.data)
def __bool__(self):
def __bool__(self) -> bool:
return True
@@ -443,8 +501,14 @@ class AbstractCollectionWriter(Generic[_T]):
if not TYPE_CHECKING:
__slots__ = ()
def __init__(self, attr, state):
self.instance = instance = state.obj()
instance: _T
_from_obj: Tuple[FromClause, ...]
def __init__(self, attr: WriteOnlyAttributeImpl, state: InstanceState[_T]):
instance = state.obj()
if TYPE_CHECKING:
assert instance
self.instance = instance
self.attr = attr
mapper = object_mapper(instance)
@@ -535,7 +599,7 @@ class WriteOnlyCollection(AbstractCollectionWriter[_T]):
stmt = stmt.order_by(*self._order_by_clauses)
return stmt
def insert(self) -> Insert[_T]:
def insert(self) -> Insert:
"""For one-to-many collections, produce a :class:`_dml.Insert` which
will insert new rows in terms of this this instance-local
:class:`_orm.WriteOnlyCollection`.
@@ -561,7 +625,7 @@ class WriteOnlyCollection(AbstractCollectionWriter[_T]):
"INSERT along with add_all()."
)
dict_ = {}
dict_: Dict[str, Any] = {}
for l, r in prop.synchronize_pairs:
fn = prop._get_attr_w_warn_on_none(
@@ -575,14 +639,14 @@ class WriteOnlyCollection(AbstractCollectionWriter[_T]):
return insert(self.attr.target_mapper).values(**dict_)
def update(self) -> Update[_T]:
def update(self) -> Update:
"""Produce a :class:`_dml.Update` which will refer to rows in terms
of this instance-local :class:`_orm.WriteOnlyCollection`.
"""
return update(self.attr.target_mapper).where(*self._where_criteria)
def delete(self) -> Delete[_T]:
def delete(self) -> Delete:
"""Produce a :class:`_dml.Delete` which will refer to rows in terms
of this instance-local :class:`_orm.WriteOnlyCollection`.