From: Mike Bayer Date: Sun, 9 Jan 2022 16:49:02 +0000 (-0500) Subject: Initial ORM typing layout X-Git-Tag: rel_2_0_0b1~543^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4999784664b9e73204474dd3dd91ee60fd174e3e;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Initial ORM typing layout introduces: 1. new mapped_column() helper 2. DeclarativeBase helper 3. declared_attr has been re-typed 4. rework of Mapped[] to return InstrumentedAtribute for class get, so works without Mapped itself having expression methods 5. ORM constructs now generic on [_T] also includes some early typing work, most of which will be in later commits: 1. URL and History become typing.NamedTuple 2. come up with type-checking friendly way of type checking cy extensions, where type checking will be applied to the py versions, just needed to come up with a succinct conditional pattern for the imports References: #6810 References: #7535 References: #7562 Change-Id: Ie5d9a44631626c021d130ca4ce395aba623c71fb --- diff --git a/doc/build/orm/mapping_api.rst b/doc/build/orm/mapping_api.rst index eeba54040c..4d9960e05b 100644 --- a/doc/build/orm/mapping_api.rst +++ b/doc/build/orm/mapping_api.rst @@ -7,15 +7,25 @@ Class Mapping API .. autoclass:: registry :members: +.. autofunction:: add_mapped_attribute + .. autofunction:: declarative_base .. autofunction:: declarative_mixin .. autofunction:: as_declarative +.. autofunction:: mapped_column + .. autoclass:: declared_attr :members: +.. autoclass:: DeclarativeBaseNoMeta + :members: + +.. autoclass:: DeclarativeBase + :members: + .. autofunction:: has_inherited_table .. autofunction:: synonym_for diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 15c9b1d951..3f916fea07 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -6,7 +6,6 @@ # the MIT License: https://www.opensource.org/licenses/mit-license.php """Define generic result set constructs.""" - import collections.abc as collections_abc import functools import itertools @@ -19,11 +18,13 @@ from .. import util from ..sql.base import _generative from ..sql.base import HasMemoized from ..sql.base import InPlaceGenerative +from ..util._has_cy import HAS_CYEXTENSION -try: - from sqlalchemy.cyextension.resultproxy import tuplegetter -except ImportError: + +if typing.TYPE_CHECKING or not HAS_CYEXTENSION: from ._py_row import tuplegetter +else: + from sqlalchemy.cyextension.resultproxy import tuplegetter class ResultMetaData: diff --git a/lib/sqlalchemy/engine/row.py b/lib/sqlalchemy/engine/row.py index 39c69c2ffc..16215ccc47 100644 --- a/lib/sqlalchemy/engine/row.py +++ b/lib/sqlalchemy/engine/row.py @@ -7,21 +7,21 @@ """Define row constructs including :class:`.Row`.""" - import collections.abc as collections_abc import operator +import typing from ..sql import util as sql_util +from ..util._has_cy import HAS_CYEXTENSION - -try: - from sqlalchemy.cyextension.resultproxy import BaseRow - from sqlalchemy.cyextension.resultproxy import KEY_INTEGER_ONLY - from sqlalchemy.cyextension.resultproxy import KEY_OBJECTS_ONLY -except ImportError: +if typing.TYPE_CHECKING or not HAS_CYEXTENSION: from ._py_row import BaseRow from ._py_row import KEY_INTEGER_ONLY from ._py_row import KEY_OBJECTS_ONLY +else: + from sqlalchemy.cyextension.resultproxy import BaseRow + from sqlalchemy.cyextension.resultproxy import KEY_INTEGER_ONLY + from sqlalchemy.cyextension.resultproxy import KEY_OBJECTS_ONLY class Row(BaseRow, collections_abc.Sequence): diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py index 9157ff008a..ec5ab2bec7 100644 --- a/lib/sqlalchemy/engine/url.py +++ b/lib/sqlalchemy/engine/url.py @@ -16,6 +16,11 @@ be used directly and is also accepted directly by ``create_engine()``. import collections.abc as collections_abc import re +from typing import Dict +from typing import NamedTuple +from typing import Optional +from typing import Tuple +from typing import Union from urllib.parse import parse_qsl from urllib.parse import quote_plus from urllib.parse import unquote @@ -27,20 +32,7 @@ from ..dialects import plugins from ..dialects import registry -class URL( - util.namedtuple( - "URL", - [ - "drivername", - "username", - "password", - "host", - "port", - "database", - "query", - ], - ) -): +class URL(NamedTuple): """ Represent the components of a URL used to connect to a database. @@ -86,17 +78,13 @@ class URL( """ - def __new__(self, *arg, **kw): - if kw.pop("_new_ok", False): - return super(URL, self).__new__(self, *arg, **kw) - else: - util.warn_deprecated( - "Calling URL() directly is deprecated and will be disabled " - "in a future release. The public constructor for URL is " - "now the URL.create() method.", - "1.4", - ) - return URL.create(*arg, **kw) + drivername: str + username: Optional[str] + password: Optional[str] + host: Optional[str] + port: Optional[int] + database: Optional[str] + query: Dict[str, Union[str, Tuple[str]]] @classmethod def create( @@ -153,7 +141,6 @@ class URL( cls._assert_port(port), cls._assert_none_str(database, "database"), cls._str_dict(query), - _new_ok=True, ) @classmethod @@ -264,10 +251,10 @@ class URL( if query is not None: kw["query"] = query - return self._replace(**kw) + return self._assert_replace(**kw) - def _replace(self, **kw): - """Override ``namedtuple._replace()`` to provide argument checking.""" + def _assert_replace(self, **kw): + """argument checks before calling _replace()""" if "drivername" in kw: self._assert_str(kw["drivername"], "drivername") @@ -279,7 +266,7 @@ class URL( if "query" in kw: kw["query"] = self._str_dict(kw["query"]) - return super(URL, self)._replace(**kw) + return self._replace(**kw) def update_query_string(self, query_string, append=False): """Return a new :class:`_engine.URL` object with the :attr:`_engine.URL.query` @@ -467,7 +454,6 @@ class URL( for key in set(self.query).difference(names) } ), - _new_ok=True, ) @util.memoized_property diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py index 52817e8381..c7d9d4f887 100644 --- a/lib/sqlalchemy/ext/hybrid.py +++ b/lib/sqlalchemy/ext/hybrid.py @@ -802,10 +802,15 @@ advanced and/or patient developers, there's probably a whole lot of amazing things it can be used for. """ # noqa +from typing import Any +from typing import TypeVar + from .. import util from ..orm import attributes from ..orm import interfaces +_T = TypeVar("_T", bound=Any) + HYBRID_METHOD = util.symbol("HYBRID_METHOD") """Symbol indicating an :class:`InspectionAttr` that's of type :class:`.hybrid_method`. @@ -1147,7 +1152,7 @@ class hybrid_property(interfaces.InspectionAttrInfo): return expr_comparator -class Comparator(interfaces.PropComparator): +class Comparator(interfaces.PropComparator[_T]): """A helper class that allows easy construction of custom :class:`~.orm.interfaces.PropComparator` classes for usage with hybrids.""" @@ -1168,7 +1173,7 @@ class Comparator(interfaces.PropComparator): return self -class ExprComparator(Comparator): +class ExprComparator(Comparator[_T]): def __init__(self, cls, expression, hybrid): self.cls = cls self.expression = expression diff --git a/lib/sqlalchemy/ext/mypy/names.py b/lib/sqlalchemy/ext/mypy/names.py index 8ec15a6d43..b6f911979c 100644 --- a/lib/sqlalchemy/ext/mypy/names.py +++ b/lib/sqlalchemy/ext/mypy/names.py @@ -104,7 +104,7 @@ _lookup: Dict[str, Tuple[int, Set[str]]] = { }, ), "TypeEngine": (TYPEENGINE, {"sqlalchemy.sql.type_api.TypeEngine"}), - "Mapped": (MAPPED, {"sqlalchemy.orm.attributes.Mapped"}), + "Mapped": (MAPPED, {NAMED_TYPE_SQLA_MAPPED}), "declarative_base": ( DECLARATIVE_BASE, { diff --git a/lib/sqlalchemy/ext/mypy/plugin.py b/lib/sqlalchemy/ext/mypy/plugin.py index 8687012a1e..0a21feb51f 100644 --- a/lib/sqlalchemy/ext/mypy/plugin.py +++ b/lib/sqlalchemy/ext/mypy/plugin.py @@ -112,6 +112,8 @@ class SQLAlchemyPlugin(Plugin): self, file: MypyFile ) -> List[Tuple[int, str, int]]: return [ + # + (10, "sqlalchemy.orm", -1), (10, "sqlalchemy.orm.attributes", -1), (10, "sqlalchemy.orm.decl_api", -1), ] @@ -270,7 +272,7 @@ def _add_globals(ctx: Union[ClassDefContext, DynamicClassDefContext]) -> None: """ - util.add_global(ctx, "sqlalchemy.orm.attributes", "Mapped", "__sa_Mapped") + util.add_global(ctx, "sqlalchemy.orm", "Mapped", "__sa_Mapped") def _set_declarative_metaclass( diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 17167a7de1..55f2f31000 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -25,18 +25,22 @@ from ._orm_constructors import contains_alias as contains_alias from ._orm_constructors import create_session as create_session from ._orm_constructors import deferred as deferred from ._orm_constructors import dynamic_loader as dynamic_loader +from ._orm_constructors import mapped_column as mapped_column from ._orm_constructors import query_expression as query_expression from ._orm_constructors import relationship as relationship from ._orm_constructors import synonym as synonym from ._orm_constructors import with_loader_criteria as with_loader_criteria from .attributes import AttributeEvent as AttributeEvent from .attributes import InstrumentedAttribute as InstrumentedAttribute -from .attributes import Mapped as Mapped from .attributes import QueryableAttribute as QueryableAttribute +from .base import Mapped as Mapped from .context import QueryContext as QueryContext +from .decl_api import add_mapped_attribute as add_mapped_attribute from .decl_api import as_declarative as as_declarative from .decl_api import declarative_base as declarative_base from .decl_api import declarative_mixin as declarative_mixin +from .decl_api import DeclarativeBase as DeclarativeBase +from .decl_api import DeclarativeBaseNoMeta as DeclarativeBaseNoMeta from .decl_api import DeclarativeMeta as DeclarativeMeta from .decl_api import declared_attr as declared_attr from .decl_api import has_inherited_table as has_inherited_table diff --git a/lib/sqlalchemy/orm/_orm_constructors.py b/lib/sqlalchemy/orm/_orm_constructors.py index be0d23d00f..80607670eb 100644 --- a/lib/sqlalchemy/orm/_orm_constructors.py +++ b/lib/sqlalchemy/orm/_orm_constructors.py @@ -6,11 +6,16 @@ # the MIT License: https://www.opensource.org/licenses/mit-license.php import typing +from typing import Any from typing import Callable +from typing import Collection +from typing import Optional +from typing import overload from typing import Type from typing import Union from . import mapper as mapperlib +from .base import Mapped from .descriptor_props import CompositeProperty from .descriptor_props import SynonymProperty from .properties import ColumnProperty @@ -21,6 +26,11 @@ from .util import LoaderCriteriaOption from .. import sql from .. import util from ..exc import InvalidRequestError +from ..sql.schema import Column +from ..sql.schema import SchemaEventTarget +from ..sql.type_api import TypeEngine +from ..util.typing import Literal + _RC = typing.TypeVar("_RC") _T = typing.TypeVar("_T") @@ -41,9 +51,138 @@ def contains_alias(alias) -> "AliasOption": return AliasOption(alias) +@overload +def mapped_column( + *args: SchemaEventTarget, + nullable: bool = ..., + primary_key: bool = ..., + **kw: Any, +) -> "Mapped": + ... + + +@overload +def mapped_column( + __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"], + *args: SchemaEventTarget, + nullable: Union[Literal[None], Literal[True]] = ..., + primary_key: Union[Literal[None], Literal[False]] = ..., + **kw: Any, +) -> "Mapped[Optional[_T]]": + ... + + +@overload +def mapped_column( + __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"], + *args: SchemaEventTarget, + nullable: Union[Literal[None], Literal[True]] = ..., + primary_key: Union[Literal[None], Literal[False]] = ..., + **kw: Any, +) -> "Mapped[Optional[_T]]": + ... + + +@overload +def mapped_column( + __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"], + *args: SchemaEventTarget, + nullable: Union[Literal[None], Literal[False]] = ..., + primary_key: Literal[True] = True, + **kw: Any, +) -> "Mapped[_T]": + ... + + +@overload +def mapped_column( + __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"], + *args: SchemaEventTarget, + nullable: Literal[False] = ..., + primary_key: bool = ..., + **kw: Any, +) -> "Mapped[_T]": + ... + + +@overload +def mapped_column( + __name: str, + __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"], + *args: SchemaEventTarget, + nullable: Union[Literal[None], Literal[True]] = ..., + primary_key: Union[Literal[None], Literal[False]] = ..., + **kw: Any, +) -> "Mapped[Optional[_T]]": + ... + + +@overload +def mapped_column( + __name: str, + __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"], + *args: SchemaEventTarget, + nullable: Union[Literal[None], Literal[True]] = ..., + primary_key: Union[Literal[None], Literal[False]] = ..., + **kw: Any, +) -> "Mapped[Optional[_T]]": + ... + + +@overload +def mapped_column( + __name: str, + __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"], + *args: SchemaEventTarget, + nullable: Union[Literal[None], Literal[False]] = ..., + primary_key: Literal[True] = True, + **kw: Any, +) -> "Mapped[_T]": + ... + + +@overload +def mapped_column( + __name: str, + __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"], + *args: SchemaEventTarget, + nullable: Literal[False] = ..., + primary_key: bool = ..., + **kw: Any, +) -> "Mapped[_T]": + ... + + +def mapped_column(*args, **kw) -> "Mapped": + """construct a new ORM-mapped :class:`_schema.Column` construct. + + The :func:`_orm.mapped_column` function is shorthand for the construction + of a Core :class:`_schema.Column` object delivered within a + :func:`_orm.column_property` construct, which provides for consistent + typing information to be delivered to the class so that it works under + static type checkers such as mypy and delivers useful information in + IDE related type checkers such as pylance. The function can be used + in declarative mappings anywhere that :class:`_schema.Column` is normally + used:: + + from sqlalchemy.orm import mapped_column + + class User(Base): + __tablename__ = 'user' + + id = mapped_column(Integer) + name = mapped_column(String) + + + .. versionadded:: 2.0 + + """ + return column_property(Column(*args, **kw)) + + def column_property( column: sql.ColumnElement[_T], *additional_columns, **kwargs -) -> "ColumnProperty[_T]": +) -> "Mapped[_T]": r"""Provide a column-level property for use with a mapping. Column-based properties can normally be applied to the mapper's @@ -130,7 +269,7 @@ def column_property( return ColumnProperty(column, *additional_columns, **kwargs) -def composite(class_: Type[_T], *attrs, **kwargs) -> "CompositeProperty[_T]": +def composite(class_: Type[_T], *attrs, **kwargs) -> "Mapped[_T]": r"""Return a composite column-based property for use with a Mapper. See the mapping documentation section :ref:`mapper_composite` for a @@ -359,13 +498,106 @@ def with_loader_criteria( ) +@overload +def relationship( + argument: Union[str, Type[_RC], Callable[[], Type[_RC]]], + secondary=None, + *, + uselist: Literal[True] = None, + primaryjoin=None, + secondaryjoin=None, + foreign_keys=None, + order_by=False, + backref=None, + back_populates=None, + overlaps=None, + post_update=False, + cascade=False, + viewonly=False, + lazy="select", + collection_class=None, + passive_deletes=RelationshipProperty._persistence_only["passive_deletes"], + passive_updates=RelationshipProperty._persistence_only["passive_updates"], + remote_side=None, + enable_typechecks=RelationshipProperty._persistence_only[ + "enable_typechecks" + ], + join_depth=None, + comparator_factory=None, + single_parent=False, + innerjoin=False, + distinct_target_key=None, + doc=None, + active_history=RelationshipProperty._persistence_only["active_history"], + cascade_backrefs=RelationshipProperty._persistence_only[ + "cascade_backrefs" + ], + load_on_pending=False, + bake_queries=True, + _local_remote_pairs=None, + query_class=None, + info=None, + omit_join=None, + sync_backref=None, + _legacy_inactive_history_style=False, +) -> Mapped[Collection[_RC]]: + ... + + +@overload +def relationship( + argument: Union[str, Type[_RC], Callable[[], Type[_RC]]], + secondary=None, + *, + uselist: Optional[bool] = None, + primaryjoin=None, + secondaryjoin=None, + foreign_keys=None, + order_by=False, + backref=None, + back_populates=None, + overlaps=None, + post_update=False, + cascade=False, + viewonly=False, + lazy="select", + collection_class=None, + passive_deletes=RelationshipProperty._persistence_only["passive_deletes"], + passive_updates=RelationshipProperty._persistence_only["passive_updates"], + remote_side=None, + enable_typechecks=RelationshipProperty._persistence_only[ + "enable_typechecks" + ], + join_depth=None, + comparator_factory=None, + single_parent=False, + innerjoin=False, + distinct_target_key=None, + doc=None, + active_history=RelationshipProperty._persistence_only["active_history"], + cascade_backrefs=RelationshipProperty._persistence_only[ + "cascade_backrefs" + ], + load_on_pending=False, + bake_queries=True, + _local_remote_pairs=None, + query_class=None, + info=None, + omit_join=None, + sync_backref=None, + _legacy_inactive_history_style=False, +) -> Mapped[_RC]: + ... + + def relationship( argument: Union[str, Type[_RC], Callable[[], Type[_RC]]], secondary=None, + *, primaryjoin=None, secondaryjoin=None, foreign_keys=None, - uselist=None, + uselist: Optional[bool] = None, order_by=False, backref=None, back_populates=None, @@ -399,7 +631,7 @@ def relationship( omit_join=None, sync_backref=None, _legacy_inactive_history_style=False, -) -> RelationshipProperty[_RC]: +) -> Mapped[_RC]: """Provide a relationship between two mapped classes. This corresponds to a parent-child or associative table relationship. @@ -1261,7 +1493,7 @@ def synonym( comparator_factory=None, doc=None, info=None, -) -> "SynonymProperty": +) -> "Mapped": """Denote an attribute name as a synonym to a mapped property, in that the attribute will mirror the value and expression behavior of another attribute. diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index d24250ea04..5a605b7c65 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -13,10 +13,14 @@ defines a large part of the ORM's interactivity. """ - +from collections import namedtuple import operator -from typing import Generic +from typing import Any +from typing import List +from typing import NamedTuple +from typing import Tuple from typing import TypeVar +from typing import Union from . import collections from . import exc as orm_exc @@ -57,6 +61,8 @@ from ..sql import roles from ..sql import traversals from ..sql import visitors +_T = TypeVar("_T") + class NoKey(str): pass @@ -67,9 +73,9 @@ NO_KEY = NoKey("no name") @inspection._self_inspects class QueryableAttribute( - interfaces._MappedAttribute, + interfaces._MappedAttribute[_T], interfaces.InspectionAttr, - interfaces.PropComparator, + interfaces.PropComparator[_T], traversals.HasCopyInternals, roles.JoinTargetRole, roles.OnClauseRole, @@ -362,80 +368,7 @@ def _queryable_attribute_unreduce(key, mapped_class, parententity, entity): return getattr(entity, key) -_T = TypeVar("_T") -_Generic_T = Generic[_T] - - -class Mapped(QueryableAttribute, _Generic_T): - """Represent an ORM mapped :term:`descriptor` attribute for typing purposes. - - This class represents the complete descriptor interface for any class - attribute that will have been :term:`instrumented` by the ORM - :class:`_orm.Mapper` class. When used with typing stubs, it is the final - type that would be used by a type checker such as mypy to provide the full - behavioral contract for the attribute. - - .. tip:: - - The :class:`_orm.Mapped` class represents attributes that are handled - directly by the :class:`_orm.Mapper` class. It does not include other - Python descriptor classes that are provided as extensions, including - :ref:`hybrids_toplevel` and the :ref:`associationproxy_toplevel`. - While these systems still make use of ORM-specific superclasses - and structures, they are not :term:`instrumented` by the - :class:`_orm.Mapper` and instead provide their own functionality - when they are accessed on a class. - - When using the :ref:`SQLAlchemy Mypy plugin `, the - :class:`_orm.Mapped` construct is used in typing annotations to indicate to - the plugin those attributes that are expected to be mapped; the plugin also - applies :class:`_orm.Mapped` as an annotation automatically when it scans - through declarative mappings in :ref:`orm_declarative_table` style. For - more indirect mapping styles such as - :ref:`imperative table ` it is - typically applied explicitly to class level attributes that expect - to be mapped based on a given :class:`_schema.Table` configuration. - - :class:`_orm.Mapped` is defined in the - `sqlalchemy2-stubs `_ project - as a :pep:`484` generic class which may subscribe to any arbitrary Python - type, which represents the Python type handled by the attribute:: - - class MyMappedClass(Base): - __table_ = Table( - "some_table", Base.metadata, - Column("id", Integer, primary_key=True), - Column("data", String(50)), - Column("created_at", DateTime) - ) - - id : Mapped[int] - data: Mapped[str] - created_at: Mapped[datetime] - - For complete background on how to use :class:`_orm.Mapped` with - pep-484 tools like Mypy, see the link below for background on SQLAlchemy's - Mypy plugin. - - .. versionadded:: 1.4 - - .. seealso:: - - :ref:`mypy_toplevel` - complete background on Mypy integration - - """ - - def __get__(self, instance, owner): - raise NotImplementedError() - - def __set__(self, instance, value): - raise NotImplementedError() - - def __delete__(self, instance): - raise NotImplementedError() - - -class InstrumentedAttribute(Mapped): +class InstrumentedAttribute(QueryableAttribute[_T]): """Class bound instrumented attribute which adds basic :term:`descriptor` methods. @@ -469,9 +402,7 @@ class InstrumentedAttribute(Mapped): return self.impl.get(state, dict_) -HasEntityNamespace = util.namedtuple( - "HasEntityNamespace", ["entity_namespace"] -) +HasEntityNamespace = namedtuple("HasEntityNamespace", ["entity_namespace"]) HasEntityNamespace.is_mapper = HasEntityNamespace.is_aliased_class = False @@ -1837,7 +1768,7 @@ _NO_HISTORY = util.symbol("NO_HISTORY") _NO_STATE_SYMBOLS = frozenset([id(PASSIVE_NO_RESULT), id(NO_VALUE)]) -class History(util.namedtuple("History", ["added", "unchanged", "deleted"])): +class History(NamedTuple): """A 3-tuple of added, unchanged and deleted values, representing the changes which have occurred on an instrumented attribute. @@ -1862,11 +1793,13 @@ class History(util.namedtuple("History", ["added", "unchanged", "deleted"])): """ + added: Union[Tuple[()], List[Any]] + unchanged: Union[Tuple[()], List[Any]] + deleted: Union[Tuple[()], List[Any]] + def __bool__(self): return self != HISTORY_BLANK - __nonzero__ = __bool__ - def empty(self): """Return True if this :class:`.History` has no changes and no existing, unchanged state. @@ -2012,7 +1945,7 @@ class History(util.namedtuple("History", ["added", "unchanged", "deleted"])): ) -HISTORY_BLANK = History(None, None, None) +HISTORY_BLANK = History((), (), ()) def get_history(obj, key, passive=PASSIVE_OFF): diff --git a/lib/sqlalchemy/orm/base.py b/lib/sqlalchemy/orm/base.py index 93e2d609ad..7ab4b77375 100644 --- a/lib/sqlalchemy/orm/base.py +++ b/lib/sqlalchemy/orm/base.py @@ -13,17 +13,25 @@ import operator import typing from typing import Any from typing import Generic +from typing import overload from typing import TypeVar +from typing import Union from . import exc from .. import exc as sa_exc from .. import inspection from .. import util +from ..sql.elements import SQLCoreOperations from ..util import typing as compat_typing +from ..util.langhelpers import TypingOnly +if typing.TYPE_CHECKING: + from .attributes import InstrumentedAttribute + _T = TypeVar("_T", bound=Any) + PASSIVE_NO_RESULT = util.symbol( "PASSIVE_NO_RESULT", """Symbol returned by a loader callable or other attribute/history @@ -579,7 +587,88 @@ class InspectionAttrInfo(InspectionAttr): return {} -class _MappedAttribute(Generic[_T]): +class SQLORMOperations(SQLCoreOperations[_T], TypingOnly): + __slots__ = () + + if typing.TYPE_CHECKING: + + def of_type(self, class_): + ... + + def and_(self, *criteria): + ... + + def any(self, criterion=None, **kwargs): # noqa A001 + ... + + def has(self, criterion=None, **kwargs): + ... + + +class Mapped(Generic[_T], util.TypingOnly): + """Represent an ORM mapped attribute for typing purposes. + + This class represents the complete descriptor interface for any class + attribute that will have been :term:`instrumented` by the ORM + :class:`_orm.Mapper` class. Provides appropriate information to type + checkers such as pylance and mypy so that ORM-mapped attributes + are correctly typed. + + .. tip:: + + The :class:`_orm.Mapped` class represents attributes that are handled + directly by the :class:`_orm.Mapper` class. It does not include other + Python descriptor classes that are provided as extensions, including + :ref:`hybrids_toplevel` and the :ref:`associationproxy_toplevel`. + While these systems still make use of ORM-specific superclasses + and structures, they are not :term:`instrumented` by the + :class:`_orm.Mapper` and instead provide their own functionality + when they are accessed on a class. + + .. versionadded:: 1.4 + + + """ + + __slots__ = () + + if typing.TYPE_CHECKING: + + @overload + def __get__( + self, instance: None, owner: Any + ) -> "InstrumentedAttribute[_T]": + ... + + @overload + def __get__(self, instance: object, owner: Any) -> _T: + ... + + def __get__( + self, instance: object, owner: Any + ) -> Union["InstrumentedAttribute[_T]", _T]: + ... + + @classmethod + def _empty_constructor(cls, arg1: Any) -> "SQLORMOperations[_T]": + ... + + @overload + def __set__(self, instance: Any, value: _T) -> None: + ... + + @overload + def __set__(self, instance: Any, value: SQLCoreOperations) -> None: + ... + + def __set__(self, instance, value): + ... + + def __delete__(self, instance: Any): + ... + + +class _MappedAttribute(Mapped[_T], TypingOnly): """Mixin for attributes which should be replaced by mapper-assigned attributes. diff --git a/lib/sqlalchemy/orm/decl_api.py b/lib/sqlalchemy/orm/decl_api.py index 99b2e9b6f1..00c5574fa8 100644 --- a/lib/sqlalchemy/orm/decl_api.py +++ b/lib/sqlalchemy/orm/decl_api.py @@ -7,6 +7,13 @@ """Public API functions and helpers for declarative.""" import itertools import re +import typing +from typing import Any +from typing import Callable +from typing import ClassVar +from typing import Optional +from typing import TypeVar +from typing import Union import weakref from . import attributes @@ -15,7 +22,9 @@ from . import exc as orm_exc from . import instrumentation from . import interfaces from . import mapperlib +from .attributes import InstrumentedAttribute from .base import _inspect_mapped_class +from .base import Mapped from .decl_base import _add_attribute from .decl_base import _as_declarative from .decl_base import _declarative_constructor @@ -23,13 +32,18 @@ from .decl_base import _DeferredMapperConfig from .decl_base import _del_attribute from .decl_base import _mapper from .descriptor_props import SynonymProperty as _orm_synonym +from .mapper import Mapper from .. import exc from .. import inspection from .. import util +from ..sql.elements import SQLCoreOperations from ..sql.schema import MetaData +from ..sql.selectable import FromClause from ..util import hybridmethod from ..util import hybridproperty +_T = TypeVar("_T", bound=Any) + def has_inherited_table(cls): """Given a class, return True if any of the classes it inherits from has a @@ -50,11 +64,21 @@ def has_inherited_table(cls): return False -class DeclarativeMeta(type): - # DeclarativeMeta could be replaced by __subclass_init__() - # except for the class-level __setattr__() and __delattr__ hooks, - # which are still very important. +class DeclarativeAttributeIntercept(type): + """Metaclass that may be used in conjunction with the + :class:`_orm.DeclarativeBase` class to support addition of class + attributes dynamically. + + """ + + def __setattr__(cls, key, value): + _add_attribute(cls, key, value) + + def __delattr__(cls, key): + _del_attribute(cls, key) + +class DeclarativeMeta(type): def __init__(cls, classname, bases, dict_, **kw): # early-consume registry from the initial declarative base, # assign privately to not conflict with subclass attributes named @@ -121,7 +145,7 @@ def synonym_for(name, map_column=False): return decorate -class declared_attr(interfaces._MappedAttribute, property): +class declared_attr(interfaces._MappedAttribute[_T]): """Mark a class-level method as representing the definition of a mapped property or special declarative member name. @@ -204,39 +228,52 @@ class declared_attr(interfaces._MappedAttribute, property): """ # noqa E501 - def __init__(self, fget, cascading=False): - super(declared_attr, self).__init__(fget) - self.__doc__ = fget.__doc__ + if typing.TYPE_CHECKING: + + def __set__(self, instance, value): + ... + + def __delete__(self, instance: Any): + ... + + def __init__( + self, + fn: Callable[..., Union[Mapped[_T], SQLCoreOperations[_T]]], + cascading=False, + ): + self.fget = fn self._cascading = cascading + self.__doc__ = fn.__doc__ - def __get__(desc, self, cls): + def __get__(self, instance, owner) -> InstrumentedAttribute[_T]: # the declared_attr needs to make use of a cache that exists # for the span of the declarative scan_attributes() phase. # to achieve this we look at the class manager that's configured. + cls = owner manager = attributes.manager_of_class(cls) if manager is None: - if not re.match(r"^__.+__$", desc.fget.__name__): + if not re.match(r"^__.+__$", self.fget.__name__): # if there is no manager at all, then this class hasn't been # run through declarative or mapper() at all, emit a warning. util.warn( "Unmanaged access of declarative attribute %s from " - "non-mapped class %s" % (desc.fget.__name__, cls.__name__) + "non-mapped class %s" % (self.fget.__name__, cls.__name__) ) - return desc.fget(cls) + 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 desc.fget(cls) + return self.fget(cls) # here, we are inside of the declarative scan. use the registry # that is tracking the values of these attributes. declarative_scan = manager.declarative_scan reg = declarative_scan.declared_attr_reg - if desc in reg: - return reg[desc] + if self in reg: + return reg[self] else: - reg[desc] = obj = desc.fget(cls) + reg[self] = obj = self.fget(cls) return obj @hybridmethod @@ -361,6 +398,115 @@ def declarative_mixin(cls): return cls +def _setup_declarative_base(cls): + if "metadata" in cls.__dict__: + metadata = cls.metadata + else: + metadata = None + + reg = cls.__dict__.get("registry", None) + if reg is not None: + if not isinstance(reg, registry): + raise exc.InvalidRequestError( + "Declarative base class has a 'registry' attribute that is " + "not an instance of sqlalchemy.orm.registry()" + ) + else: + reg = registry(metadata=metadata) + cls.registry = reg + + cls._sa_registry = reg + + if "metadata" not in cls.__dict__: + cls.metadata = cls.registry.metadata + + +class DeclarativeBaseNoMeta: + """Same as :class:`_orm.DeclarativeBase`, but does not use a metaclass + to intercept new attributes. + + The :class:`_orm.DeclarativeBaseNoMeta` base may be used when use of + custom metaclasses is desirable. + + .. versionadded:: 2.0 + + + """ + + registry: ClassVar["registry"] + _sa_registry: ClassVar["registry"] + metadata: ClassVar[MetaData] + __mapper__: ClassVar[Mapper] + __table__: Optional[FromClause] + + if typing.TYPE_CHECKING: + + def __init__(self, **kw: Any): + ... + + def __init_subclass__(cls) -> None: + if DeclarativeBaseNoMeta in cls.__bases__: + _setup_declarative_base(cls) + else: + cls._sa_registry.map_declaratively(cls) + + +class DeclarativeBase(metaclass=DeclarativeAttributeIntercept): + """Base class used for declarative class definitions. + + The :class:`_orm.DeclarativeBase` allows for the creation of new + declarative bases in such a way that is compatible with type checkers:: + + + from sqlalchemy.orm import DeclarativeBase + + class Base(DeclarativeBase): + pass + + + The above ``Base`` class is now usable as the base for new declarative + mappings. The superclass makes use of the ``__init_subclass__()`` + method to set up new classes and metaclasses aren't used. + + .. versionadded:: 2.0 + + """ + + registry: ClassVar["registry"] + _sa_registry: ClassVar["registry"] + metadata: ClassVar[MetaData] + __mapper__: ClassVar[Mapper] + __table__: Optional[FromClause] + + if typing.TYPE_CHECKING: + + def __init__(self, **kw: Any): + ... + + def __init_subclass__(cls) -> None: + if DeclarativeBase in cls.__bases__: + _setup_declarative_base(cls) + else: + cls._sa_registry.map_declaratively(cls) + + +def add_mapped_attribute(target, key, attr): + """Add a new mapped attribute to an ORM mapped class. + + E.g.:: + + add_mapped_attribute(User, "addresses", relationship(Address)) + + This may be used for ORM mappings that aren't using a declarative + metaclass that intercepts attribute set operations. + + .. versionadded:: 2.0 + + + """ + _add_attribute(target, key, attr) + + def declarative_base( metadata=None, mapper=None, @@ -369,7 +515,7 @@ def declarative_base( constructor=_declarative_constructor, class_registry=None, metaclass=DeclarativeMeta, -): +) -> Any: r"""Construct a base class for declarative class definitions. The new base class will be given a metaclass that produces @@ -1010,7 +1156,9 @@ def as_declarative(**kw): ).as_declarative_base(**kw) -@inspection._inspects(DeclarativeMeta) +@inspection._inspects( + DeclarativeMeta, DeclarativeBase, DeclarativeAttributeIntercept +) def _inspect_decl_meta(cls): mp = _inspect_mapped_class(cls) if mp is None: diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index 80fce86d0d..5e67b64cd9 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -28,6 +28,7 @@ from ..sql import expression from ..sql import operators _T = TypeVar("_T", bound=Any) +_PT = TypeVar("_PT", bound=Any) class DescriptorProperty(MapperProperty[_T]): @@ -362,7 +363,7 @@ class CompositeProperty(DescriptorProperty[_T]): return proc - class Comparator(PropComparator): + class Comparator(PropComparator[_PT]): """Produce boolean, comparison, and other operators for :class:`.CompositeProperty` attributes. @@ -448,7 +449,7 @@ class CompositeProperty(DescriptorProperty[_T]): return str(self.parent.class_.__name__) + "." + self.key -class ConcreteInheritedProperty(DescriptorProperty): +class ConcreteInheritedProperty(DescriptorProperty[_T]): """A 'do nothing' :class:`.MapperProperty` that disables an attribute on a concrete subclass that is only present on the inherited mapper, not the concrete classes' mapper. @@ -501,7 +502,7 @@ class ConcreteInheritedProperty(DescriptorProperty): self.descriptor = NoninheritedConcreteProp() -class SynonymProperty(DescriptorProperty): +class SynonymProperty(DescriptorProperty[_T]): def __init__( self, name, diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index df265db575..08189a1b75 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -17,7 +17,9 @@ are exposed when inspecting mappings. """ import collections +import typing from typing import Any +from typing import cast from typing import TypeVar from . import exc as orm_exc @@ -32,6 +34,7 @@ from .base import MANYTOMANY from .base import MANYTOONE from .base import NOT_EXTENSION from .base import ONETOMANY +from .base import SQLORMOperations from .. import inspect from .. import inspection from .. import util @@ -307,8 +310,10 @@ class MapperProperty( @inspection._self_inspects -class PropComparator(operators.ColumnOperators): - r"""Defines SQL operators for :class:`.MapperProperty` objects. +class PropComparator( + SQLORMOperations[_T], operators.ColumnOperators[SQLORMOperations] +): + r"""Defines SQL operations for ORM mapped attributes. SQLAlchemy allows for operators to be redefined at both the Core and ORM level. :class:`.PropComparator` @@ -316,12 +321,6 @@ class PropComparator(operators.ColumnOperators): including those of :class:`.ColumnProperty`, :class:`.RelationshipProperty`, and :class:`.CompositeProperty`. - .. note:: With the advent of Hybrid properties introduced in SQLAlchemy - 0.7, as well as Core-level operator redefinition in - SQLAlchemy 0.8, the use case for user-defined :class:`.PropComparator` - instances is extremely rare. See :ref:`hybrids_toplevel` as well - as :ref:`types_operators`. - User-defined subclasses of :class:`.PropComparator` may be created. The built-in Python comparison and math operator methods, such as :meth:`.operators.ColumnOperators.__eq__`, @@ -463,18 +462,34 @@ class PropComparator(operators.ColumnOperators): return self.property.info @staticmethod - def any_op(a, b, **kwargs): + def _any_op(a, b, **kwargs): return a.any(b, **kwargs) @staticmethod - def has_op(a, b, **kwargs): - return a.has(b, **kwargs) + def _has_op(left, other, **kwargs): + return left.has(other, **kwargs) @staticmethod - def of_type_op(a, class_): + def _of_type_op(a, class_): return a.of_type(class_) - def of_type(self, class_): + any_op = cast(operators.OperatorType, _any_op) + has_op = cast(operators.OperatorType, _has_op) + of_type_op = cast(operators.OperatorType, _of_type_op) + + if typing.TYPE_CHECKING: + + def operate( + self, op: operators.OperatorType, *other: Any, **kwargs: Any + ) -> "SQLORMOperations": + ... + + def reverse_operate( + self, op: operators.OperatorType, other: Any, **kwargs: Any + ) -> "SQLORMOperations": + ... + + def of_type(self, class_) -> "SQLORMOperations[_T]": r"""Redefine this object in terms of a polymorphic subclass, :func:`_orm.with_polymorphic` construct, or :func:`_orm.aliased` construct. @@ -500,7 +515,7 @@ class PropComparator(operators.ColumnOperators): return self.operate(PropComparator.of_type_op, class_) - def and_(self, *criteria): + def and_(self, *criteria) -> "SQLORMOperations[_T]": """Add additional criteria to the ON clause that's represented by this relationship attribute. @@ -528,7 +543,7 @@ class PropComparator(operators.ColumnOperators): """ return self.operate(operators.and_, *criteria) - def any(self, criterion=None, **kwargs): + def any(self, criterion=None, **kwargs) -> "SQLORMOperations[_T]": r"""Return true if this collection contains any member that meets the given criterion. @@ -546,7 +561,7 @@ class PropComparator(operators.ColumnOperators): return self.operate(PropComparator.any_op, criterion, **kwargs) - def has(self, criterion=None, **kwargs): + def has(self, criterion=None, **kwargs) -> "SQLORMOperations[_T]": r"""Return true if this element references a member which meets the given criterion. @@ -565,7 +580,7 @@ class PropComparator(operators.ColumnOperators): return self.operate(PropComparator.has_op, criterion, **kwargs) -class StrategizedProperty(MapperProperty): +class StrategizedProperty(MapperProperty[_T]): """A MapperProperty which uses selectable strategies to affect loading behavior. diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 8ee26315ef..c4aac5a385 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -13,7 +13,6 @@ mapped attributes. """ from typing import Any -from typing import Generic from typing import TypeVar from . import attributes @@ -32,6 +31,7 @@ from ..sql import coercions from ..sql import roles _T = TypeVar("_T", bound=Any) +_PT = TypeVar("_PT", bound=Any) __all__ = [ "ColumnProperty", @@ -43,7 +43,7 @@ __all__ = [ @log.class_logger -class ColumnProperty(StrategizedProperty, Generic[_T]): +class ColumnProperty(StrategizedProperty[_T]): """Describes an object attribute that corresponds to a table column. Public constructor is the :func:`_orm.column_property` function. @@ -90,6 +90,7 @@ class ColumnProperty(StrategizedProperty, Generic[_T]): ) for c in columns ] + self.parent = self.key = None self.group = kwargs.pop("group", None) self.deferred = kwargs.pop("deferred", False) self.raiseload = kwargs.pop("raiseload", False) @@ -253,7 +254,7 @@ class ColumnProperty(StrategizedProperty, Generic[_T]): dest_dict, [self.key], no_loader=True ) - class Comparator(util.MemoizedSlots, PropComparator): + class Comparator(util.MemoizedSlots, PropComparator[_PT]): """Produce boolean, comparison, and other operators for :class:`.ColumnProperty` attributes. @@ -361,4 +362,6 @@ class ColumnProperty(StrategizedProperty, Generic[_T]): return op(col._bind_param(op, other), col, **kwargs) def __str__(self): + if not self.parent or not self.key: + return object.__repr__(self) return str(self.parent.class_.__name__) + "." + self.key diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index 330d454305..c5ea07051a 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -15,8 +15,8 @@ and `secondaryjoin` aspects of :func:`_orm.relationship`. """ import collections import re +from typing import Any from typing import Callable -from typing import Generic from typing import Type from typing import TypeVar from typing import Union @@ -53,7 +53,8 @@ from ..sql.util import join_condition from ..sql.util import selectables_overlap from ..sql.util import visit_binary_product -_RC = TypeVar("_RC") +_T = TypeVar("_T", bound=Any) +_PT = TypeVar("_PT", bound=Any) def remote(expr): @@ -96,7 +97,7 @@ def foreign(expr): @log.class_logger -class RelationshipProperty(StrategizedProperty, Generic[_RC]): +class RelationshipProperty(StrategizedProperty[_T]): """Describes an object property that holds a single item or list of items that correspond to a related database table. @@ -125,7 +126,7 @@ class RelationshipProperty(StrategizedProperty, Generic[_RC]): def __init__( self, - argument: Union[str, Type[_RC], Callable[[], Type[_RC]]], + argument: Union[str, Type[_T], Callable[[], Type[_T]]], secondary=None, primaryjoin=None, secondaryjoin=None, @@ -285,7 +286,7 @@ class RelationshipProperty(StrategizedProperty, Generic[_RC]): doc=self.doc, ) - class Comparator(PropComparator): + class Comparator(PropComparator[_PT]): """Produce boolean, comparison, and other operators for :class:`.RelationshipProperty` attributes. @@ -861,6 +862,8 @@ class RelationshipProperty(StrategizedProperty, Generic[_RC]): self.prop.parent._check_configure() return self.prop + comparator: Comparator[_T] + def _with_parent(self, instance, alias_secondary=True, from_entity=None): assert instance is not None adapt_source = None diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py index 6ab9a75f6f..7841ce88aa 100644 --- a/lib/sqlalchemy/sql/base.py +++ b/lib/sqlalchemy/sql/base.py @@ -31,11 +31,12 @@ from .. import util from ..util import HasMemoized from ..util import hybridmethod from ..util import typing as compat_typing +from ..util._has_cy import HAS_CYEXTENSION -try: - from sqlalchemy.cyextension.util import prefix_anon_map # noqa -except ImportError: +if typing.TYPE_CHECKING or not HAS_CYEXTENSION: from ._py_util import prefix_anon_map # noqa +else: + from sqlalchemy.cyextension.util import prefix_anon_map # noqa coercions = None elements = None diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py index 3bec73f7dc..d5a75a1658 100644 --- a/lib/sqlalchemy/sql/coercions.py +++ b/lib/sqlalchemy/sql/coercions.py @@ -10,12 +10,10 @@ import numbers import re import typing from typing import Any -from typing import Callable +from typing import Any as TODO_Any from typing import Optional -from typing import overload from typing import Type from typing import TypeVar -from typing import Union from . import operators from . import roles @@ -42,7 +40,6 @@ if typing.TYPE_CHECKING: from . import selectable from . import traversals from .elements import ClauseElement - from .elements import ColumnElement _SR = TypeVar("_SR", bound=roles.SQLRole) _StringOnlyR = TypeVar("_StringOnlyR", bound=roles.StringRole) @@ -129,59 +126,10 @@ def _expression_collection_was_a_list(attrname, fnname, args): return args -@overload -def expect( - role: Type[roles.InElementRole], - element: Any, - *, - apply_propagate_attrs: Optional["ClauseElement"] = None, - argname: Optional[str] = None, - post_inspect: bool = False, - **kw: Any, -) -> Union["elements.ColumnElement", "selectable.Select"]: - ... - - -@overload -def expect( - role: Type[roles.HasCTERole], - element: Any, - *, - apply_propagate_attrs: Optional["ClauseElement"] = None, - argname: Optional[str] = None, - post_inspect: bool = False, - **kw: Any, -) -> "selectable.HasCTE": - ... +# TODO; would like to have overloads here, however mypy is being extremely +# pedantic about them. not sure why pylance is OK with them. -@overload -def expect( - role: Type[roles.ExpressionElementRole], - element: Any, - *, - apply_propagate_attrs: Optional["ClauseElement"] = None, - argname: Optional[str] = None, - post_inspect: bool = False, - **kw: Any, -) -> "ColumnElement": - ... - - -@overload -def expect( - role: "Type[_StringOnlyR]", - element: Any, - *, - apply_propagate_attrs: Optional["ClauseElement"] = None, - argname: Optional[str] = None, - post_inspect: bool = False, - **kw: Any, -) -> str: - ... - - -@overload def expect( role: Type[_SR], element: Any, @@ -190,32 +138,7 @@ def expect( argname: Optional[str] = None, post_inspect: bool = False, **kw: Any, -) -> _SR: - ... - - -@overload -def expect( - role: Type[_SR], - element: Callable[..., Any], - *, - apply_propagate_attrs: Optional["ClauseElement"] = None, - argname: Optional[str] = None, - post_inspect: bool = False, - **kw: Any, -) -> "lambdas.LambdaElement": - ... - - -def expect( - role: Type[_SR], - element: Any, - *, - apply_propagate_attrs: Optional["ClauseElement"] = None, - argname: Optional[str] = None, - post_inspect: bool = False, - **kw: Any, -) -> Union[str, _SR, "lambdas.LambdaElement"]: +) -> TODO_Any: if ( role.allows_lambda # note callable() will not invoke a __getattr__() method, whereas @@ -350,7 +273,9 @@ class RoleImpl: self.name = role_class._role_name self._use_inspection = issubclass(role_class, roles.UsesInspection) - def _implicit_coercions(self, element, resolved, argname=None, **kw): + def _implicit_coercions( + self, element, resolved, argname=None, **kw + ) -> Any: self._raise_for_expected(element, argname, resolved) def _raise_for_expected( @@ -422,9 +347,8 @@ class _ColumnCoercions: "subquery.", ) - def _implicit_coercions( - self, original_element, resolved, argname=None, **kw - ): + def _implicit_coercions(self, element, resolved, argname=None, **kw): + original_element = element if not getattr(resolved, "is_clause_element", False): self._raise_for_expected(original_element, argname, resolved) elif resolved._is_select_statement: diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 705a898899..65f345fb38 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -50,6 +50,7 @@ from .visitors import Traversible from .. import exc from .. import inspection from .. import util +from ..util.langhelpers import TypingOnly if typing.TYPE_CHECKING: from decimal import Decimal @@ -572,262 +573,8 @@ class CompilerColumnElement( __slots__ = () -class ColumnElement( - roles.ColumnArgumentOrKeyRole, - roles.StatementOptionRole, - roles.WhereHavingRole, - roles.BinaryElementRole, - roles.OrderByRole, - roles.ColumnsClauseRole, - roles.LimitOffsetRole, - roles.DMLColumnRole, - roles.DDLConstraintColumnRole, - roles.DDLExpressionRole, - operators.ColumnOperators["ColumnElement"], - ClauseElement, - Generic[_T], -): - """Represent a column-oriented SQL expression suitable for usage in the - "columns" clause, WHERE clause etc. of a statement. - - While the most familiar kind of :class:`_expression.ColumnElement` is the - :class:`_schema.Column` object, :class:`_expression.ColumnElement` - serves as the basis - for any unit that may be present in a SQL expression, including - the expressions themselves, SQL functions, bound parameters, - literal expressions, keywords such as ``NULL``, etc. - :class:`_expression.ColumnElement` - is the ultimate base class for all such elements. - - A wide variety of SQLAlchemy Core functions work at the SQL expression - level, and are intended to accept instances of - :class:`_expression.ColumnElement` as - arguments. These functions will typically document that they accept a - "SQL expression" as an argument. What this means in terms of SQLAlchemy - usually refers to an input which is either already in the form of a - :class:`_expression.ColumnElement` object, - or a value which can be **coerced** into - one. The coercion rules followed by most, but not all, SQLAlchemy Core - functions with regards to SQL expressions are as follows: - - * a literal Python value, such as a string, integer or floating - point value, boolean, datetime, ``Decimal`` object, or virtually - any other Python object, will be coerced into a "literal bound - value". This generally means that a :func:`.bindparam` will be - produced featuring the given value embedded into the construct; the - resulting :class:`.BindParameter` object is an instance of - :class:`_expression.ColumnElement`. - The Python value will ultimately be sent - to the DBAPI at execution time as a parameterized argument to the - ``execute()`` or ``executemany()`` methods, after SQLAlchemy - type-specific converters (e.g. those provided by any associated - :class:`.TypeEngine` objects) are applied to the value. - - * any special object value, typically ORM-level constructs, which - feature an accessor called ``__clause_element__()``. The Core - expression system looks for this method when an object of otherwise - unknown type is passed to a function that is looking to coerce the - argument into a :class:`_expression.ColumnElement` and sometimes a - :class:`_expression.SelectBase` expression. - It is used within the ORM to - convert from ORM-specific objects like mapped classes and - mapped attributes into Core expression objects. - - * The Python ``None`` value is typically interpreted as ``NULL``, - which in SQLAlchemy Core produces an instance of :func:`.null`. - - A :class:`_expression.ColumnElement` provides the ability to generate new - :class:`_expression.ColumnElement` - objects using Python expressions. This means that Python operators - such as ``==``, ``!=`` and ``<`` are overloaded to mimic SQL operations, - and allow the instantiation of further :class:`_expression.ColumnElement` - instances - which are composed from other, more fundamental - :class:`_expression.ColumnElement` - objects. For example, two :class:`.ColumnClause` objects can be added - together with the addition operator ``+`` to produce - a :class:`.BinaryExpression`. - Both :class:`.ColumnClause` and :class:`.BinaryExpression` are subclasses - of :class:`_expression.ColumnElement`:: - - >>> from sqlalchemy.sql import column - >>> column('a') + column('b') - - >>> print(column('a') + column('b')) - a + b - - .. seealso:: - - :class:`_schema.Column` - - :func:`_expression.column` - - """ - - __visit_name__ = "column_element" - - primary_key = False - foreign_keys = [] - _proxies = () - - _tq_label = None - """The named label that can be used to target - this column in a result set in a "table qualified" context. - - This label is almost always the label used when - rendering AS