diff options
| author | mike bayer <mike_mp@zzzcomputing.com> | 2020-07-08 15:07:44 +0000 |
|---|---|---|
| committer | Gerrit Code Review <gerrit@bbpush.zzzcomputing.com> | 2020-07-08 15:07:44 +0000 |
| commit | b330ffbc13ddb4274a004eab6a13ce40d641e555 (patch) | |
| tree | 88605188d6adac26c77defa544fc5cde208952f5 /lib/sqlalchemy | |
| parent | a6d8b674e92ef1cabdb2ab85490397f3ed12a42c (diff) | |
| parent | 91f376692d472a5bf0c4b4033816250ec1ce3ab6 (diff) | |
| download | sqlalchemy-b330ffbc13ddb4274a004eab6a13ce40d641e555.tar.gz | |
Merge "Add future=True to create_engine/Session; unify select()"
Diffstat (limited to 'lib/sqlalchemy')
24 files changed, 661 insertions, 421 deletions
diff --git a/lib/sqlalchemy/cextension/resultproxy.c b/lib/sqlalchemy/cextension/resultproxy.c index ed6f57470..f99236e1e 100644 --- a/lib/sqlalchemy/cextension/resultproxy.c +++ b/lib/sqlalchemy/cextension/resultproxy.c @@ -505,7 +505,8 @@ BaseRow_getattro(BaseRow *self, PyObject *name) else return tmp; - tmp = BaseRow_subscript_mapping(self, name); + tmp = BaseRow_subscript_impl(self, name, 1); + if (tmp == NULL && PyErr_ExceptionMatches(PyExc_KeyError)) { #if PY_MAJOR_VERSION >= 3 diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 4b211bde7..06ea80b9e 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -2612,7 +2612,7 @@ class MSDialect(default.DefaultDialect): def has_table(self, connection, tablename, dbname, owner, schema): tables = ischema.tables - s = sql.select([tables.c.table_name]).where( + s = sql.select(tables.c.table_name).where( sql.and_( tables.c.table_type == "BASE TABLE", tables.c.table_name == tablename, @@ -2630,7 +2630,7 @@ class MSDialect(default.DefaultDialect): def has_sequence(self, connection, sequencename, dbname, owner, schema): sequences = ischema.sequences - s = sql.select([sequences.c.sequence_name]).where( + s = sql.select(sequences.c.sequence_name).where( sequences.c.sequence_name == sequencename ) @@ -2646,7 +2646,7 @@ class MSDialect(default.DefaultDialect): def get_sequence_names(self, connection, dbname, owner, schema, **kw): sequences = ischema.sequences - s = sql.select([sequences.c.sequence_name]) + s = sql.select(sequences.c.sequence_name) if owner: s = s.where(sequences.c.sequence_schema == owner) @@ -2668,7 +2668,7 @@ class MSDialect(default.DefaultDialect): def get_table_names(self, connection, dbname, owner, schema, **kw): tables = ischema.tables s = ( - sql.select([tables.c.table_name]) + sql.select(tables.c.table_name) .where( sql.and_( tables.c.table_schema == owner, @@ -2684,12 +2684,15 @@ class MSDialect(default.DefaultDialect): @_db_plus_owner_listing def get_view_names(self, connection, dbname, owner, schema, **kw): tables = ischema.tables - s = sql.select( - [tables.c.table_name], - sql.and_( - tables.c.table_schema == owner, tables.c.table_type == "VIEW" - ), - order_by=[tables.c.table_name], + s = ( + sql.select(tables.c.table_name) + .where( + sql.and_( + tables.c.table_schema == owner, + tables.c.table_type == "VIEW", + ) + ) + .order_by(tables.c.table_name) ) view_names = [r[0] for r in connection.execute(s)] return view_names @@ -2807,11 +2810,13 @@ class MSDialect(default.DefaultDialect): computed_cols.c.definition, NVARCHAR(4000) ) - s = sql.select( - [columns, computed_definition, computed_cols.c.is_persisted], - whereclause, - from_obj=join, - order_by=[columns.c.ordinal_position], + s = ( + sql.select( + columns, computed_definition, computed_cols.c.is_persisted + ) + .where(whereclause) + .select_from(join) + .order_by(columns.c.ordinal_position) ) c = connection.execution_options(future_result=True).execute(s) @@ -2930,7 +2935,8 @@ class MSDialect(default.DefaultDialect): # Primary key constraints s = sql.select( - [C.c.column_name, TC.c.constraint_type, C.c.constraint_name], + C.c.column_name, TC.c.constraint_type, C.c.constraint_name + ).where( sql.and_( TC.c.constraint_name == C.c.constraint_name, TC.c.table_schema == C.c.table_schema, @@ -2957,8 +2963,8 @@ class MSDialect(default.DefaultDialect): R = ischema.key_constraints.alias("R") # Foreign key constraints - s = sql.select( - [ + s = ( + sql.select( C.c.column_name, R.c.table_schema, R.c.table_name, @@ -2967,17 +2973,19 @@ class MSDialect(default.DefaultDialect): RR.c.match_option, RR.c.update_rule, RR.c.delete_rule, - ], - sql.and_( - C.c.table_name == tablename, - C.c.table_schema == owner, - RR.c.constraint_schema == C.c.table_schema, - C.c.constraint_name == RR.c.constraint_name, - R.c.constraint_name == RR.c.unique_constraint_name, - R.c.constraint_schema == RR.c.unique_constraint_schema, - C.c.ordinal_position == R.c.ordinal_position, - ), - order_by=[RR.c.constraint_name, R.c.ordinal_position], + ) + .where( + sql.and_( + C.c.table_name == tablename, + C.c.table_schema == owner, + RR.c.constraint_schema == C.c.table_schema, + C.c.constraint_name == RR.c.constraint_name, + R.c.constraint_name == RR.c.unique_constraint_name, + R.c.constraint_schema == RR.c.unique_constraint_schema, + C.c.ordinal_position == R.c.ordinal_position, + ) + ) + .order_by(RR.c.constraint_name, R.c.ordinal_position) ) # group rows by constraint ID, to handle multi-column FKs diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 9ac61fe12..6bc9588ad 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1511,7 +1511,6 @@ class Connection(Connectable): # legacy stuff. if should_close_with_result and context._soft_closed: assert not self._is_future - assert not context._is_future_result # CursorResult already exhausted rows / has no rows. # close us now diff --git a/lib/sqlalchemy/engine/create.py b/lib/sqlalchemy/engine/create.py index c199c21e0..8b0377a58 100644 --- a/lib/sqlalchemy/engine/create.py +++ b/lib/sqlalchemy/engine/create.py @@ -227,6 +227,15 @@ def create_engine(url, **kwargs): be applied to all connections. See :meth:`~sqlalchemy.engine.Connection.execution_options` + :param future: Use the 2.0 style :class:`_future.Engine` and + :class:`_future.Connection` API. + + ..versionadded:: 1.4 + + .. seealso:: + + :ref:`migration_20_toplevel` + :param hide_parameters: Boolean, when set to True, SQL statement parameters will not be displayed in INFO logging nor will they be formatted into the string representation of :class:`.StatementError` objects. @@ -575,7 +584,14 @@ def create_engine(url, **kwargs): pool._dialect = dialect # create engine. - engineclass = kwargs.pop("_future_engine_class", base.Engine) + if kwargs.pop("future", False): + from sqlalchemy import future + + default_engine_class = future.Engine + else: + default_engine_class = base.Engine + + engineclass = kwargs.pop("_future_engine_class", default_engine_class) engine_args = {} for k in util.get_cls_kwargs(engineclass): diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index f1fc505ac..e567e11e7 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -393,9 +393,7 @@ class DefaultDialect(interfaces.Dialect): parameters = {} def check_unicode(test): - statement = cast_to( - expression.select([test]).compile(dialect=self) - ) + statement = cast_to(expression.select(test).compile(dialect=self)) try: cursor = connection.connection.cursor() connection._cursor_execute(cursor, statement, parameters) @@ -453,7 +451,7 @@ class DefaultDialect(interfaces.Dialect): cursor.execute( cast_to( expression.select( - [expression.literal_column("'x'").label("some_label")] + expression.literal_column("'x'").label("some_label") ).compile(dialect=self) ) ) diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py index ecbf871e2..fc6623609 100644 --- a/lib/sqlalchemy/ext/baked.py +++ b/lib/sqlalchemy/ext/baked.py @@ -405,7 +405,7 @@ class Result(object): ) result = self.session.execute( - statement, params, execution_options=execution_options + statement, params, execution_options=execution_options, future=True ) if result._attributes.get("is_single_entity", False): result = result.scalars() diff --git a/lib/sqlalchemy/future/__init__.py b/lib/sqlalchemy/future/__init__.py index 6a3581599..37ce46e47 100644 --- a/lib/sqlalchemy/future/__init__.py +++ b/lib/sqlalchemy/future/__init__.py @@ -11,7 +11,7 @@ from .engine import Connection # noqa from .engine import create_engine # noqa from .engine import Engine # noqa -from .selectable import Select # noqa +from ..sql.selectable import Select # noqa from ..util.langhelpers import public_factory select = public_factory(Select._create_future_select, ".future.select") diff --git a/lib/sqlalchemy/future/selectable.py b/lib/sqlalchemy/future/selectable.py deleted file mode 100644 index 9d0ae7c89..000000000 --- a/lib/sqlalchemy/future/selectable.py +++ /dev/null @@ -1,165 +0,0 @@ -from ..sql import coercions -from ..sql import roles -from ..sql.base import _generative -from ..sql.selectable import GenerativeSelect -from ..sql.selectable import Select as _LegacySelect -from ..sql.selectable import SelectState -from ..sql.util import _entity_namespace_key - - -class Select(_LegacySelect): - _is_future = True - _setup_joins = () - _legacy_setup_joins = () - inherit_cache = True - - @classmethod - def _create_select(cls, *entities): - raise NotImplementedError("use _create_future_select") - - @classmethod - def _create_future_select(cls, *entities): - r"""Construct a new :class:`_expression.Select` using the 2. - x style API. - - .. versionadded:: 2.0 - the :func:`_future.select` construct is - the same construct as the one returned by - :func:`_expression.select`, except that the function only - accepts the "columns clause" entities up front; the rest of the - state of the SELECT should be built up using generative methods. - - Similar functionality is also available via the - :meth:`_expression.FromClause.select` method on any - :class:`_expression.FromClause`. - - .. seealso:: - - :ref:`coretutorial_selecting` - Core Tutorial description of - :func:`_expression.select`. - - :param \*entities: - Entities to SELECT from. For Core usage, this is typically a series - of :class:`_expression.ColumnElement` and / or - :class:`_expression.FromClause` - objects which will form the columns clause of the resulting - statement. For those objects that are instances of - :class:`_expression.FromClause` (typically :class:`_schema.Table` - or :class:`_expression.Alias` - objects), the :attr:`_expression.FromClause.c` - collection is extracted - to form a collection of :class:`_expression.ColumnElement` objects. - - This parameter will also accept :class:`_expression.TextClause` - constructs as - given, as well as ORM-mapped classes. - - """ - - self = cls.__new__(cls) - self._raw_columns = [ - coercions.expect( - roles.ColumnsClauseRole, ent, apply_propagate_attrs=self - ) - for ent in entities - ] - - GenerativeSelect.__init__(self) - - return self - - def filter(self, *criteria): - """A synonym for the :meth:`_future.Select.where` method.""" - - return self.where(*criteria) - - def _exported_columns_iterator(self): - meth = SelectState.get_plugin_class(self).exported_columns_iterator - return meth(self) - - def _filter_by_zero(self): - if self._setup_joins: - meth = SelectState.get_plugin_class( - self - ).determine_last_joined_entity - _last_joined_entity = meth(self) - if _last_joined_entity is not None: - return _last_joined_entity - - if self._from_obj: - return self._from_obj[0] - - return self._raw_columns[0] - - def filter_by(self, **kwargs): - r"""Apply the given filtering criterion as a WHERE clause - to this select. - - """ - from_entity = self._filter_by_zero() - - clauses = [ - _entity_namespace_key(from_entity, key) == value - for key, value in kwargs.items() - ] - return self.filter(*clauses) - - @property - def column_descriptions(self): - """Return a 'column descriptions' structure which may be - plugin-specific. - - """ - meth = SelectState.get_plugin_class(self).get_column_descriptions - return meth(self) - - @_generative - def join(self, target, onclause=None, isouter=False, full=False): - r"""Create a SQL JOIN against this :class:`_expression.Select` - object's criterion - and apply generatively, returning the newly resulting - :class:`_expression.Select`. - - - """ - target = coercions.expect( - roles.JoinTargetRole, target, apply_propagate_attrs=self - ) - if onclause is not None: - onclause = coercions.expect(roles.OnClauseRole, onclause) - self._setup_joins += ( - (target, onclause, None, {"isouter": isouter, "full": full}), - ) - - @_generative - def join_from( - self, from_, target, onclause=None, isouter=False, full=False - ): - r"""Create a SQL JOIN against this :class:`_expression.Select` - object's criterion - and apply generatively, returning the newly resulting - :class:`_expression.Select`. - - - """ - # note the order of parsing from vs. target is important here, as we - # are also deriving the source of the plugin (i.e. the subject mapper - # in an ORM query) which should favor the "from_" over the "target" - - from_ = coercions.expect( - roles.FromClauseRole, from_, apply_propagate_attrs=self - ) - target = coercions.expect( - roles.JoinTargetRole, target, apply_propagate_attrs=self - ) - - self._setup_joins += ( - (target, onclause, from_, {"isouter": isouter, "full": full}), - ) - - def outerjoin(self, target, onclause=None, full=False): - """Create a left outer join. - - - - """ - return self.join(target, onclause=onclause, isouter=True, full=full,) diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py index 09163d4e9..d5f001db1 100644 --- a/lib/sqlalchemy/orm/context.py +++ b/lib/sqlalchemy/orm/context.py @@ -25,6 +25,7 @@ from ..sql import expression from ..sql import roles from ..sql import util as sql_util from ..sql import visitors +from ..sql.base import _entity_namespace_key from ..sql.base import _select_iterables from ..sql.base import CacheableOptions from ..sql.base import CompileState @@ -241,8 +242,6 @@ class ORMCompileState(CompileState): # were passed to session.execute: # session.execute(legacy_select([User.id, User.name])) # see test_query->test_legacy_tuple_old_select - if not statement._is_future: - return result load_options = execution_options.get( "_sa_orm_load_options", QueryContext.default_load_options @@ -399,6 +398,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): compound_eager_adapter = None correlate = None + correlate_except = None _where_criteria = () _having_criteria = () @@ -406,9 +406,6 @@ class ORMSelectCompileState(ORMCompileState, SelectState): def create_for_statement(cls, statement, compiler, **kw): """compiler hook, we arrive here from compiler.visit_select() only.""" - if not statement._is_future: - return SelectState(statement, compiler, **kw) - if compiler is not None: toplevel = not compiler.stack compiler._rewrites_selected_columns = True @@ -592,6 +589,13 @@ class ORMSelectCompileState(ORMCompileState, SelectState): for s in query._correlate ) ) + elif query._correlate_except: + self.correlate_except = tuple( + util.flatten_iterator( + sql_util.surface_selectables(s) if s is not None else None + for s in query._correlate_except + ) + ) elif not query._auto_correlate: self.correlate = (None,) @@ -827,6 +831,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): hints=self.select_statement._hints, statement_hints=self.select_statement._statement_hints, correlate=self.correlate, + correlate_except=self.correlate_except, **self._select_args ) @@ -902,6 +907,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): hints=self.select_statement._hints, statement_hints=self.select_statement._statement_hints, correlate=self.correlate, + correlate_except=self.correlate_except, **self._select_args ) @@ -921,6 +927,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): hints, statement_hints, correlate, + correlate_except, limit_clause, offset_clause, distinct, @@ -972,6 +979,11 @@ class ORMSelectCompileState(ORMCompileState, SelectState): if correlate: statement.correlate.non_generative(statement, *correlate) + if correlate_except: + statement.correlate_except.non_generative( + statement, *correlate_except + ) + return statement def _adapt_polymorphic_element(self, element): @@ -1222,7 +1234,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): # string given, e.g. query(Foo).join("bar"). # we look to the left entity or what we last joined # towards - onclause = sql.util._entity_namespace_key( + onclause = _entity_namespace_key( inspect(self._joinpoint_zero()), onclause ) @@ -1243,9 +1255,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): info = inspect(jp0) if getattr(info, "mapper", None) is onclause._parententity: - onclause = sql.util._entity_namespace_key( - info, onclause.key - ) + onclause = _entity_namespace_key(info, onclause.key) # legacy ^^^^^^^^^^^^^^^^^^^^^^^^^^^ if isinstance(onclause, interfaces.PropComparator): diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index abb8ce32d..55c2b79f5 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -431,6 +431,7 @@ def load_on_pk_identity( params=load_options._params, execution_options={"_sa_orm_load_options": load_options}, bind_arguments=bind_arguments, + future=True, ) .unique() .scalars() diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index 45ac2442a..1b2779c00 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -29,17 +29,17 @@ from .. import future from .. import sql from .. import util from ..engine import result as _result -from ..future import select as future_select from ..sql import coercions from ..sql import expression from ..sql import operators from ..sql import roles +from ..sql import select +from ..sql.base import _entity_namespace_key from ..sql.base import CompileState from ..sql.base import Options from ..sql.dml import DeleteDMLState from ..sql.dml import UpdateDMLState from ..sql.elements import BooleanClauseList -from ..sql.util import _entity_namespace_key def _bulk_insert( @@ -887,7 +887,7 @@ def _emit_update_statements( ) ) - stmt = table.update(clauses) + stmt = table.update().where(clauses) return stmt cached_stmt = base_mapper._memo(("update", table), update_stmt) @@ -1280,7 +1280,7 @@ def _emit_post_update_statements( ) ) - stmt = table.update(clauses) + stmt = table.update().where(clauses) if mapper.version_id_col is not None: stmt = stmt.return_defaults(mapper.version_id_col) @@ -1394,7 +1394,7 @@ def _emit_delete_statements( ) ) - return table.delete(clauses) + return table.delete().where(clauses) statement = base_mapper._memo(("delete", table), delete_stmt) for connection, recs in groupby(delete, lambda rec: rec[1]): # connection @@ -1950,7 +1950,7 @@ class BulkUDCompileState(CompileState): for k, v in iterator: if mapper: if isinstance(k, util.string_types): - desc = sql.util._entity_namespace_key(mapper, k) + desc = _entity_namespace_key(mapper, k) values.extend(desc._bulk_update_tuples(v)) elif "entity_namespace" in k._annotations: k_anno = k._annotations @@ -1999,7 +1999,7 @@ class BulkUDCompileState(CompileState): ): mapper = update_options._subject_mapper - select_stmt = future_select( + select_stmt = select( *(mapper.primary_key + (mapper.select_identity_token,)) ) select_stmt._where_criteria = statement._where_criteria @@ -2017,6 +2017,7 @@ class BulkUDCompileState(CompileState): execution_options, bind_arguments, _add_event=skip_for_full_returning, + future=True, ) matched_rows = result.fetchall() diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 1ca65c733..acc76094b 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -45,12 +45,13 @@ from .. import inspection from .. import log from .. import sql from .. import util -from ..future.selectable import Select as FutureSelect from ..sql import coercions from ..sql import expression from ..sql import roles +from ..sql import Select from ..sql import util as sql_util from ..sql.annotation import SupportsCloneAnnotations +from ..sql.base import _entity_namespace_key from ..sql.base import _generative from ..sql.base import Executable from ..sql.selectable import _SelectFromElements @@ -61,7 +62,6 @@ from ..sql.selectable import HasSuffixes from ..sql.selectable import LABEL_STYLE_NONE from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL from ..sql.selectable import SelectStatementGrouping -from ..sql.util import _entity_namespace_key from ..sql.visitors import InternalTraversal from ..util import collections_abc @@ -419,7 +419,7 @@ class Query( stmt._propagate_attrs = self._propagate_attrs else: # Query / select() internal attributes are 99% cross-compatible - stmt = FutureSelect.__new__(FutureSelect) + stmt = Select.__new__(Select) stmt.__dict__.update(self.__dict__) stmt.__dict__.update( _label_style=self._label_style, @@ -2836,6 +2836,7 @@ class Query( statement, params, execution_options={"_sa_orm_load_options": self.load_options}, + future=True, ) # legacy: automatically set scalars, unique @@ -3209,6 +3210,7 @@ class Query( delete_, self.load_options._params, execution_options={"synchronize_session": synchronize_session}, + future=True, ) bulk_del.result = result self.session.dispatch.after_bulk_delete(bulk_del) @@ -3363,6 +3365,7 @@ class Query( upd, self.load_options._params, execution_options={"synchronize_session": synchronize_session}, + future=True, ) bulk_ud.result = result self.session.dispatch.after_bulk_update(bulk_ud) diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index bedc54153..0be15260e 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -1352,12 +1352,18 @@ class RelationshipProperty(StrategizedProperty): crit = j & sql.True_._ifnone(criterion) if secondary is not None: - ex = sql.exists( - [1], crit, from_obj=[dest, secondary] - ).correlate_except(dest, secondary) + ex = ( + sql.exists(1) + .where(crit) + .select_from(dest, secondary) + .correlate_except(dest, secondary) + ) else: - ex = sql.exists([1], crit, from_obj=dest).correlate_except( - dest + ex = ( + sql.exists(1) + .where(crit) + .select_from(dest) + .correlate_except(dest) ) return ex diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index abc990f7b..f4f7374e4 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -118,6 +118,7 @@ class ORMExecuteState(util.MemoizedSlots): "_compile_state_cls", "_starting_event_idx", "_events_todo", + "_future", ) def __init__( @@ -129,6 +130,7 @@ class ORMExecuteState(util.MemoizedSlots): bind_arguments, compile_state_cls, events_todo, + future, ): self.session = session self.statement = statement @@ -137,6 +139,7 @@ class ORMExecuteState(util.MemoizedSlots): self.bind_arguments = bind_arguments self._compile_state_cls = compile_state_cls self._events_todo = list(events_todo) + self._future = future def _remaining_events(self): return self._events_todo[self._starting_event_idx + 1 :] @@ -212,6 +215,7 @@ class ORMExecuteState(util.MemoizedSlots): _execution_options, _bind_arguments, _parent_execute_state=self, + future=self._future, ) @property @@ -924,6 +928,7 @@ class Session(_SessionClassMethods): self, bind=None, autoflush=True, + future=False, expire_on_commit=True, autocommit=False, twophase=False, @@ -1039,6 +1044,26 @@ class Session(_SessionClassMethods): so that all attribute/object access subsequent to a completed transaction will load from the most recent database state. + :param future: if True, use 2.0 style behavior for the + :meth:`_orm.Session.execute` method. This includes that the + :class:`_engine.Result` object returned will return new-style + tuple rows, as well as that Core constructs such as + :class:`_sql.Select`, + :class:`_sql.Update` and :class:`_sql.Delete` will be interpreted + in an ORM context if they are made against ORM entities rather than + plain :class:`.Table` metadata objects. + + The "future" flag is also available on a per-execution basis + using the :paramref:`_orm.Session.execute.future` flag. + + .. versionadded:: 1.4 + + .. seealso:: + + :ref:`migration_20_toplevel` + + :ref:`migration_20_result_rows` + :param info: optional dictionary of arbitrary data to be associated with this :class:`.Session`. Is available via the :attr:`.Session.info` attribute. Note the dictionary is copied at @@ -1071,6 +1096,7 @@ class Session(_SessionClassMethods): self._flushing = False self._warn_on_events = False self._transaction = None + self.future = future self.hash_key = _new_sessionid() self.autoflush = autoflush self.autocommit = autocommit @@ -1387,6 +1413,7 @@ class Session(_SessionClassMethods): params=None, execution_options=util.immutabledict(), bind_arguments=None, + future=False, _parent_execute_state=None, _add_event=None, **kw @@ -1493,6 +1520,14 @@ class Session(_SessionClassMethods): Contents of this dictionary are passed to the :meth:`.Session.get_bind` method. + :param future: + Use future style execution for this statement. This is + the same effect as the :paramref:`_orm.Session.future` flag, + except at the level of this single statement execution. See + that flag for details. + + .. versionadded:: 1.4 + :param mapper: deprecated; use the bind_arguments dictionary @@ -1518,15 +1553,18 @@ class Session(_SessionClassMethods): """ statement = coercions.expect(roles.CoerceTextStatementRole, statement) + future = future or self.future + if not bind_arguments: bind_arguments = kw elif kw: bind_arguments.update(kw) - if ( + if future and ( statement._propagate_attrs.get("compile_state_plugin", None) == "orm" ): + # note that even without "future" mode, we need compile_state_cls = CompileState._get_plugin_class_for_plugin( statement, "orm" ) @@ -1547,7 +1585,7 @@ class Session(_SessionClassMethods): ) else: bind_arguments.setdefault("clause", statement) - if statement._is_future: + if future: execution_options = util.immutabledict().merge_with( execution_options, {"future_result": True} ) @@ -1568,6 +1606,7 @@ class Session(_SessionClassMethods): bind_arguments, compile_state_cls, events_todo, + future, ) for idx, fn in enumerate(events_todo): orm_exec_state._starting_event_idx = idx diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py index 6cdab8eac..4bc6d8280 100644 --- a/lib/sqlalchemy/sql/base.py +++ b/lib/sqlalchemy/sql/base.py @@ -1388,3 +1388,25 @@ def _bind_or_error(schemaitem, msg=None): ) raise exc.UnboundExecutionError(msg) return bind + + +def _entity_namespace_key(entity, key): + """Return an entry from an entity_namespace. + + + Raises :class:`_exc.InvalidRequestError` rather than attribute error + on not found. + + """ + + ns = entity.entity_namespace + try: + return getattr(ns, key) + except AttributeError as err: + util.raise_( + exc.InvalidRequestError( + 'Entity namespace for "%s" has no property "%s"' + % (entity, key) + ), + replace_context=err, + ) diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 37441a125..d60c63363 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -185,7 +185,7 @@ tablesample = public_factory( lateral = public_factory(Lateral._factory, ".sql.expression.lateral") or_ = public_factory(BooleanClauseList.or_, ".sql.expression.or_") bindparam = public_factory(BindParameter, ".sql.expression.bindparam") -select = public_factory(Select, ".sql.expression.select") +select = public_factory(Select._create, ".sql.expression.select") text = public_factory(TextClause._create_text, ".sql.expression.text") table = public_factory(TableClause, ".sql.expression.table") column = public_factory(ColumnClause, ".sql.expression.column") diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 832da1a57..12fcc00c3 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -24,6 +24,7 @@ from .annotation import SupportsCloneAnnotations from .base import _clone from .base import _cloned_difference from .base import _cloned_intersection +from .base import _entity_namespace_key from .base import _expand_cloned from .base import _from_objects from .base import _generative @@ -83,7 +84,7 @@ def subquery(alias, *args, **kwargs): :func:`_expression.select` function. """ - return Select(*args, **kwargs).subquery(alias) + return Select.create_legacy_select(*args, **kwargs).subquery(alias) class ReturnsRows(roles.ReturnsRowsRole, ClauseElement): @@ -468,8 +469,38 @@ class FromClause(roles.AnonymizedFromClauseRole, Selectable): _use_schema_map = False - def select(self, whereclause=None, **params): - """Return a SELECT of this :class:`_expression.FromClause`. + @util.deprecated_params( + whereclause=( + "2.0", + "The :paramref:`_sql.FromClause.select().whereclause` parameter " + "is deprecated and will be removed in version 2.0. " + "Please make use of " + "the :meth:`.Select.where` " + "method to add WHERE criteria to the SELECT statement.", + ), + kwargs=( + "2.0", + "The :meth:`_sql.FromClause.select` method will no longer accept " + "keyword arguments in version 2.0. Please use generative methods " + "from the " + ":class:`_sql.Select` construct in order to apply additional " + "modifications.", + ), + ) + def select(self, whereclause=None, **kwargs): + r"""Return a SELECT of this :class:`_expression.FromClause`. + + + e.g.:: + + stmt = some_table.select().where(some_table.c.id == 5) + + :param whereclause: a WHERE clause, equivalent to calling the + :meth:`_sql.Select.where` method. + + :param \**kwargs: additional keyword arguments are passed to the + legacy constructor for :class:`_sql.Select` described at + :meth:`_sql.Select.create_legacy_select`. .. seealso:: @@ -477,8 +508,9 @@ class FromClause(roles.AnonymizedFromClauseRole, Selectable): method which allows for arbitrary column lists. """ - - return Select([self], whereclause, **params) + if whereclause is not None: + kwargs["whereclause"] = whereclause + return Select._create_select_from_fromclause(self, [self], **kwargs) def join(self, right, onclause=None, isouter=False, full=False): """Return a :class:`_expression.Join` from this @@ -1138,24 +1170,45 @@ class Join(roles.DMLTableRole, FromClause): "join explicitly." % (a.description, b.description) ) + @util.deprecated_params( + whereclause=( + "2.0", + "The :paramref:`_sql.Join.select().whereclause` parameter " + "is deprecated and will be removed in version 2.0. " + "Please make use of " + "the :meth:`.Select.where` " + "method to add WHERE criteria to the SELECT statement.", + ), + kwargs=( + "2.0", + "The :meth:`_sql.Join.select` method will no longer accept " + "keyword arguments in version 2.0. Please use generative " + "methods from the " + ":class:`_sql.Select` construct in order to apply additional " + "modifications.", + ), + ) def select(self, whereclause=None, **kwargs): r"""Create a :class:`_expression.Select` from this :class:`_expression.Join`. - The equivalent long-hand form, given a :class:`_expression.Join` - object - ``j``, is:: + E.g.:: + + stmt = table_a.join(table_b, table_a.c.id == table_b.c.a_id) - from sqlalchemy import select - j = select([j.left, j.right], **kw).\ - where(whereclause).\ - select_from(j) + stmt = stmt.select() - :param whereclause: the WHERE criterion that will be sent to - the :func:`select()` function + The above will produce a SQL string resembling:: - :param \**kwargs: all other kwargs are sent to the - underlying :func:`select()` function. + SELECT table_a.id, table_a.col, table_b.id, table_b.a_id + FROM table_a JOIN table_b ON table_a.id = table_b.a_id + + :param whereclause: WHERE criteria, same as calling + :meth:`_sql.Select.where` on the resulting statement + + :param \**kwargs: additional keyword arguments are passed to the + legacy constructor for :class:`_sql.Select` described at + :meth:`_sql.Select.create_legacy_select`. """ collist = [self.left, self.right] @@ -2444,30 +2497,6 @@ class SelectBase( def select(self, *arg, **kw): return self._implicit_subquery.select(*arg, **kw) - @util.deprecated( - "1.4", - "The :meth:`_expression.SelectBase.join` method is deprecated " - "and will be removed in a future release; this method implicitly " - "creates a subquery that should be explicit. " - "Please call :meth:`_expression.SelectBase.subquery` " - "first in order to create " - "a subquery, which then can be selected.", - ) - def join(self, *arg, **kw): - return self._implicit_subquery.join(*arg, **kw) - - @util.deprecated( - "1.4", - "The :meth:`_expression.SelectBase.outerjoin` method is deprecated " - "and will be removed in a future release; this method implicitly " - "creates a subquery that should be explicit. " - "Please call :meth:`_expression.SelectBase.subquery` " - "first in order to create " - "a subquery, which then can be selected.", - ) - def outerjoin(self, *arg, **kw): - return self._implicit_subquery.outerjoin(*arg, **kw) - @HasMemoized.memoized_attribute def _implicit_subquery(self): return self.subquery() @@ -3103,6 +3132,16 @@ class CompoundSelect(HasCompileState, GenerativeSelect): for s in selects ] + if kwargs and util.SQLALCHEMY_WARN_20: + util.warn_deprecated_20( + "Set functions such as union(), union_all(), extract(), etc. " + "in SQLAlchemy 2.0 will accept a " + "series of SELECT statements only. " + "Please use generative methods such as order_by() for " + "additional modifications to this CompoundSelect.", + stacklevel=4, + ) + GenerativeSelect.__init__(self, **kwargs) @classmethod @@ -3770,7 +3809,6 @@ class Select( __visit_name__ = "select" - _is_future = False _setup_joins = () _legacy_setup_joins = () @@ -3817,38 +3855,21 @@ class Select( ] @classmethod - def _create_select(cls, *entities): - r"""Construct an old style :class:`_expression.Select` using the - the 2.x style constructor. - - """ - - self = cls.__new__(cls) - self._raw_columns = [ - coercions.expect(roles.ColumnsClauseRole, ent) for ent in entities - ] - - GenerativeSelect.__init__(self) - - return self - - @classmethod def _create_select_from_fromclause(cls, target, entities, *arg, **kw): if arg or kw: - if util.SQLALCHEMY_WARN_20: - util.warn_deprecated_20( - "Passing arguments to %s.select() is deprecated and " - "will be removed in SQLAlchemy 2.0. " - "Please use generative " - "methods such as select().where(), etc." - % (target.__class__.__name__,) - ) - return Select(entities, *arg, **kw) + return Select.create_legacy_select(entities, *arg, **kw) else: return Select._create_select(*entities) - def __init__( - self, + @classmethod + @util.deprecated( + "2.0", + "The legacy calling style of :func:`_sql.select` is deprecated and " + "will be removed in SQLAlchemy 2.0. Please use the new calling " + "style described at :func:`_sql.select`.", + ) + def create_legacy_select( + cls, columns=None, whereclause=None, from_obj=None, @@ -3859,18 +3880,25 @@ class Select( suffixes=None, **kwargs ): - """Construct a new :class:`_expression.Select` using the 1.x style - API. + """Construct a new :class:`_expression.Select` using the 1.x style API. + + This method is called implicitly when the :func:`_expression.select` + construct is used and the first argument is a Python list or other + plain sequence object, which is taken to refer to the columns + collection. + + .. versionchanged:: 1.4 Added the :meth:`.Select.create_legacy_select` + constructor which documents the calling style in use when the + :func:`.select` construct is invoked using 1.x-style arguments. Similar functionality is also available via the :meth:`_expression.FromClause.select` method on any :class:`_expression.FromClause`. - All arguments which accept :class:`_expression.ClauseElement` - arguments also - accept string arguments, which will be converted as appropriate into - either :func:`_expression.text` or - :func:`_expression.literal_column` constructs. + All arguments which accept :class:`_expression.ClauseElement` arguments + also accept string arguments, which will be converted as appropriate + into either :func:`_expression.text()` or + :func:`_expression.literal_column()` constructs. .. seealso:: @@ -4054,14 +4082,7 @@ class Select( :meth:`_expression.Select.apply_labels` """ - if util.SQLALCHEMY_WARN_20: - util.warn_deprecated_20( - "The select() function in SQLAlchemy 2.0 will accept a " - "series of columns / tables and other entities only, " - "passed positionally. For forwards compatibility, use the " - "sqlalchemy.future.select() construct.", - stacklevel=4, - ) + self = cls.__new__(cls) self._auto_correlate = correlate @@ -4079,8 +4100,10 @@ class Select( except TypeError as err: util.raise_( exc.ArgumentError( - "columns argument to select() must " - "be a Python list or other iterable" + "select() construct created in legacy mode, i.e. with " + "keyword arguments, must provide the columns argument as " + "a Python list or other iterable.", + code="c9ae", ), from_=err, ) @@ -4108,12 +4131,247 @@ class Select( self._setup_suffixes(suffixes) GenerativeSelect.__init__(self, **kwargs) + return self + + @classmethod + def _create_future_select(cls, *entities): + r"""Construct a new :class:`_expression.Select` using the 2. + x style API. + + .. versionadded:: 1.4 - The :func:`_sql.select` function now accepts + column arguments positionally. The top-level :func:`_sql.select` + function will automatically use the 1.x or 2.x style API based on + the incoming argumnents; using :func:`_future.select` from the + ``sqlalchemy.future`` module will enforce that only the 2.x style + constructor is used. + + Similar functionality is also available via the + :meth:`_expression.FromClause.select` method on any + :class:`_expression.FromClause`. + + .. seealso:: + + :ref:`coretutorial_selecting` - Core Tutorial description of + :func:`_expression.select`. + + :param \*entities: + Entities to SELECT from. For Core usage, this is typically a series + of :class:`_expression.ColumnElement` and / or + :class:`_expression.FromClause` + objects which will form the columns clause of the resulting + statement. For those objects that are instances of + :class:`_expression.FromClause` (typically :class:`_schema.Table` + or :class:`_expression.Alias` + objects), the :attr:`_expression.FromClause.c` + collection is extracted + to form a collection of :class:`_expression.ColumnElement` objects. + + This parameter will also accept :class:`_expression.TextClause` + constructs as + given, as well as ORM-mapped classes. + + """ + + self = cls.__new__(cls) + self._raw_columns = [ + coercions.expect( + roles.ColumnsClauseRole, ent, apply_propagate_attrs=self + ) + for ent in entities + ] + + GenerativeSelect.__init__(self) + + return self + + _create_select = _create_future_select + + @classmethod + def _create(cls, *args, **kw): + r"""Create a :class:`.Select` using either the 1.x or 2.0 constructor + style. + + For the legacy calling style, see :meth:`.Select.create_legacy_select`. + If the first argument passed is a Python sequence or if keyword + arguments are present, this style is used. + + .. versionadded:: 2.0 - the :func:`_future.select` construct is + the same construct as the one returned by + :func:`_expression.select`, except that the function only + accepts the "columns clause" entities up front; the rest of the + state of the SELECT should be built up using generative methods. + + Similar functionality is also available via the + :meth:`_expression.FromClause.select` method on any + :class:`_expression.FromClause`. + + .. seealso:: + + :ref:`coretutorial_selecting` - Core Tutorial description of + :func:`_expression.select`. + + :param \*entities: + Entities to SELECT from. For Core usage, this is typically a series + of :class:`_expression.ColumnElement` and / or + :class:`_expression.FromClause` + objects which will form the columns clause of the resulting + statement. For those objects that are instances of + :class:`_expression.FromClause` (typically :class:`_schema.Table` + or :class:`_expression.Alias` + objects), the :attr:`_expression.FromClause.c` + collection is extracted + to form a collection of :class:`_expression.ColumnElement` objects. + + This parameter will also accept :class:`_expression.TextClause` + constructs as given, as well as ORM-mapped classes. + + """ + if (args and isinstance(args[0], list)) or kw: + return cls.create_legacy_select(*args, **kw) + else: + return cls._create_future_select(*args) + + def __init__(self,): + raise NotImplementedError() def _scalar_type(self): elem = self._raw_columns[0] cols = list(elem._select_iterable) return cols[0].type + def filter(self, *criteria): + """A synonym for the :meth:`_future.Select.where` method.""" + + return self.where(*criteria) + + def _filter_by_zero(self): + if self._setup_joins: + meth = SelectState.get_plugin_class( + self + ).determine_last_joined_entity + _last_joined_entity = meth(self) + if _last_joined_entity is not None: + return _last_joined_entity + + if self._from_obj: + return self._from_obj[0] + + return self._raw_columns[0] + + def filter_by(self, **kwargs): + r"""apply the given filtering criterion as a WHERE clause + to this select. + + """ + from_entity = self._filter_by_zero() + + clauses = [ + _entity_namespace_key(from_entity, key) == value + for key, value in kwargs.items() + ] + return self.filter(*clauses) + + @property + def column_descriptions(self): + """Return a 'column descriptions' structure which may be + plugin-specific. + + """ + meth = SelectState.get_plugin_class(self).get_column_descriptions + return meth(self) + + @_generative + def join(self, target, onclause=None, isouter=False, full=False): + r"""Create a SQL JOIN against this :class:`_expresson.Select` + object's criterion + and apply generatively, returning the newly resulting + :class:`_expression.Select`. + + .. versionchanged:: 1.4 :meth:`_expression.Select.join` now modifies + the FROM list of the :class:`.Select` object in place, rather than + implicitly producing a subquery. + + :param target: target table to join towards + + :param onclause: ON clause of the join. + + :param isouter: if True, generate LEFT OUTER join. Same as + :meth:`_expression.Select.outerjoin`. + + :param full: if True, generate FULL OUTER join. + + .. seealso:: + + :meth:`_expression.Select.join_from` + + """ + target = coercions.expect( + roles.JoinTargetRole, target, apply_propagate_attrs=self + ) + if onclause is not None: + onclause = coercions.expect(roles.OnClauseRole, onclause) + self._setup_joins += ( + (target, onclause, None, {"isouter": isouter, "full": full}), + ) + + @_generative + def join_from( + self, from_, target, onclause=None, isouter=False, full=False + ): + r"""Create a SQL JOIN against this :class:`_expresson.Select` + object's criterion + and apply generatively, returning the newly resulting + :class:`_expression.Select`. + + .. versionadded:: 1.4 + + :param from\_: the left side of the join, will be rendered in the + FROM clause and is roughly equivalent to using the + :meth:`.Select.select_from` method. + + :param target: target table to join towards + + :param onclause: ON clause of the join. + + :param isouter: if True, generate LEFT OUTER join. Same as + :meth:`_expression.Select.outerjoin`. + + :param full: if True, generate FULL OUTER join. + + .. seealso:: + + :meth:`_expression.Select.join` + + """ + # note the order of parsing from vs. target is important here, as we + # are also deriving the source of the plugin (i.e. the subject mapper + # in an ORM query) which should favor the "from_" over the "target" + + from_ = coercions.expect( + roles.FromClauseRole, from_, apply_propagate_attrs=self + ) + target = coercions.expect( + roles.JoinTargetRole, target, apply_propagate_attrs=self + ) + if onclause is not None: + onclause = coercions.expect(roles.OnClauseRole, onclause) + + self._setup_joins += ( + (target, onclause, from_, {"isouter": isouter, "full": full}), + ) + + def outerjoin(self, target, onclause=None, full=False): + """Create a left outer join. + + Parameters are the same as that of :meth:`_expression.Select.join`. + + .. versionchanged:: 1.4 :meth:`_expression.Select.outerjoin` now + modifies the FROM list of the :class:`.Select` object in place, + rather than implicitly producing a subquery. + + """ + return self.join(target, onclause=onclause, isouter=True, full=full,) + @property def froms(self): """Return the displayed list of :class:`_expression.FromClause` @@ -4642,8 +4900,12 @@ class Select( return ColumnCollection(collection).as_immutable() + # def _exported_columns_iterator(self): + # return _select_iterables(self._raw_columns) + def _exported_columns_iterator(self): - return _select_iterables(self._raw_columns) + meth = SelectState.get_plugin_class(self).exported_columns_iterator + return meth(self) def _ensure_disambiguated_names(self): if self._label_style is LABEL_STYLE_NONE: @@ -4922,37 +5184,30 @@ class Exists(UnaryExpression): inherit_cache = True def __init__(self, *args, **kwargs): - """Construct a new :class:`_expression.Exists` against an existing - :class:`_expression.Select` object. + """Construct a new :class:`_expression.Exists` construct. - Calling styles are of the following forms:: + The modern form of :func:`.exists` is to invoke with no arguments, + which will produce an ``"EXISTS *"`` construct. A WHERE clause + is then added using the :meth:`.Exists.where` method:: - # use on an existing select() - s = select([table.c.col1]).where(table.c.col2==5) - s_e = exists(s) + exists_criteria = exists().where(table1.c.col1 == table2.c.col2) - # an exists is usually used in a where of another select - # to produce a WHERE EXISTS (SELECT ... ) - select([table.c.col1]).where(s_e) + The EXISTS criteria is then used inside of an enclosing SELECT:: - # but can also be used in a select to produce a - # SELECT EXISTS (SELECT ... ) query - select([s_e]) + stmt = select(table1.c.col1).where(exists_criteria) - # construct a select() at once - exists(['*'], **select_arguments).where(criterion) + The above statement will then be of the form:: - # columns argument is optional, generates "EXISTS (SELECT *)" - # by default. - exists().where(table.c.col2==5) + SELECT col1 FROM table1 WHERE EXISTS + (SELECT * FROM table2 WHERE table2.col2 = table1.col1) """ if args and isinstance(args[0], (SelectBase, ScalarSelect)): s = args[0] else: if not args: - args = ([literal_column("*")],) - s = Select(*args, **kwargs).scalar_subquery() + args = (literal_column("*"),) + s = Select._create(*args, **kwargs).scalar_subquery() UnaryExpression.__init__( self, @@ -4967,10 +5222,52 @@ class Exists(UnaryExpression): element = fn(element) return element.self_group(against=operators.exists) - def select(self, whereclause=None, **params): + @util.deprecated_params( + whereclause=( + "2.0", + "The :paramref:`_sql.Exists.select().whereclause` parameter " + "is deprecated and will be removed in version 2.0. " + "Please make use " + "of the :meth:`.Select.where` " + "method to add WHERE criteria to the SELECT statement.", + ), + kwargs=( + "2.0", + "The :meth:`_sql.Exists.select` method will no longer accept " + "keyword arguments in version 2.0. " + "Please use generative methods from the " + ":class:`_sql.Select` construct in order to apply additional " + "modifications.", + ), + ) + def select(self, whereclause=None, **kwargs): + r"""Return a SELECT of this :class:`_expression.Exists`. + + e.g.:: + + stmt = exists(some_table.c.id).where(some_table.c.id == 5).select() + + This will produce a statement resembling:: + + SELECT EXISTS (SELECT id FROM some_table WHERE some_table = :param) AS anon_1 + + :param whereclause: a WHERE clause, equivalent to calling the + :meth:`_sql.Select.where` method. + + :param **kwargs: additional keyword arguments are passed to the + legacy constructor for :class:`_sql.Select` described at + :meth:`_sql.Select.create_legacy_select`. + + .. seealso:: + + :func:`_expression.select` - general purpose + method which allows for arbitrary column lists. + + """ # noqa + if whereclause is not None: - params["whereclause"] = whereclause - return Select._create_select_from_fromclause(self, [self], **params) + kwargs["whereclause"] = whereclause + return Select._create_select_from_fromclause(self, [self], **kwargs) def correlate(self, *fromclause): e = self._clone() @@ -4986,7 +5283,7 @@ class Exists(UnaryExpression): ) return e - def select_from(self, clause): + def select_from(self, *froms): """Return a new :class:`_expression.Exists` construct, applying the given expression to the :meth:`_expression.Select.select_from` @@ -4995,7 +5292,7 @@ class Exists(UnaryExpression): """ e = self._clone() - e.element = self._regroup(lambda element: element.select_from(clause)) + e.element = self._regroup(lambda element: element.select_from(*froms)) return e def where(self, clause): diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index b803ef912..814253266 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -977,25 +977,3 @@ class ColumnAdapter(ClauseAdapter): def __setstate__(self, state): self.__dict__.update(state) self.columns = util.WeakPopulateDict(self._locate_col) - - -def _entity_namespace_key(entity, key): - """Return an entry from an entity_namespace. - - - Raises :class:`_exc.InvalidRequestError` rather than attribute error - on not found. - - """ - - ns = entity.entity_namespace - try: - return getattr(ns, key) - except AttributeError as err: - util.raise_( - exc.InvalidRequestError( - 'Entity namespace for "%s" has no property "%s"' - % (entity, key) - ), - replace_context=err, - ) diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py index 998dde66b..1ce59431e 100644 --- a/lib/sqlalchemy/testing/assertions.py +++ b/lib/sqlalchemy/testing/assertions.py @@ -170,7 +170,11 @@ def _expect_warnings( with mock.patch("warnings.warn", our_warn), mock.patch( "sqlalchemy.util.SQLALCHEMY_WARN_20", True - ), mock.patch("sqlalchemy.engine.row.LegacyRow._default_key_style", 2): + ), mock.patch( + "sqlalchemy.util.deprecations.SQLALCHEMY_WARN_20", True + ), mock.patch( + "sqlalchemy.engine.row.LegacyRow._default_key_style", 2 + ): yield if assert_ and (not py2konly or not compat.py3k): diff --git a/lib/sqlalchemy/testing/suite/test_types.py b/lib/sqlalchemy/testing/suite/test_types.py index 00b5fab27..48144f885 100644 --- a/lib/sqlalchemy/testing/suite/test_types.py +++ b/lib/sqlalchemy/testing/suite/test_types.py @@ -114,9 +114,7 @@ class _UnicodeFixture(_LiteralRoundTripFixture, fixtures.TestBase): connection.execute(unicode_table.insert(), {"unicode_data": self.data}) - row = connection.execute( - select([unicode_table.c.unicode_data]) - ).first() + row = connection.execute(select(unicode_table.c.unicode_data)).first() eq_(row, (self.data,)) assert isinstance(row[0], util.text_type) @@ -130,7 +128,7 @@ class _UnicodeFixture(_LiteralRoundTripFixture, fixtures.TestBase): ) rows = connection.execute( - select([unicode_table.c.unicode_data]) + select(unicode_table.c.unicode_data) ).fetchall() eq_(rows, [(self.data,) for i in range(3)]) for row in rows: @@ -140,18 +138,14 @@ class _UnicodeFixture(_LiteralRoundTripFixture, fixtures.TestBase): unicode_table = self.tables.unicode_table connection.execute(unicode_table.insert(), {"unicode_data": None}) - row = connection.execute( - select([unicode_table.c.unicode_data]) - ).first() + row = connection.execute(select(unicode_table.c.unicode_data)).first() eq_(row, (None,)) def _test_empty_strings(self, connection): unicode_table = self.tables.unicode_table connection.execute(unicode_table.insert(), {"unicode_data": u("")}) - row = connection.execute( - select([unicode_table.c.unicode_data]) - ).first() + row = connection.execute(select(unicode_table.c.unicode_data)).first() eq_(row, (u(""),)) def test_literal(self): @@ -214,7 +208,7 @@ class TextTest(_LiteralRoundTripFixture, fixtures.TablesTest): text_table = self.tables.text_table connection.execute(text_table.insert(), {"text_data": "some text"}) - row = connection.execute(select([text_table.c.text_data])).first() + row = connection.execute(select(text_table.c.text_data)).first() eq_(row, ("some text",)) @testing.requires.empty_strings_text @@ -222,14 +216,14 @@ class TextTest(_LiteralRoundTripFixture, fixtures.TablesTest): text_table = self.tables.text_table connection.execute(text_table.insert(), {"text_data": ""}) - row = connection.execute(select([text_table.c.text_data])).first() + row = connection.execute(select(text_table.c.text_data)).first() eq_(row, ("",)) def test_text_null_strings(self, connection): text_table = self.tables.text_table connection.execute(text_table.insert(), {"text_data": None}) - row = connection.execute(select([text_table.c.text_data])).first() + row = connection.execute(select(text_table.c.text_data)).first() eq_(row, (None,)) def test_literal(self): @@ -302,7 +296,7 @@ class _DateFixture(_LiteralRoundTripFixture, fixtures.TestBase): connection.execute(date_table.insert(), {"date_data": self.data}) - row = connection.execute(select([date_table.c.date_data])).first() + row = connection.execute(select(date_table.c.date_data)).first() compare = self.compare or self.data eq_(row, (compare,)) @@ -313,7 +307,7 @@ class _DateFixture(_LiteralRoundTripFixture, fixtures.TestBase): connection.execute(date_table.insert(), {"date_data": None}) - row = connection.execute(select([date_table.c.date_data])).first() + row = connection.execute(select(date_table.c.date_data)).first() eq_(row, (None,)) @testing.requires.datetime_literals @@ -332,7 +326,7 @@ class _DateFixture(_LiteralRoundTripFixture, fixtures.TestBase): date_table.insert(), {"date_data": self.data} ) id_ = result.inserted_primary_key[0] - stmt = select([date_table.c.id]).where( + stmt = select(date_table.c.id).where( case( [ ( @@ -438,7 +432,7 @@ class IntegerTest(_LiteralRoundTripFixture, fixtures.TestBase): connection.execute(int_table.insert(), {"integer_data": data}) - row = connection.execute(select([int_table.c.integer_data])).first() + row = connection.execute(select(int_table.c.integer_data)).first() eq_(row, (data,)) @@ -545,7 +539,7 @@ class NumericTest(_LiteralRoundTripFixture, fixtures.TestBase): def test_float_coerce_round_trip(self, connection): expr = 15.7563 - val = connection.scalar(select([literal(expr)])) + val = connection.scalar(select(literal(expr))) eq_(val, expr) # this does not work in MySQL, see #4036, however we choose not @@ -556,14 +550,14 @@ class NumericTest(_LiteralRoundTripFixture, fixtures.TestBase): def test_decimal_coerce_round_trip(self, connection): expr = decimal.Decimal("15.7563") - val = connection.scalar(select([literal(expr)])) + val = connection.scalar(select(literal(expr))) eq_(val, expr) @testing.emits_warning(r".*does \*not\* support Decimal objects natively") def test_decimal_coerce_round_trip_w_cast(self, connection): expr = decimal.Decimal("15.7563") - val = connection.scalar(select([cast(expr, Numeric(10, 4))])) + val = connection.scalar(select(cast(expr, Numeric(10, 4)))) eq_(val, expr) @testing.requires.precision_numerics_general @@ -665,9 +659,7 @@ class BooleanTest(_LiteralRoundTripFixture, fixtures.TablesTest): ) row = connection.execute( - select( - [boolean_table.c.value, boolean_table.c.unconstrained_value] - ) + select(boolean_table.c.value, boolean_table.c.unconstrained_value) ).first() eq_(row, (True, False)) @@ -683,9 +675,7 @@ class BooleanTest(_LiteralRoundTripFixture, fixtures.TablesTest): ) row = connection.execute( - select( - [boolean_table.c.value, boolean_table.c.unconstrained_value] - ) + select(boolean_table.c.value, boolean_table.c.unconstrained_value) ).first() eq_(row, (None, None)) @@ -705,13 +695,13 @@ class BooleanTest(_LiteralRoundTripFixture, fixtures.TablesTest): eq_( conn.scalar( - select([boolean_table.c.id]).where(boolean_table.c.value) + select(boolean_table.c.id).where(boolean_table.c.value) ), 1, ) eq_( conn.scalar( - select([boolean_table.c.id]).where( + select(boolean_table.c.id).where( boolean_table.c.unconstrained_value ) ), @@ -719,13 +709,13 @@ class BooleanTest(_LiteralRoundTripFixture, fixtures.TablesTest): ) eq_( conn.scalar( - select([boolean_table.c.id]).where(~boolean_table.c.value) + select(boolean_table.c.id).where(~boolean_table.c.value) ), 2, ) eq_( conn.scalar( - select([boolean_table.c.id]).where( + select(boolean_table.c.id).where( ~boolean_table.c.unconstrained_value ) ), @@ -760,7 +750,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): data_table.insert(), {"name": "row1", "data": data_element} ) - row = connection.execute(select([data_table.c.data])).first() + row = connection.execute(select(data_table.c.data)).first() eq_(row, (data_element,)) @@ -806,7 +796,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): expr = data_table.c.data["key1"] expr = getattr(expr, "as_%s" % datatype)() - roundtrip = conn.scalar(select([expr])) + roundtrip = conn.scalar(select(expr)) eq_(roundtrip, value) if util.py3k: # skip py2k to avoid comparing unicode to str etc. is_(type(roundtrip), type(value)) @@ -828,7 +818,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): expr = data_table.c.data["key1"] expr = getattr(expr, "as_%s" % datatype)() - row = conn.execute(select([expr]).where(expr == value)).first() + row = conn.execute(select(expr).where(expr == value)).first() # make sure we get a row even if value is None eq_(row, (value,)) @@ -850,7 +840,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): expr = data_table.c.data[("key1", "subkey1")] expr = getattr(expr, "as_%s" % datatype)() - row = conn.execute(select([expr]).where(expr == value)).first() + row = conn.execute(select(expr).where(expr == value)).first() # make sure we get a row even if value is None eq_(row, (value,)) @@ -882,7 +872,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): ) row = conn.execute( - select([data_table.c.data, data_table.c.nulldata]) + select(data_table.c.data, data_table.c.nulldata) ).first() eq_(row, (data_element, data_element)) @@ -903,7 +893,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): conn.execute( data_table.insert(), {"name": "row1", "data": data_element} ) - row = conn.execute(select([data_table.c.data])).first() + row = conn.execute(select(data_table.c.data)).first() eq_(row, (data_element,)) eq_(js.mock_calls, [mock.call(data_element)]) @@ -919,12 +909,12 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): eq_( conn.scalar( - select([self.tables.data_table.c.name]).where(col.is_(null())) + select(self.tables.data_table.c.name).where(col.is_(null())) ), "r1", ) - eq_(conn.scalar(select([col])), None) + eq_(conn.scalar(select(col)), None) def test_round_trip_json_null_as_json_null(self, connection): col = self.tables.data_table.c["data"] @@ -936,14 +926,14 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): eq_( conn.scalar( - select([self.tables.data_table.c.name]).where( + select(self.tables.data_table.c.name).where( cast(col, String) == "null" ) ), "r1", ) - eq_(conn.scalar(select([col])), None) + eq_(conn.scalar(select(col)), None) def test_round_trip_none_as_json_null(self): col = self.tables.data_table.c["data"] @@ -955,14 +945,14 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): eq_( conn.scalar( - select([self.tables.data_table.c.name]).where( + select(self.tables.data_table.c.name).where( cast(col, String) == "null" ) ), "r1", ) - eq_(conn.scalar(select([col])), None) + eq_(conn.scalar(select(col)), None) def test_unicode_round_trip(self): # note we include Unicode supplementary characters as well @@ -979,7 +969,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): ) eq_( - conn.scalar(select([self.tables.data_table.c.data])), + conn.scalar(select(self.tables.data_table.c.data)), { util.u("réve🐍 illé"): util.u("réve🐍 illé"), "data": {"k1": util.u("drôl🐍e")}, @@ -1087,7 +1077,7 @@ class JSONStringCastIndexTest(_LiteralRoundTripFixture, fixtures.TablesTest): def _test_index_criteria(self, crit, expected, test_literal=True): self._criteria_fixture() with config.db.connect() as conn: - stmt = select([self.tables.data_table.c.name]).where(crit) + stmt = select(self.tables.data_table.c.name).where(crit) eq_(conn.scalar(stmt), expected) diff --git a/lib/sqlalchemy/testing/warnings.py b/lib/sqlalchemy/testing/warnings.py index 3850f65c8..298b20c11 100644 --- a/lib/sqlalchemy/testing/warnings.py +++ b/lib/sqlalchemy/testing/warnings.py @@ -31,9 +31,6 @@ def setup_filters(): "ignore", category=DeprecationWarning, message=".*inspect.get.*argspec" ) - # ignore 2.0 warnings unless we are explicitly testing for them - warnings.filterwarnings("ignore", category=sa_exc.RemovedIn20Warning) - # ignore things that are deprecated *as of* 2.0 :) warnings.filterwarnings( "ignore", diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py index fb90975a1..b2407ea18 100644 --- a/lib/sqlalchemy/util/__init__.py +++ b/lib/sqlalchemy/util/__init__.py @@ -92,6 +92,7 @@ from .deprecations import deprecated_20_cls # noqa from .deprecations import deprecated_cls # noqa from .deprecations import deprecated_params # noqa from .deprecations import inject_docstring_text # noqa +from .deprecations import SQLALCHEMY_WARN_20 # noqa from .deprecations import warn_deprecated # noqa from .deprecations import warn_deprecated_20 # noqa from .langhelpers import add_parameter_text # noqa @@ -149,6 +150,3 @@ from .langhelpers import warn # noqa from .langhelpers import warn_exception # noqa from .langhelpers import warn_limited # noqa from .langhelpers import wrap_callable # noqa - - -SQLALCHEMY_WARN_20 = False diff --git a/lib/sqlalchemy/util/deprecations.py b/lib/sqlalchemy/util/deprecations.py index e0669c4e8..0a79344c5 100644 --- a/lib/sqlalchemy/util/deprecations.py +++ b/lib/sqlalchemy/util/deprecations.py @@ -8,6 +8,7 @@ """Helpers related to deprecation of functions, methods, classes, other functionality.""" +import os import re import warnings @@ -19,7 +20,19 @@ from .langhelpers import inject_param_text from .. import exc +SQLALCHEMY_WARN_20 = False + +if os.getenv("SQLALCHEMY_WARN_20", "false").lower() in ("true", "yes", "1"): + SQLALCHEMY_WARN_20 = True + + def _warn_with_version(msg, version, type_, stacklevel): + if type_ is exc.RemovedIn20Warning and not SQLALCHEMY_WARN_20: + return + + if type_ is exc.RemovedIn20Warning: + msg += " (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)" + warn = type_(msg) warn.deprecated_since = version @@ -41,7 +54,6 @@ def warn_deprecated_limited(msg, args, version, stacklevel=3): def warn_deprecated_20(msg, stacklevel=3): - msg += " (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)" _warn_with_version( msg, @@ -69,7 +81,7 @@ def deprecated_cls(version, message, constructor="__init__"): def deprecated_20_cls(clsname, alternative=None, constructor="__init__"): message = ( - ".. deprecated:: 2.0 The %s class is considered legacy as of the " + ".. deprecated:: 1.4 The %s class is considered legacy as of the " "1.x series of SQLAlchemy and will be removed in 2.0." % clsname ) @@ -108,8 +120,16 @@ def deprecated( """ + # nothing is deprecated "since" 2.0 at this time. All "removed in 2.0" + # should emit the RemovedIn20Warning, but messaging should be expressed + # in terms of "deprecated since 1.4". + + if version == "2.0": + if warning is None: + warning = exc.RemovedIn20Warning + version = "1.4" if add_deprecation_to_docstring: - header = ".. deprecated:: %s %s" % (version, (message or "")) + header = ".. deprecated:: %s %s" % (version, (message or ""),) else: header = None @@ -119,7 +139,8 @@ def deprecated( if warning is None: warning = exc.SADeprecationWarning - message += " (deprecated since: %s)" % version + if warning is not exc.RemovedIn20Warning: + message += " (deprecated since: %s)" % version def decorate(fn): return _decorate_with_warning( @@ -162,6 +183,7 @@ def deprecated_params(**specs): messages = {} versions = {} version_warnings = {} + for param, (version, message) in specs.items(): versions[param] = version messages[param] = _sanitize_restructured_text(message) @@ -173,6 +195,7 @@ def deprecated_params(**specs): def decorate(fn): spec = compat.inspect_getfullargspec(fn) + if spec.defaults is not None: defaults = dict( zip( @@ -186,6 +209,8 @@ def deprecated_params(**specs): check_defaults = () check_kw = set(messages) + check_any_kw = spec.varkw + @decorator def warned(fn, *args, **kwargs): for m in check_defaults: @@ -198,6 +223,18 @@ def deprecated_params(**specs): version_warnings[m], stacklevel=3, ) + + if check_any_kw in messages and set(kwargs).difference( + check_defaults + ): + + _warn_with_version( + messages[check_any_kw], + versions[check_any_kw], + version_warnings[check_any_kw], + stacklevel=3, + ) + for m in check_kw: if m in kwargs: _warn_with_version( @@ -206,7 +243,6 @@ def deprecated_params(**specs): version_warnings[m], stacklevel=3, ) - return fn(*args, **kwargs) doc = fn.__doc__ is not None and fn.__doc__ or "" @@ -214,7 +250,8 @@ def deprecated_params(**specs): doc = inject_param_text( doc, { - param: ".. deprecated:: %s %s" % (version, (message or "")) + param: ".. deprecated:: %s %s" + % ("1.4" if version == "2.0" else version, (message or "")) for param, (version, message) in specs.items() }, ) diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 57d3be83b..28b7aa4cc 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -1701,9 +1701,9 @@ def inject_param_text(doctext, inject_params): while doclines: line = doclines.pop(0) if to_inject is None: - m = re.match(r"(\s+):param (?:\\\*\*?)?(.+?):", line) + m = re.match(r"(\s+):param (.+?):", line) if m: - param = m.group(2) + param = m.group(2).lstrip("*") if param in inject_params: # default indent to that of :param: plus one indent = " " * len(m.group(1)) + " " |
