summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2020-06-26 16:15:19 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2020-07-08 11:05:11 -0400
commit91f376692d472a5bf0c4b4033816250ec1ce3ab6 (patch)
tree31f7f72cbe981eb73ed0ba11808d4fb5ae6b7d51 /lib/sqlalchemy
parent3dc9a4a2392d033f9d1bd79dd6b6ecea6281a61c (diff)
downloadsqlalchemy-91f376692d472a5bf0c4b4033816250ec1ce3ab6.tar.gz
Add future=True to create_engine/Session; unify select()
Several weeks of using the future_select() construct has led to the proposal there be just one select() construct again which features the new join() method, and otherwise accepts both the 1.x and 2.x argument styles. This would make migration simpler and reduce confusion. However, confusion may be increased by the fact that select().join() is different Current thinking is we may be better off with a few hard behavioral changes to old and relatively unknown APIs rather than trying to play both sides within two extremely similar but subtly different APIs. At the moment, the .join() thing seems to be the only behavioral change that occurs without the user taking any explicit steps. Session.execute() will still behave the old way as we are adding a future flag. This change also adds the "future" flag to Session() and session.execute(), so that interpretation of the incoming statement, as well as that the new style result is returned, does not occur for existing applications unless they add the use of this flag. The change in general is moving the "removed in 2.0" system further along where we want the test suite to fully pass even if the SQLALCHEMY_WARN_20 flag is set. Get many tests to pass when SQLALCHEMY_WARN_20 is set; this should be ongoing after this patch merges. Improve the RemovedIn20 warning; these are all deprecated "since" 1.4, so ensure that's what the messages read. Make sure the inforamtion link is on all warnings. Add deprecation warnings for parameters present and add warnings to all FromClause.select() types of methods. Fixes: #5379 Fixes: #5284 Change-Id: I765a0b912b3dcd0e995426427d8bb7997cbffd51 References: #5159
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/cextension/resultproxy.c3
-rw-r--r--lib/sqlalchemy/dialects/mssql/base.py66
-rw-r--r--lib/sqlalchemy/engine/base.py1
-rw-r--r--lib/sqlalchemy/engine/create.py18
-rw-r--r--lib/sqlalchemy/engine/default.py6
-rw-r--r--lib/sqlalchemy/ext/baked.py2
-rw-r--r--lib/sqlalchemy/future/__init__.py2
-rw-r--r--lib/sqlalchemy/future/selectable.py165
-rw-r--r--lib/sqlalchemy/orm/context.py28
-rw-r--r--lib/sqlalchemy/orm/loading.py1
-rw-r--r--lib/sqlalchemy/orm/persistence.py15
-rw-r--r--lib/sqlalchemy/orm/query.py9
-rw-r--r--lib/sqlalchemy/orm/relationships.py16
-rw-r--r--lib/sqlalchemy/orm/session.py43
-rw-r--r--lib/sqlalchemy/sql/base.py22
-rw-r--r--lib/sqlalchemy/sql/expression.py2
-rw-r--r--lib/sqlalchemy/sql/selectable.py517
-rw-r--r--lib/sqlalchemy/sql/util.py22
-rw-r--r--lib/sqlalchemy/testing/assertions.py6
-rw-r--r--lib/sqlalchemy/testing/suite/test_types.py78
-rw-r--r--lib/sqlalchemy/testing/warnings.py3
-rw-r--r--lib/sqlalchemy/util/__init__.py4
-rw-r--r--lib/sqlalchemy/util/deprecations.py49
-rw-r--r--lib/sqlalchemy/util/langhelpers.py4
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 cbe7bde33..0cc16b96e 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
@@ -1940,7 +1940,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
@@ -1989,7 +1989,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
@@ -2007,6 +2007,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 e8726000b..f4aa878ab 100644
--- a/lib/sqlalchemy/sql/util.py
+++ b/lib/sqlalchemy/sql/util.py
@@ -959,25 +959,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)) + " "