diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-01-09 11:49:02 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-01-14 16:30:41 -0500 |
| commit | 4999784664b9e73204474dd3dd91ee60fd174e3e (patch) | |
| tree | 18f612f9960d5abee702b1bc1e0769ca26728793 /lib/sqlalchemy/sql | |
| parent | 43f6ae639ca0186f4802255861acdc20f19e702f (diff) | |
| download | sqlalchemy-4999784664b9e73204474dd3dd91ee60fd174e3e.tar.gz | |
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
Diffstat (limited to 'lib/sqlalchemy/sql')
| -rw-r--r-- | lib/sqlalchemy/sql/base.py | 7 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/coercions.py | 94 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/elements.py | 559 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/functions.py | 7 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/selectable.py | 26 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/sqltypes.py | 14 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/type_api.py | 5 |
7 files changed, 331 insertions, 381 deletions
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') - <sqlalchemy.sql.expression.BinaryExpression object at 0x101029dd0> - >>> 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 <expr> AS <label> in a SELECT statement when using - the LABEL_STYLE_TABLENAME_PLUS_COL label style, which is what the legacy - ORM ``Query`` object uses as well. - - For a regular Column bound to a Table, this is typically the label - <tablename>_<columnname>. For other constructs, different rules - may apply, such as anonymized labels and others. - - .. versionchanged:: 1.4.21 renamed from ``._label`` - - """ - - key = None - """The 'key' that in some circumstances refers to this object in a - Python namespace. - - This typically refers to the "key" of the column as present in the - ``.c`` collection of a selectable, e.g. ``sometable.c["somekey"]`` would - return a :class:`_schema.Column` with a ``.key`` of "somekey". - - """ - - @HasMemoized.memoized_attribute - def _tq_key_label(self): - """A label-based version of 'key' that in some circumstances refers - to this object in a Python namespace. - - - _tq_key_label comes into play when a select() statement is constructed - with apply_labels(); in this case, all Column objects in the ``.c`` - collection are rendered as <tablename>_<columnname> in SQL; this is - essentially the value of ._label. But to locate those columns in the - ``.c`` collection, the name is along the lines of <tablename>_<key>; - that's the typical value of .key_label. - - .. versionchanged:: 1.4.21 renamed from ``._key_label`` - - """ - return self._proxy_key - - @property - def _key_label(self): - """legacy; renamed to _tq_key_label""" - return self._tq_key_label - - @property - def _label(self): - """legacy; renamed to _tq_label""" - return self._tq_label - - @property - def _non_anon_label(self): - """the 'name' that naturally applies this element when rendered in - SQL. - - Concretely, this is the "name" of a column or a label in a - SELECT statement; ``<columnname>`` and ``<labelname>`` below:: - - SELECT <columnmame> FROM table - - SELECT column AS <labelname> FROM table - - Above, the two names noted will be what's present in the DBAPI - ``cursor.description`` as the names. - - If this attribute returns ``None``, it means that the SQL element as - written does not have a 100% fully predictable "name" that would appear - in the ``cursor.description``. Examples include SQL functions, CAST - functions, etc. While such things do return names in - ``cursor.description``, they are only predictable on a - database-specific basis; e.g. an expression like ``MAX(table.col)`` may - appear as the string ``max`` on one database (like PostgreSQL) or may - appear as the whole expression ``max(table.col)`` on SQLite. - - The default implementation looks for a ``.name`` attribute on the - object, as has been the precedent established in SQLAlchemy for many - years. An exception is made on the ``FunctionElement`` subclass - so that the return value is always ``None``. - - .. versionadded:: 1.4.21 - - - - """ - return getattr(self, "name", None) - - _render_label_in_columns_clause = True - """A flag used by select._columns_plus_names that helps to determine - we are actually going to render in terms of "SELECT <col> AS <label>". - This flag can be returned as False for some Column objects that want - to be rendered as simple "SELECT <col>"; typically columns that don't have - any parent table and are named the same as what the label would be - in any case. - - """ - - _allow_label_resolve = True - """A flag that can be flipped to prevent a column from being resolvable - by string label name. - - The joined eager loader strategy in the ORM uses this, for example. - - """ - - _is_implicitly_boolean = False - - _alt_names = () - - def self_group(self, against=None): - if ( - against in (operators.and_, operators.or_, operators._asbool) - and self.type._type_affinity is type_api.BOOLEANTYPE._type_affinity - ): - return AsBoolean(self, operators.is_true, operators.is_false) - elif against in (operators.any_op, operators.all_op): - return Grouping(self) - else: - return self - - def _negate(self): - if self.type._type_affinity is type_api.BOOLEANTYPE._type_affinity: - return AsBoolean(self, operators.is_false, operators.is_true) - else: - return super(ColumnElement, self)._negate() - - @util.memoized_property - def type(self) -> "TypeEngine[_T]": - return type_api.NULLTYPE - - @HasMemoized.memoized_attribute - def comparator(self) -> "TypeEngine.Comparator[_T]": - try: - comparator_factory = self.type.comparator_factory - except AttributeError as err: - raise TypeError( - "Object %r associated with '.type' attribute " - "is not a TypeEngine class or object" % self.type - ) from err - else: - return comparator_factory(self) - - def __getattr__(self, key): - try: - return getattr(self.comparator, key) - except AttributeError as err: - raise AttributeError( - "Neither %r object nor %r object has an attribute %r" - % ( - type(self).__name__, - type(self.comparator).__name__, - key, - ) - ) from err +class SQLCoreOperations(Generic[_T], TypingOnly): + __slots__ = () # annotations for comparison methods # these are from operators->Operators / ColumnOperators, @@ -894,7 +641,9 @@ class ColumnElement( ... @overload - def concat(self, other: Any) -> "BinaryExpression[_ST]": + def concat( + self: "SQLCoreOperations[_ST]", other: Any + ) -> "BinaryExpression[_ST]": ... @overload @@ -986,7 +735,7 @@ class ColumnElement( ) -> "BinaryExpression[bool]": ... - def distinct(self: "ColumnElement[_T]") -> "UnaryExpression[_T]": + def distinct(self: "SQLCoreOperations[_T]") -> "UnaryExpression[_T]": ... def any_(self) -> "CollectionAggregate": @@ -996,22 +745,28 @@ class ColumnElement( ... # numeric overloads. These need more tweaking + # in particular they all need to have a variant for Optiona[_T] + # because Optional only applies to the data side, not the expression + # side @overload def __add__( - self: "ColumnElement[_NT]", other: "Union[ColumnElement[_NT], _NT]" + self: "Union[_SQO[_NT], _SQO[Optional[_NT]]]", + other: "Union[_SQO[Optional[_NT]], _SQO[_NT], _NT]", ) -> "BinaryExpression[_NT]": ... @overload def __add__( - self: "ColumnElement[_NT]", other: Any + self: "Union[_SQO[_NT], _SQO[Optional[_NT]]]", + other: Any, ) -> "BinaryExpression[_NUMERIC]": ... @overload def __add__( - self: "ColumnElement[_ST]", other: Any + self: "Union[_SQO[_ST], _SQO[Optional[_ST]]]", + other: Any, ) -> "BinaryExpression[_ST]": ... @@ -1031,7 +786,8 @@ class ColumnElement( @overload def __sub__( - self: "ColumnElement[_NT]", other: "Union[ColumnElement[_NT], _NT]" + self: "SQLCoreOperations[_NT]", + other: "Union[SQLCoreOperations[_NT], _NT]", ) -> "BinaryExpression[_NT]": ... @@ -1044,7 +800,7 @@ class ColumnElement( @overload def __rsub__( - self: "ColumnElement[_NT]", other: Any + self: "SQLCoreOperations[_NT]", other: Any ) -> "BinaryExpression[_NUMERIC]": ... @@ -1057,7 +813,7 @@ class ColumnElement( @overload def __mul__( - self: "ColumnElement[_NT]", other: Any + self: "SQLCoreOperations[_NT]", other: Any ) -> "BinaryExpression[_NUMERIC]": ... @@ -1070,7 +826,7 @@ class ColumnElement( @overload def __rmul__( - self: "ColumnElement[_NT]", other: Any + self: "SQLCoreOperations[_NT]", other: Any ) -> "BinaryExpression[_NUMERIC]": ... @@ -1083,7 +839,7 @@ class ColumnElement( @overload def __mod__( - self: "ColumnElement[_NT]", other: Any + self: "SQLCoreOperations[_NT]", other: Any ) -> "BinaryExpression[_NUMERIC]": ... @@ -1096,7 +852,7 @@ class ColumnElement( @overload def __rmod__( - self: "ColumnElement[_NT]", other: Any + self: "SQLCoreOperations[_NT]", other: Any ) -> "BinaryExpression[_NUMERIC]": ... @@ -1109,7 +865,7 @@ class ColumnElement( @overload def __truediv__( - self: "ColumnElement[_NT]", other: Any + self: "SQLCoreOperations[_NT]", other: Any ) -> "BinaryExpression[_NUMERIC]": ... @@ -1122,7 +878,7 @@ class ColumnElement( @overload def __rtruediv__( - self: "ColumnElement[_NT]", other: Any + self: "SQLCoreOperations[_NT]", other: Any ) -> "BinaryExpression[_NUMERIC]": ... @@ -1135,7 +891,7 @@ class ColumnElement( @overload def __floordiv__( - self: "ColumnElement[_NT]", other: Any + self: "SQLCoreOperations[_NT]", other: Any ) -> "BinaryExpression[_NUMERIC]": ... @@ -1148,7 +904,7 @@ class ColumnElement( @overload def __rfloordiv__( - self: "ColumnElement[_NT]", other: Any + self: "SQLCoreOperations[_NT]", other: Any ) -> "BinaryExpression[_NUMERIC]": ... @@ -1159,6 +915,267 @@ class ColumnElement( def __rfloordiv__(self, other: Any) -> "BinaryExpression": ... + +_SQO = SQLCoreOperations + + +class ColumnElement( + roles.ColumnArgumentOrKeyRole, + roles.StatementOptionRole, + roles.WhereHavingRole, + roles.BinaryElementRole, + roles.OrderByRole, + roles.ColumnsClauseRole, + roles.LimitOffsetRole, + roles.DMLColumnRole, + roles.DDLConstraintColumnRole, + roles.DDLExpressionRole, + SQLCoreOperations[_T], + operators.ColumnOperators[SQLCoreOperations], + ClauseElement, +): + """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') + <sqlalchemy.sql.expression.BinaryExpression object at 0x101029dd0> + >>> 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 <expr> AS <label> in a SELECT statement when using + the LABEL_STYLE_TABLENAME_PLUS_COL label style, which is what the legacy + ORM ``Query`` object uses as well. + + For a regular Column bound to a Table, this is typically the label + <tablename>_<columnname>. For other constructs, different rules + may apply, such as anonymized labels and others. + + .. versionchanged:: 1.4.21 renamed from ``._label`` + + """ + + key = None + """The 'key' that in some circumstances refers to this object in a + Python namespace. + + This typically refers to the "key" of the column as present in the + ``.c`` collection of a selectable, e.g. ``sometable.c["somekey"]`` would + return a :class:`_schema.Column` with a ``.key`` of "somekey". + + """ + + @HasMemoized.memoized_attribute + def _tq_key_label(self): + """A label-based version of 'key' that in some circumstances refers + to this object in a Python namespace. + + + _tq_key_label comes into play when a select() statement is constructed + with apply_labels(); in this case, all Column objects in the ``.c`` + collection are rendered as <tablename>_<columnname> in SQL; this is + essentially the value of ._label. But to locate those columns in the + ``.c`` collection, the name is along the lines of <tablename>_<key>; + that's the typical value of .key_label. + + .. versionchanged:: 1.4.21 renamed from ``._key_label`` + + """ + return self._proxy_key + + @property + def _key_label(self): + """legacy; renamed to _tq_key_label""" + return self._tq_key_label + + @property + def _label(self): + """legacy; renamed to _tq_label""" + return self._tq_label + + @property + def _non_anon_label(self): + """the 'name' that naturally applies this element when rendered in + SQL. + + Concretely, this is the "name" of a column or a label in a + SELECT statement; ``<columnname>`` and ``<labelname>`` below:: + + SELECT <columnmame> FROM table + + SELECT column AS <labelname> FROM table + + Above, the two names noted will be what's present in the DBAPI + ``cursor.description`` as the names. + + If this attribute returns ``None``, it means that the SQL element as + written does not have a 100% fully predictable "name" that would appear + in the ``cursor.description``. Examples include SQL functions, CAST + functions, etc. While such things do return names in + ``cursor.description``, they are only predictable on a + database-specific basis; e.g. an expression like ``MAX(table.col)`` may + appear as the string ``max`` on one database (like PostgreSQL) or may + appear as the whole expression ``max(table.col)`` on SQLite. + + The default implementation looks for a ``.name`` attribute on the + object, as has been the precedent established in SQLAlchemy for many + years. An exception is made on the ``FunctionElement`` subclass + so that the return value is always ``None``. + + .. versionadded:: 1.4.21 + + + + """ + return getattr(self, "name", None) + + _render_label_in_columns_clause = True + """A flag used by select._columns_plus_names that helps to determine + we are actually going to render in terms of "SELECT <col> AS <label>". + This flag can be returned as False for some Column objects that want + to be rendered as simple "SELECT <col>"; typically columns that don't have + any parent table and are named the same as what the label would be + in any case. + + """ + + _allow_label_resolve = True + """A flag that can be flipped to prevent a column from being resolvable + by string label name. + + The joined eager loader strategy in the ORM uses this, for example. + + """ + + _is_implicitly_boolean = False + + _alt_names = () + + def self_group(self, against=None): + if ( + against in (operators.and_, operators.or_, operators._asbool) + and self.type._type_affinity is type_api.BOOLEANTYPE._type_affinity + ): + return AsBoolean(self, operators.is_true, operators.is_false) + elif against in (operators.any_op, operators.all_op): + return Grouping(self) + else: + return self + + def _negate(self): + if self.type._type_affinity is type_api.BOOLEANTYPE._type_affinity: + return AsBoolean(self, operators.is_false, operators.is_true) + else: + return super(ColumnElement, self)._negate() + + @util.memoized_property + def type(self) -> "TypeEngine[_T]": + return type_api.NULLTYPE + + @HasMemoized.memoized_attribute + def comparator(self) -> "TypeEngine.Comparator[_T]": + try: + comparator_factory = self.type.comparator_factory + except AttributeError as err: + raise TypeError( + "Object %r associated with '.type' attribute " + "is not a TypeEngine class or object" % self.type + ) from err + else: + return comparator_factory(self) + + def __getattr__(self, key): + try: + return getattr(self.comparator, key) + except AttributeError as err: + raise AttributeError( + "Neither %r object nor %r object has an attribute %r" + % ( + type(self).__name__, + type(self.comparator).__name__, + key, + ) + ) from err + def operate( self, op: operators.OperatorType, diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py index 7a1e80889..2e6d64c55 100644 --- a/lib/sqlalchemy/sql/functions.py +++ b/lib/sqlalchemy/sql/functions.py @@ -9,6 +9,9 @@ """ +from typing import Any +from typing import TypeVar + from . import annotation from . import coercions from . import operators @@ -42,6 +45,8 @@ from .visitors import InternalTraversal from .. import util +_T = TypeVar("_T", bound=Any) + _registry = util.defaultdict(dict) @@ -67,7 +72,7 @@ def register_function(identifier, fn, package="_default"): reg[identifier] = fn -class FunctionElement(Executable, ColumnElement, FromClause, Generative): +class FunctionElement(Executable, ColumnElement[_T], FromClause, Generative): """Base for SQL function-oriented constructs. .. seealso:: diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index fd1abd71b..00e20e3fb 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -15,6 +15,9 @@ import collections import itertools from operator import attrgetter import typing +from typing import Any as TODO_Any +from typing import Optional +from typing import Tuple from typing import Type from typing import Union @@ -53,6 +56,7 @@ from .elements import BooleanClauseList from .elements import ClauseElement from .elements import ClauseList from .elements import ColumnClause +from .elements import ColumnElement from .elements import GroupedElement from .elements import Grouping from .elements import literal_column @@ -2648,8 +2652,7 @@ class SelectBase( """ return self.selected_columns - @property - @util.deprecated( + @util.deprecated_property( "1.4", "The :attr:`_expression.SelectBase.c` and " ":attr:`_expression.SelectBase.columns` attributes " @@ -4039,16 +4042,16 @@ class Select( __visit_name__ = "select" - _setup_joins = () - _memoized_select_entities = () + _setup_joins: Tuple[TODO_Any, ...] = () + _memoized_select_entities: Tuple[TODO_Any, ...] = () _distinct = False - _distinct_on = () - _correlate = () - _correlate_except = None - _where_criteria = () - _having_criteria = () - _from_obj = () + _distinct_on: Tuple[ColumnElement, ...] = () + _correlate: Tuple[FromClause, ...] = () + _correlate_except: Optional[Tuple[FromClause, ...]] = None + _where_criteria: Tuple[ColumnElement, ...] = () + _having_criteria: Tuple[ColumnElement, ...] = () + _from_obj: Tuple[FromClause, ...] = () _auto_correlate = True _compile_options = SelectState.default_select_compile_options @@ -4417,8 +4420,7 @@ class Select( """ return self._compile_state_factory(self, None)._get_display_froms() - @property - @util.deprecated( + @util.deprecated_property( "1.4.23", "The :attr:`_expression.Select.froms` attribute is moved to " "the :meth:`_expression.Select.get_final_froms` method.", diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index 81434fbb9..42fad5e04 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -65,7 +65,7 @@ class _LookupExpressionAdapter: def _expression_adaptations(self): raise NotImplementedError() - class Comparator(TypeEngine.Comparator): + class Comparator(TypeEngine.Comparator[_T]): _blank_dict = util.immutabledict() def _adapt_expression(self, op, other_comparator): @@ -88,7 +88,7 @@ class Concatenable: """A mixin that marks a type as supporting 'concatenation', typically strings.""" - class Comparator(TypeEngine.Comparator): + class Comparator(TypeEngine.Comparator[_T]): def _adapt_expression(self, op, other_comparator): if op is operators.add and isinstance( other_comparator, @@ -113,7 +113,7 @@ class Indexable: """ - class Comparator(TypeEngine.Comparator): + class Comparator(TypeEngine.Comparator[_T]): def _setup_getitem(self, index): raise NotImplementedError() @@ -1377,7 +1377,7 @@ class Enum(Emulated, String, TypeEngine[Union[str, enum.Enum]], SchemaType): ) ) from err - class Comparator(String.Comparator): + class Comparator(String.Comparator[_T]): def _adapt_expression(self, op, other_comparator): op, typ = super(Enum.Comparator, self)._adapt_expression( op, other_comparator @@ -2204,7 +2204,7 @@ class JSON(Indexable, TypeEngine[Any]): """ - class Comparator(Indexable.Comparator, Concatenable.Comparator): + class Comparator(Indexable.Comparator[_T], Concatenable.Comparator[_T]): """Define comparison operations for :class:`_types.JSON`.""" def _setup_getitem(self, index): @@ -2523,7 +2523,7 @@ class ARRAY( """If True, Python zero-based indexes should be interpreted as one-based on the SQL expression side.""" - class Comparator(Indexable.Comparator, Concatenable.Comparator): + class Comparator(Indexable.Comparator[_T], Concatenable.Comparator[_T]): """Define comparison operations for :class:`_types.ARRAY`. @@ -2967,7 +2967,7 @@ class NullType(TypeEngine): return process - class Comparator(TypeEngine.Comparator): + class Comparator(TypeEngine.Comparator[_T]): def _adapt_expression(self, op, other_comparator): if isinstance( other_comparator, NullType.Comparator diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py index 75eb1b8c5..dd29b2c3a 100644 --- a/lib/sqlalchemy/sql/type_api.py +++ b/lib/sqlalchemy/sql/type_api.py @@ -11,6 +11,7 @@ import typing from typing import Any +from typing import Callable from typing import Generic from typing import Tuple from typing import Type @@ -1400,7 +1401,7 @@ class TypeDecorator(ExternalType, SchemaEventTarget, TypeEngine[_T]): """ - class Comparator(TypeEngine.Comparator): + class Comparator(TypeEngine.Comparator[_CT]): """A :class:`.TypeEngine.Comparator` that is specific to :class:`.TypeDecorator`. @@ -1425,7 +1426,7 @@ class TypeDecorator(ExternalType, SchemaEventTarget, TypeEngine[_T]): ) @property - def comparator_factory(self): + def comparator_factory(self) -> Callable[..., TypeEngine.Comparator[_T]]: if TypeDecorator.Comparator in self.impl.comparator_factory.__mro__: return self.impl.comparator_factory else: |
