diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-04-19 21:06:41 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-04-27 14:46:36 -0400 |
| commit | ad11c482e2233f44e8747d4d5a2b17a995fff1fa (patch) | |
| tree | 57f8ddd30928951519fd6ac0f418e9cbf8e65610 /lib/sqlalchemy/orm/util.py | |
| parent | 033d1a16e7a220555d7611a5b8cacb1bd83822ae (diff) | |
| download | sqlalchemy-ad11c482e2233f44e8747d4d5a2b17a995fff1fa.tar.gz | |
pep484 ORM / SQL result support
after some experimentation it seems mypy is more amenable
to the generic types being fully integrated rather than
having separate spin-off types. so key structures
like Result, Row, Select become generic. For DML
Insert, Update, Delete, these are spun into type-specific
subclasses ReturningInsert, ReturningUpdate, ReturningDelete,
which is fine since the "row-ness" of these constructs
doesn't happen until returning() is called in any case.
a Tuple based model is then integrated so that these
objects can carry along information about their return
types. Overloads at the .execute() level carry through
the Tuple from the invoked object to the result.
To suit the issue of AliasedClass generating attributes
that are dynamic, experimented with a custom subclass
AsAliased, but then just settled on having aliased()
lie to the type checker and return `Type[_O]`, essentially.
will need some type-related accessors for with_polymorphic()
also.
Additionally, identified an issue in Update when used
"mysql style" against a join(), it basically doesn't work
if asked to UPDATE two tables on the same column name.
added an error message to the specific condition where
it happens with a very non-specific error message that we
hit a thing we can't do right now, suggest multi-table
update as a possible cause.
Change-Id: I5eff7eefe1d6166ee74160b2785c5e6a81fa8b95
Diffstat (limited to 'lib/sqlalchemy/orm/util.py')
| -rw-r--r-- | lib/sqlalchemy/orm/util.py | 53 |
1 files changed, 39 insertions, 14 deletions
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 3934de535..8148793b1 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -28,6 +28,7 @@ from typing import Union import weakref from . import attributes # noqa +from . import exc from ._typing import _O from ._typing import insp_is_aliased_class from ._typing import insp_is_mapper @@ -41,6 +42,7 @@ from .base import InspectionAttr as InspectionAttr from .base import instance_str as instance_str from .base import object_mapper as object_mapper from .base import object_state as object_state +from .base import opt_manager_of_class from .base import state_attribute_str as state_attribute_str from .base import state_class_str as state_class_str from .base import state_str as state_str @@ -68,6 +70,7 @@ from ..sql.base import ColumnCollection from ..sql.cache_key import HasCacheKey from ..sql.cache_key import MemoizedHasCacheKey from ..sql.elements import ColumnElement +from ..sql.elements import KeyedColumnElement from ..sql.selectable import FromClause from ..util.langhelpers import MemoizedSlots from ..util.typing import de_stringify_annotation @@ -95,9 +98,7 @@ if typing.TYPE_CHECKING: from ..sql.selectable import _ColumnsClauseElement from ..sql.selectable import Alias from ..sql.selectable import Subquery - from ..sql.visitors import _ET from ..sql.visitors import anon_map - from ..sql.visitors import ExternallyTraversible _T = TypeVar("_T", bound=Any) @@ -341,7 +342,7 @@ def identity_key( ident: Union[Any, Tuple[Any, ...]] = None, *, instance: Optional[_T] = None, - row: Optional[Union[Row, RowMapping]] = None, + row: Optional[Union[Row[Any], RowMapping]] = None, identity_token: Optional[Any] = None, ) -> _IdentityKeyType[_T]: r"""Generate "identity key" tuples, as are used as keys in the @@ -468,7 +469,9 @@ class ORMAdapter(sql_util.ColumnAdapter): return not entity or entity.isa(self.mapper) -class AliasedClass(inspection.Inspectable["AliasedInsp[_O]"], Generic[_O]): +class AliasedClass( + inspection.Inspectable["AliasedInsp[_O]"], ORMColumnsClauseRole[_O] +): r"""Represents an "aliased" form of a mapped class for usage with Query. The ORM equivalent of a :func:`~sqlalchemy.sql.expression.alias` @@ -663,7 +666,7 @@ class AliasedClass(inspection.Inspectable["AliasedInsp[_O]"], Generic[_O]): @inspection._self_inspects class AliasedInsp( - ORMEntityColumnsClauseRole, + ORMEntityColumnsClauseRole[_O], ORMFromClauseRole, HasCacheKey, InspectionAttr, @@ -1276,12 +1279,29 @@ class LoaderCriteriaOption(CriteriaOption): inspection._inspects(AliasedClass)(lambda target: target._aliased_insp) +@inspection._inspects(type) +def _inspect_mc( + class_: Type[_O], +) -> Optional[Mapper[_O]]: + + try: + class_manager = opt_manager_of_class(class_) + if class_manager is None or not class_manager.is_mapped: + return None + mapper = class_manager.mapper + except exc.NO_STATE: + + return None + else: + return mapper + + @inspection._self_inspects class Bundle( - ORMColumnsClauseRole, + ORMColumnsClauseRole[_T], SupportsCloneAnnotations, MemoizedHasCacheKey, - inspection.Inspectable["Bundle"], + inspection.Inspectable["Bundle[_T]"], InspectionAttr, ): """A grouping of SQL expressions that are returned by a :class:`.Query` @@ -1373,10 +1393,10 @@ class Bundle( @property def entity_namespace( self, - ) -> ReadOnlyColumnCollection[str, ColumnElement[Any]]: + ) -> ReadOnlyColumnCollection[str, KeyedColumnElement[Any]]: return self.c - columns: ReadOnlyColumnCollection[str, ColumnElement[Any]] + columns: ReadOnlyColumnCollection[str, KeyedColumnElement[Any]] """A namespace of SQL expressions referred to by this :class:`.Bundle`. @@ -1402,7 +1422,7 @@ class Bundle( """ - c: ReadOnlyColumnCollection[str, ColumnElement[Any]] + c: ReadOnlyColumnCollection[str, KeyedColumnElement[Any]] """An alias for :attr:`.Bundle.columns`.""" def _clone(self): @@ -1908,9 +1928,10 @@ def _extract_mapped_subtype( raw_annotation: Union[type, str], cls: type, key: str, - attr_cls: type, + attr_cls: Type[Any], required: bool, is_dataclass_field: bool, + superclasses: Optional[Tuple[Type[Any], ...]] = None, ) -> Optional[Union[type, str]]: if raw_annotation is None: @@ -1930,9 +1951,13 @@ def _extract_mapped_subtype( if is_dataclass_field: return annotated else: - if ( - not hasattr(annotated, "__origin__") - or not issubclass(annotated.__origin__, attr_cls) # type: ignore + # TODO: there don't seem to be tests for the failure + # conditions here + if not hasattr(annotated, "__origin__") or ( + not issubclass( + annotated.__origin__, # type: ignore + superclasses if superclasses else attr_cls, + ) and not issubclass(attr_cls, annotated.__origin__) # type: ignore ): our_annotated_str = ( |
