From 4999784664b9e73204474dd3dd91ee60fd174e3e Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 9 Jan 2022 11:49:02 -0500 Subject: 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 --- lib/sqlalchemy/sql/base.py | 7 +- lib/sqlalchemy/sql/coercions.py | 94 +------ lib/sqlalchemy/sql/elements.py | 559 ++++++++++++++++++++------------------- lib/sqlalchemy/sql/functions.py | 7 +- lib/sqlalchemy/sql/selectable.py | 26 +- lib/sqlalchemy/sql/sqltypes.py | 14 +- lib/sqlalchemy/sql/type_api.py | 5 +- 7 files changed, 331 insertions(+), 381 deletions(-) (limited to 'lib/sqlalchemy/sql') diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py index 6ab9a75f6..7841ce88a 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 3bec73f7d..d5a75a165 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 705a89889..65f345fb3 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