summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/dialects/mssql/base.py62
-rw-r--r--lib/sqlalchemy/dialects/mssql/pyodbc.py19
-rw-r--r--lib/sqlalchemy/dialects/mysql/base.py10
-rw-r--r--lib/sqlalchemy/dialects/mysql/dml.py7
-rw-r--r--lib/sqlalchemy/dialects/postgresql/base.py12
-rw-r--r--lib/sqlalchemy/dialects/postgresql/dml.py7
-rw-r--r--lib/sqlalchemy/engine/result.py113
-rw-r--r--lib/sqlalchemy/orm/relationships.py21
-rw-r--r--lib/sqlalchemy/sql/expression.py88
-rw-r--r--lib/sqlalchemy/testing/exclusions.py45
-rw-r--r--lib/sqlalchemy/testing/plugin/pytestplugin.py164
-rw-r--r--lib/sqlalchemy/testing/profiling.py5
-rw-r--r--lib/sqlalchemy/testing/requirements.py7
-rw-r--r--lib/sqlalchemy/util/compat.py15
-rw-r--r--lib/sqlalchemy/util/langhelpers.py34
15 files changed, 406 insertions, 203 deletions
diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py
index 1c47902ab..1b1bd8c5c 100644
--- a/lib/sqlalchemy/dialects/mssql/base.py
+++ b/lib/sqlalchemy/dialects/mssql/base.py
@@ -245,9 +245,16 @@ CREATE TABLE statement for this column will yield::
LIMIT/OFFSET Support
--------------------
-MSSQL has no support for the LIMIT or OFFSET keywords. LIMIT is
-supported directly through the ``TOP`` Transact SQL keyword. A statement
-such as::
+MSSQL has added support for LIMIT / OFFSET as of SQL Server 2012, via the
+"OFFSET n ROWS" and "FETCH NEXT n ROWS" clauses. SQLAlchemy supports these
+syntaxes automatically if SQL Server 2012 or greater is detected.
+
+.. versionchanged:: 1.4 support added for SQL Server "OFFSET n ROWS" and
+ "FETCH NEXT n ROWS" syntax.
+
+For statements that specify only LIMIT and no OFFSET, all versions of SQL
+Server support the TOP keyword. This syntax is used for all SQL Server
+versions when no OFFSET clause is present. A statement such as::
select([some_table]).limit(5)
@@ -255,8 +262,9 @@ will render similarly to::
SELECT TOP 5 col1, col2.. FROM table
-LIMIT with OFFSET support is implemented using the using the ``ROW_NUMBER()``
-window function. A statement such as::
+For versions of SQL Server prior to SQL Server 2012, a statement that uses
+LIMIT and OFFSET, or just OFFSET alone, will be rendered using the
+``ROW_NUMBER()`` window function. A statement such as::
select([some_table]).order_by(some_table.c.col3).limit(5).offset(10)
@@ -267,8 +275,9 @@ will render similarly to::
mssql_rn FROM table WHERE t.x = :x_1) AS
anon_1 WHERE mssql_rn > :param_1 AND mssql_rn <= :param_2 + :param_1
-Note that when using LIMIT and OFFSET together, the statement must have
-an ORDER BY as well.
+Note that when using LIMIT and/or OFFSET, whether using the older
+or newer SQL Server syntaxes, the statement must have an ORDER BY as well,
+else a :class:`.CompileError` is raised.
.. _mssql_isolation_level:
@@ -1220,7 +1229,7 @@ class TryCast(sql.elements.Cast):
super(TryCast, self).__init__(*arg, **kw)
-try_cast = public_factory(TryCast, ".mssql.try_cast")
+try_cast = public_factory(TryCast, ".dialects.mssql.try_cast")
# old names.
MSDateTime = _MSDateTime
@@ -1641,10 +1650,7 @@ class MSSQLCompiler(compiler.SQLCompiler):
"""
if self.dialect._supports_offset_fetch and (
- (
- not select._simple_int_limit
- and select._limit_clause is not None
- )
+ (not select._simple_int_limit and select._limit_clause is not None)
or (
select._offset_clause is not None
and not select._simple_int_offset
@@ -1664,7 +1670,7 @@ class MSSQLCompiler(compiler.SQLCompiler):
if select._offset_clause is not None:
offset_str = self.process(select._offset_clause, **kw)
else:
- offset_str = '0'
+ offset_str = "0"
text += "\n OFFSET %s ROWS" % offset_str
if select._limit_clause is not None:
@@ -1687,14 +1693,21 @@ class MSSQLCompiler(compiler.SQLCompiler):
MSSQL 2012 and above are excluded
"""
- if not self.dialect._supports_offset_fetch and (
- (not select._simple_int_limit and select._limit_clause is not None)
- or (
- select._offset_clause is not None
- and not select._simple_int_offset
- or select._offset
+ if (
+ not self.dialect._supports_offset_fetch
+ and (
+ (
+ not select._simple_int_limit
+ and select._limit_clause is not None
+ )
+ or (
+ select._offset_clause is not None
+ and not select._simple_int_offset
+ or select._offset
+ )
)
- ) and not getattr(select, "_mssql_visit", None):
+ and not getattr(select, "_mssql_visit", None)
+ ):
# to use ROW_NUMBER(), an ORDER BY is required.
if not select._order_by_clause.clauses:
@@ -1880,7 +1893,11 @@ class MSSQLCompiler(compiler.SQLCompiler):
def order_by_clause(self, select, **kw):
# MSSQL only allows ORDER BY in subqueries if there is a LIMIT
- if self.is_subquery() and not select._limit:
+ if (
+ self.is_subquery()
+ and not select._limit
+ and (not select._offset or not self.dialect._supports_offset_fetch)
+ ):
# avoid processing the order by clause if we won't end up
# using it, because we don't want all the bind params tacked
# onto the positional list if that is what the dbapi requires
@@ -2476,7 +2493,8 @@ class MSDialect(default.DefaultDialect):
)
self._supports_offset_fetch = (
- self.server_version_info and self.server_version_info[0] >= 11)
+ self.server_version_info and self.server_version_info[0] >= 11
+ )
def _get_default_schema_name(self, connection):
if self.server_version_info < MS_2005_VERSION:
diff --git a/lib/sqlalchemy/dialects/mssql/pyodbc.py b/lib/sqlalchemy/dialects/mssql/pyodbc.py
index 879fe9a2a..4ba3a0dfa 100644
--- a/lib/sqlalchemy/dialects/mssql/pyodbc.py
+++ b/lib/sqlalchemy/dialects/mssql/pyodbc.py
@@ -234,10 +234,21 @@ class _ms_binary_pyodbc(object):
class _ODBCDateTimeOffset(DATETIMEOFFSET):
def bind_processor(self, dialect):
def process(value):
- """Convert to string format required by T-SQL."""
- dto_string = value.strftime("%Y-%m-%d %H:%M:%S.%f %z")
- # offset needs a colon, e.g., -0700 -> -07:00
- return dto_string[:30] + ":" + dto_string[30:]
+ if value is None:
+ return None
+ elif isinstance(value, util.string_types):
+ # if a string was passed directly, allow it through
+ return value
+ else:
+ # Convert to string format required by T-SQL
+ dto_string = value.strftime("%Y-%m-%d %H:%M:%S.%f %z")
+ # offset needs a colon, e.g., -0700 -> -07:00
+ # "UTC offset in the form (+-)HHMM[SS[.ffffff]]"
+ # backend currently rejects seconds / fractional seconds
+ dto_string = re.sub(
+ r"([\+\-]\d{2})([\d\.]+)$", r"\1:\2", dto_string
+ )
+ return dto_string
return process
diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py
index 6e84c9da1..e0bf16793 100644
--- a/lib/sqlalchemy/dialects/mysql/base.py
+++ b/lib/sqlalchemy/dialects/mysql/base.py
@@ -352,8 +352,8 @@ will be performed. The statement allows for separate specification of the
values to INSERT versus the values for UPDATE.
SQLAlchemy provides ``ON DUPLICATE KEY UPDATE`` support via the MySQL-specific
-:func:`.mysql.dml.insert()` function, which provides
-the generative method :meth:`~.mysql.dml.Insert.on_duplicate_key_update`::
+:func:`.mysql.insert()` function, which provides
+the generative method :meth:`~.mysql.Insert.on_duplicate_key_update`::
from sqlalchemy.dialects.mysql import insert
@@ -377,7 +377,7 @@ an error or to skip performing an UPDATE.
existing row, using any combination of new values as well as values
from the proposed insertion. These values are normally specified using
keyword arguments passed to the
-:meth:`~.mysql.dml.Insert.on_duplicate_key_update`
+:meth:`~.mysql.Insert.on_duplicate_key_update`
given column key values (usually the name of the column, unless it
specifies :paramref:`.Column.key`) as keys and literal or SQL expressions
as values::
@@ -421,8 +421,8 @@ this context is unambiguous::
In order to refer to the proposed insertion row, the special alias
-:attr:`~.mysql.dml.Insert.inserted` is available as an attribute on
-the :class:`.mysql.dml.Insert` object; this object is a
+:attr:`~.mysql.Insert.inserted` is available as an attribute on
+the :class:`.mysql.Insert` object; this object is a
:class:`.ColumnCollection` which contains all columns of the target
table::
diff --git a/lib/sqlalchemy/dialects/mysql/dml.py b/lib/sqlalchemy/dialects/mysql/dml.py
index b43b364fa..531b31bc3 100644
--- a/lib/sqlalchemy/dialects/mysql/dml.py
+++ b/lib/sqlalchemy/dialects/mysql/dml.py
@@ -15,6 +15,9 @@ class Insert(StandardInsert):
Adds methods for MySQL-specific syntaxes such as ON DUPLICATE KEY UPDATE.
+ The :class:`~.mysql.Insert` object is created using the
+ :func:`sqlalchemy.dialects.mysql.insert` function.
+
.. versionadded:: 1.2
"""
@@ -105,7 +108,9 @@ class Insert(StandardInsert):
self._post_values_clause = OnDuplicateClause(inserted_alias, values)
-insert = public_factory(Insert, ".dialects.mysql.insert")
+insert = public_factory(
+ Insert, ".dialects.mysql.insert", ".dialects.mysql.Insert"
+)
class OnDuplicateClause(ClauseElement):
diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py
index 82c619c85..ceefc20b0 100644
--- a/lib/sqlalchemy/dialects/postgresql/base.py
+++ b/lib/sqlalchemy/dialects/postgresql/base.py
@@ -304,9 +304,9 @@ or they may be *inferred* by stating the columns and conditions that comprise
the indexes.
SQLAlchemy provides ``ON CONFLICT`` support via the PostgreSQL-specific
-:func:`.postgresql.dml.insert()` function, which provides
-the generative methods :meth:`~.postgresql.dml.Insert.on_conflict_do_update`
-and :meth:`~.postgresql.dml.Insert.on_conflict_do_nothing`::
+:func:`.postgresql.insert()` function, which provides
+the generative methods :meth:`~.postgresql.Insert.on_conflict_do_update`
+and :meth:`~.postgresql.Insert.on_conflict_do_nothing`::
from sqlalchemy.dialects.postgresql import insert
@@ -415,8 +415,8 @@ for UPDATE::
:paramref:`.Insert.on_conflict_do_update.set_` dictionary.
In order to refer to the proposed insertion row, the special alias
-:attr:`~.postgresql.dml.Insert.excluded` is available as an attribute on
-the :class:`.postgresql.dml.Insert` object; this object is a
+:attr:`~.postgresql.Insert.excluded` is available as an attribute on
+the :class:`.postgresql.Insert` object; this object is a
:class:`.ColumnCollection` which alias contains all columns of the target
table::
@@ -452,7 +452,7 @@ parameter, which will limit those rows which receive an UPDATE::
``ON CONFLICT`` may also be used to skip inserting a row entirely
if any conflict with a unique or exclusion constraint occurs; below
this is illustrated using the
-:meth:`~.postgresql.dml.Insert.on_conflict_do_nothing` method::
+:meth:`~.postgresql.Insert.on_conflict_do_nothing` method::
from sqlalchemy.dialects.postgresql import insert
diff --git a/lib/sqlalchemy/dialects/postgresql/dml.py b/lib/sqlalchemy/dialects/postgresql/dml.py
index ec40ae87e..626f81018 100644
--- a/lib/sqlalchemy/dialects/postgresql/dml.py
+++ b/lib/sqlalchemy/dialects/postgresql/dml.py
@@ -23,6 +23,9 @@ class Insert(StandardInsert):
Adds methods for PG-specific syntaxes such as ON CONFLICT.
+ The :class:`.postgresql.Insert` object is created using the
+ :func:`sqlalchemy.dialects.postgresql.insert` function.
+
.. versionadded:: 1.1
"""
@@ -139,7 +142,9 @@ class Insert(StandardInsert):
)
-insert = public_factory(Insert, ".dialects.postgresql.insert")
+insert = public_factory(
+ Insert, ".dialects.postgresql.insert", ".dialects.postgresql.Insert"
+)
class OnConflictClause(ClauseElement):
diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py
index c2f976aa0..13738cb46 100644
--- a/lib/sqlalchemy/engine/result.py
+++ b/lib/sqlalchemy/engine/result.py
@@ -129,9 +129,12 @@ except ImportError:
class Row(BaseRow, collections_abc.Sequence):
"""Represent a single result row.
+ The :class:`.Row` object is retrieved from a database result, from the
+ :class:`.ResultProxy` object using methods like
+ :meth:`.ResultProxy.fetchall`.
+
The :class:`.Row` object seeks to act mostly like a Python named
- tuple, but also provides for mapping-oriented access via the
- :attr:`.Row._mapping` attribute.
+ tuple, but also provides some Python dictionary behaviors at the same time.
.. seealso::
@@ -189,7 +192,22 @@ class Row(BaseRow, collections_abc.Sequence):
return repr(sql_util._repr_row(self))
def has_key(self, key):
- """Return True if this Row contains the given key."""
+ """Return True if this :class:`.Row` contains the given key.
+
+ Through the SQLAlchemy 1.x series, the ``__contains__()`` method
+ of :class:`.Row` also links to :meth:`.Row.has_key`, in that
+ an expression such as ::
+
+ "some_col" in row
+
+ Will return True if the row contains a column named ``"some_col"``,
+ in the way that a Python mapping works.
+
+ However, it is planned that the 2.0 series of SQLAlchemy will reverse
+ this behavior so that ``__contains__()`` will refer to a value being
+ present in the row, in the way that a Python tuple works.
+
+ """
return self._parent._has_key(key)
@@ -197,23 +215,52 @@ class Row(BaseRow, collections_abc.Sequence):
return self._get_by_key_impl(key)
def items(self):
- """Return a list of tuples, each tuple containing a key/value pair."""
- # TODO: no coverage here
+ """Return a list of tuples, each tuple containing a key/value pair.
+
+ This method is analogous to the Python dictionary ``.items()`` method,
+ except that it returns a list, not an iterator.
+
+ """
+
return [(key, self[key]) for key in self.keys()]
def keys(self):
- """Return the list of keys as strings represented by this Row."""
+ """Return the list of keys as strings represented by this
+ :class:`.Row`.
+
+ This method is analogous to the Python dictionary ``.keys()`` method,
+ except that it returns a list, not an iterator.
+
+ """
return [k for k in self._parent.keys if k is not None]
def iterkeys(self):
+ """Return a an iterator against the :meth:`.Row.keys` method.
+
+ This method is analogous to the Python-2-only dictionary
+ ``.iterkeys()`` method.
+
+ """
return iter(self._parent.keys)
def itervalues(self):
+ """Return a an iterator against the :meth:`.Row.values` method.
+
+ This method is analogous to the Python-2-only dictionary
+ ``.itervalues()`` method.
+
+ """
return iter(self)
def values(self):
- """Return the values represented by this Row as a list."""
+ """Return the values represented by this :class:`.Row` as a list.
+
+ This method is analogous to the Python dictionary ``.values()`` method,
+ except that it returns a list, not an iterator.
+
+ """
+
return self._values_impl()
@@ -827,23 +874,16 @@ class ResultMetaData(object):
class ResultProxy(object):
- """Wraps a DB-API cursor object to provide easier access to row columns.
-
- Individual columns may be accessed by their integer position,
- case-insensitive column name, or by ``schema.Column``
- object. e.g.::
-
- row = fetchone()
-
- col1 = row[0] # access via integer position
+ """A facade around a DBAPI cursor object.
- col2 = row['col2'] # access via name
+ Returns database rows via the :class:`.Row` class, which provides
+ additional API features and behaviors on top of the raw data returned
+ by the DBAPI.
- col3 = row[mytable.c.mycol] # access via Column object.
+ .. seealso::
- ``ResultProxy`` also handles post-processing of result column
- data using ``TypeEngine`` objects, which are referenced from
- the originating SQL statement that produced this result set.
+ :ref:`coretutorial_selecting` - introductory material for accessing
+ :class:`.ResultProxy` and :class:`.Row` objects.
"""
@@ -912,7 +952,9 @@ class ResultProxy(object):
)
def keys(self):
- """Return the current set of string keys for rows."""
+ """Return the list of string keys that would represented by each
+ :class:`.Row`."""
+
if self._metadata:
return self._metadata.keys
else:
@@ -1085,8 +1127,6 @@ class ResultProxy(object):
:ref:`connections_toplevel`
- :meth:`.ResultProxy._soft_close`
-
"""
if not self.closed:
@@ -1104,7 +1144,10 @@ class ResultProxy(object):
yield row
def __next__(self):
- """Implement the next() protocol.
+ """Implement the Python next() protocol.
+
+ This method, mirrored as both ``.next()`` and ``.__next__()``, is part
+ of Python's API for producing iterator-like behavior.
.. versionadded:: 1.2
@@ -1358,9 +1401,7 @@ class ResultProxy(object):
an empty list. After the :meth:`.ResultProxy.close` method is
called, the method will raise :class:`.ResourceClosedError`.
- .. versionchanged:: 1.0.0 - Added "soft close" behavior which
- allows the result to be used in an "exhausted" state prior to
- calling the :meth:`.ResultProxy.close` method.
+ :return: a list of :class:`.Row` objects
"""
@@ -1386,9 +1427,7 @@ class ResultProxy(object):
an empty list. After the :meth:`.ResultProxy.close` method is
called, the method will raise :class:`.ResourceClosedError`.
- .. versionchanged:: 1.0.0 - Added "soft close" behavior which
- allows the result to be used in an "exhausted" state prior to
- calling the :meth:`.ResultProxy.close` method.
+ :return: a list of :class:`.Row` objects
"""
@@ -1414,9 +1453,7 @@ class ResultProxy(object):
After the :meth:`.ResultProxy.close` method is
called, the method will raise :class:`.ResourceClosedError`.
- .. versionchanged:: 1.0.0 - Added "soft close" behavior which
- allows the result to be used in an "exhausted" state prior to
- calling the :meth:`.ResultProxy.close` method.
+ :return: a :class:`.Row` object, or None if no rows remain
"""
try:
@@ -1434,11 +1471,11 @@ class ResultProxy(object):
def first(self):
"""Fetch the first row and then close the result set unconditionally.
- Returns None if no row is present.
-
After calling this method, the object is fully closed,
e.g. the :meth:`.ResultProxy.close` method will have been called.
+ :return: a :class:`.Row` object, or None if no rows remain
+
"""
if self._metadata is None:
return self._non_result(None)
@@ -1461,11 +1498,11 @@ class ResultProxy(object):
def scalar(self):
"""Fetch the first column of the first row, and close the result set.
- Returns None if no row is present.
-
After calling this method, the object is fully closed,
e.g. the :meth:`.ResultProxy.close` method will have been called.
+ :return: a Python scalar value , or None if no rows remain
+
"""
row = self.first()
if row is not None:
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index d745500c1..5573f7c9a 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -827,6 +827,19 @@ class RelationshipProperty(StrategizedProperty):
the full set of related objects, to prevent modifications of the
collection from resulting in persistence operations.
+ .. warning:: The viewonly=True relationship should not be mutated
+ in Python; that means, elements should not be added or removed
+ from collections nor should a many-to-one or one-to-one attribute
+ be altered in Python. The viewonly=True relationship should only
+ be accessed via read. Towards this behavior, it is also not
+ appropriate for the viewonly=True relationship to have any kind
+ of persistence cascade settings, nor should it be the target of
+ either :paramref:`.relationship.backref` or
+ :paramref:`.relationship.back_populates`, as backrefs imply
+ in-Python mutation of the attribute. SQLAlchemy may emit
+ warnings for some or all of these conditions as of the 1.3 and
+ 1.4 series of SQLAlchemy and will eventually be disallowed.
+
:param omit_join:
Allows manual control over the "selectin" automatic join
optimization. Set to ``False`` to disable the "omit join" feature
@@ -1841,6 +1854,14 @@ class RelationshipProperty(StrategizedProperty):
def _add_reverse_property(self, key):
other = self.mapper.get_property(key, _configure_mappers=False)
+ if other.viewonly:
+ util.warn_limited(
+ "Setting backref / back_populates on relationship %s to refer "
+ "to viewonly relationship %s will be deprecated in SQLAlchemy "
+ "1.4, and will be disallowed in a future release. "
+ "viewonly relationships should not be mutated",
+ (self, other),
+ )
self._reverse_property.add(other)
other._reverse_property.add(self)
diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py
index 9cd6a179a..7ea0e2ad3 100644
--- a/lib/sqlalchemy/sql/expression.py
+++ b/lib/sqlalchemy/sql/expression.py
@@ -164,62 +164,66 @@ from ..util.langhelpers import public_factory # noqa
# the functions to be available in the sqlalchemy.sql.* namespace and
# to be auto-cross-documenting from the function to the class itself.
-all_ = public_factory(CollectionAggregate._create_all, ".expression.all_")
-any_ = public_factory(CollectionAggregate._create_any, ".expression.any_")
-and_ = public_factory(BooleanClauseList.and_, ".expression.and_")
-alias = public_factory(Alias._factory, ".expression.alias")
-tablesample = public_factory(TableSample._factory, ".expression.tablesample")
-lateral = public_factory(Lateral._factory, ".expression.lateral")
-or_ = public_factory(BooleanClauseList.or_, ".expression.or_")
-bindparam = public_factory(BindParameter, ".expression.bindparam")
-select = public_factory(Select, ".expression.select")
-text = public_factory(TextClause._create_text, ".expression.text")
-table = public_factory(TableClause, ".expression.table")
-column = public_factory(ColumnClause, ".expression.column")
-over = public_factory(Over, ".expression.over")
-within_group = public_factory(WithinGroup, ".expression.within_group")
-label = public_factory(Label, ".expression.label")
-case = public_factory(Case, ".expression.case")
-cast = public_factory(Cast, ".expression.cast")
-cte = public_factory(CTE._factory, ".expression.cte")
-extract = public_factory(Extract, ".exp # noqaression.extract")
-tuple_ = public_factory(Tuple, ".expression.tuple_")
-except_ = public_factory(CompoundSelect._create_except, ".expression.except_")
+all_ = public_factory(CollectionAggregate._create_all, ".sql.expression.all_")
+any_ = public_factory(CollectionAggregate._create_any, ".sql.expression.any_")
+and_ = public_factory(BooleanClauseList.and_, ".sql.expression.and_")
+alias = public_factory(Alias._factory, ".sql.expression.alias")
+tablesample = public_factory(
+ TableSample._factory, ".sql.expression.tablesample"
+)
+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")
+text = public_factory(TextClause._create_text, ".sql.expression.text")
+table = public_factory(TableClause, ".sql.expression.table")
+column = public_factory(ColumnClause, ".sql.expression.column")
+over = public_factory(Over, ".sql.expression.over")
+within_group = public_factory(WithinGroup, ".sql.expression.within_group")
+label = public_factory(Label, ".sql.expression.label")
+case = public_factory(Case, ".sql.expression.case")
+cast = public_factory(Cast, ".sql.expression.cast")
+cte = public_factory(CTE._factory, ".sql.expression.cte")
+extract = public_factory(Extract, ".sql.expression.extract")
+tuple_ = public_factory(Tuple, ".sql.expression.tuple_")
+except_ = public_factory(
+ CompoundSelect._create_except, ".sql.expression.except_"
+)
except_all = public_factory(
- CompoundSelect._create_except_all, ".expression.except_all"
+ CompoundSelect._create_except_all, ".sql.expression.except_all"
)
intersect = public_factory(
- CompoundSelect._create_intersect, ".expression.intersect"
+ CompoundSelect._create_intersect, ".sql.expression.intersect"
)
intersect_all = public_factory(
- CompoundSelect._create_intersect_all, ".expression.intersect_all"
+ CompoundSelect._create_intersect_all, ".sql.expression.intersect_all"
)
-union = public_factory(CompoundSelect._create_union, ".expression.union")
+union = public_factory(CompoundSelect._create_union, ".sql.expression.union")
union_all = public_factory(
- CompoundSelect._create_union_all, ".expression.union_all"
+ CompoundSelect._create_union_all, ".sql.expression.union_all"
)
-exists = public_factory(Exists, ".expression.exists")
+exists = public_factory(Exists, ".sql.expression.exists")
nullsfirst = public_factory(
- UnaryExpression._create_nullsfirst, ".expression.nullsfirst"
+ UnaryExpression._create_nullsfirst, ".sql.expression.nullsfirst"
)
nullslast = public_factory(
- UnaryExpression._create_nullslast, ".expression.nullslast"
+ UnaryExpression._create_nullslast, ".sql.expression.nullslast"
)
-asc = public_factory(UnaryExpression._create_asc, ".expression.asc")
-desc = public_factory(UnaryExpression._create_desc, ".expression.desc")
+asc = public_factory(UnaryExpression._create_asc, ".sql.expression.asc")
+desc = public_factory(UnaryExpression._create_desc, ".sql.expression.desc")
distinct = public_factory(
- UnaryExpression._create_distinct, ".expression.distinct"
+ UnaryExpression._create_distinct, ".sql.expression.distinct"
)
-type_coerce = public_factory(TypeCoerce, ".expression.type_coerce")
-true = public_factory(True_._instance, ".expression.true")
-false = public_factory(False_._instance, ".expression.false")
-null = public_factory(Null._instance, ".expression.null")
-join = public_factory(Join._create_join, ".expression.join")
-outerjoin = public_factory(Join._create_outerjoin, ".expression.outerjoin")
-insert = public_factory(Insert, ".expression.insert")
-update = public_factory(Update, ".expression.update")
-delete = public_factory(Delete, ".expression.delete")
-funcfilter = public_factory(FunctionFilter, ".expression.funcfilter")
+type_coerce = public_factory(TypeCoerce, ".sql.expression.type_coerce")
+true = public_factory(True_._instance, ".sql.expression.true")
+false = public_factory(False_._instance, ".sql.expression.false")
+null = public_factory(Null._instance, ".sql.expression.null")
+join = public_factory(Join._create_join, ".sql.expression.join")
+outerjoin = public_factory(Join._create_outerjoin, ".sql.expression.outerjoin")
+insert = public_factory(Insert, ".sql.expression.insert")
+update = public_factory(Update, ".sql.expression.update")
+delete = public_factory(Delete, ".sql.expression.delete")
+funcfilter = public_factory(FunctionFilter, ".sql.expression.funcfilter")
# internal functions still being called from tests and the ORM,
diff --git a/lib/sqlalchemy/testing/exclusions.py b/lib/sqlalchemy/testing/exclusions.py
index b2828b107..0c05bf9e9 100644
--- a/lib/sqlalchemy/testing/exclusions.py
+++ b/lib/sqlalchemy/testing/exclusions.py
@@ -35,20 +35,10 @@ class compound(object):
self.fails = set()
self.skips = set()
self.tags = set()
- self.combinations = {}
def __add__(self, other):
return self.add(other)
- def with_combination(self, **kw):
- copy = compound()
- copy.fails.update(self.fails)
- copy.skips.update(self.skips)
- copy.tags.update(self.tags)
- copy.combinations.update((f, kw) for f in copy.fails)
- copy.combinations.update((s, kw) for s in copy.skips)
- return copy
-
def add(self, *others):
copy = compound()
copy.fails.update(self.fails)
@@ -95,7 +85,6 @@ class compound(object):
self.skips.update(other.skips)
self.fails.update(other.fails)
self.tags.update(other.tags)
- self.combinations.update(other.combinations)
def __call__(self, fn):
if hasattr(fn, "_sa_exclusion_extend"):
@@ -118,29 +107,13 @@ class compound(object):
try:
yield
except Exception as ex:
- all_fails._expect_failure(config._current, ex, None)
+ all_fails._expect_failure(config._current, ex)
else:
- all_fails._expect_success(config._current, None)
-
- def _check_combinations(self, combination, predicate):
- if predicate in self.combinations:
- for k, v in combination:
- if (
- k in self.combinations[predicate]
- and self.combinations[predicate][k] != v
- ):
- return False
- return True
+ all_fails._expect_success(config._current)
def _do(self, cfg, fn, *args, **kw):
- if len(args) > 1:
- insp = inspect_getfullargspec(fn)
- combination = list(zip(insp.args[1:], args[1:]))
- else:
- combination = None
-
for skip in self.skips:
- if self._check_combinations(combination, skip) and skip(cfg):
+ if skip(cfg):
msg = "'%s' : %s" % (
config.get_current_test_name(),
skip._as_string(cfg),
@@ -150,14 +123,14 @@ class compound(object):
try:
return_value = fn(*args, **kw)
except Exception as ex:
- self._expect_failure(cfg, ex, combination, name=fn.__name__)
+ self._expect_failure(cfg, ex, name=fn.__name__)
else:
- self._expect_success(cfg, combination, name=fn.__name__)
+ self._expect_success(cfg, name=fn.__name__)
return return_value
- def _expect_failure(self, config, ex, combination, name="block"):
+ def _expect_failure(self, config, ex, name="block"):
for fail in self.fails:
- if self._check_combinations(combination, fail) and fail(config):
+ if fail(config):
if util.py2k:
str_ex = unicode(ex).encode( # noqa: F821
"utf-8", errors="ignore"
@@ -174,12 +147,12 @@ class compound(object):
else:
util.raise_from_cause(ex)
- def _expect_success(self, config, combination, name="block"):
+ def _expect_success(self, config, name="block"):
if not self.fails:
return
for fail in self.fails:
- if self._check_combinations(combination, fail) and fail(config):
+ if fail(config):
raise AssertionError(
"Unexpected success for '%s' (%s)"
% (
diff --git a/lib/sqlalchemy/testing/plugin/pytestplugin.py b/lib/sqlalchemy/testing/plugin/pytestplugin.py
index 3e0630890..c39f9f32e 100644
--- a/lib/sqlalchemy/testing/plugin/pytestplugin.py
+++ b/lib/sqlalchemy/testing/plugin/pytestplugin.py
@@ -7,6 +7,7 @@ except ImportError:
import argparse
import collections
+from functools import update_wrapper
import inspect
import itertools
import operator
@@ -16,6 +17,13 @@ import sys
import pytest
+try:
+ import typing
+except ImportError:
+ pass
+else:
+ if typing.TYPE_CHECKING:
+ from typing import Sequence
try:
import xdist # noqa
@@ -295,6 +303,49 @@ def getargspec(fn):
return inspect.getargspec(fn)
+def _pytest_fn_decorator(target):
+ """Port of langhelpers.decorator with pytest-specific tricks."""
+
+ from sqlalchemy.util.langhelpers import format_argspec_plus
+ from sqlalchemy.util.compat import inspect_getfullargspec
+
+ def _exec_code_in_env(code, env, fn_name):
+ exec(code, env)
+ return env[fn_name]
+
+ def decorate(fn, add_positional_parameters=()):
+
+ spec = inspect_getfullargspec(fn)
+ if add_positional_parameters:
+ spec.args.extend(add_positional_parameters)
+
+ metadata = dict(target="target", fn="fn", name=fn.__name__)
+ metadata.update(format_argspec_plus(spec, grouped=False))
+ code = (
+ """\
+def %(name)s(%(args)s):
+ return %(target)s(%(fn)s, %(apply_kw)s)
+"""
+ % metadata
+ )
+ decorated = _exec_code_in_env(
+ code, {"target": target, "fn": fn}, fn.__name__
+ )
+ if not add_positional_parameters:
+ decorated.__defaults__ = getattr(fn, "im_func", fn).__defaults__
+ decorated.__wrapped__ = fn
+ return update_wrapper(decorated, fn)
+ else:
+ # this is the pytest hacky part. don't do a full update wrapper
+ # because pytest is really being sneaky about finding the args
+ # for the wrapped function
+ decorated.__module__ = fn.__module__
+ decorated.__name__ = fn.__name__
+ return decorated
+
+ return decorate
+
+
class PytestFixtureFunctions(plugin_base.FixtureFunctions):
def skip_test_exception(self, *arg, **kw):
return pytest.skip.Exception(*arg, **kw)
@@ -326,8 +377,6 @@ class PytestFixtureFunctions(plugin_base.FixtureFunctions):
argnames = kw.pop("argnames", None)
- exclusion_combinations = []
-
def _filter_exclusions(args):
result = []
gathered_exclusions = []
@@ -337,13 +386,12 @@ class PytestFixtureFunctions(plugin_base.FixtureFunctions):
else:
result.append(a)
- exclusion_combinations.extend(
- [(exclusion, result) for exclusion in gathered_exclusions]
- )
- return result
+ return result, gathered_exclusions
id_ = kw.pop("id_", None)
+ tobuild_pytest_params = []
+ has_exclusions = False
if id_:
_combination_id_fns = self._combination_id_fns
@@ -364,53 +412,87 @@ class PytestFixtureFunctions(plugin_base.FixtureFunctions):
if char in _combination_id_fns
]
- arg_sets = [
- pytest.param(
- *_arg_getter(_filter_exclusions(arg))[1:],
- id="-".join(
- comb_fn(getter(arg)) for getter, comb_fn in fns
+ for arg in arg_sets:
+ if not isinstance(arg, tuple):
+ arg = (arg,)
+
+ fn_params, param_exclusions = _filter_exclusions(arg)
+
+ parameters = _arg_getter(fn_params)[1:]
+
+ if param_exclusions:
+ has_exclusions = True
+
+ tobuild_pytest_params.append(
+ (
+ parameters,
+ param_exclusions,
+ "-".join(
+ comb_fn(getter(arg)) for getter, comb_fn in fns
+ ),
)
)
- for arg in [
- (arg,) if not isinstance(arg, tuple) else arg
- for arg in arg_sets
- ]
- ]
+
else:
- # ensure using pytest.param so that even a 1-arg paramset
- # still needs to be a tuple. otherwise paramtrize tries to
- # interpret a single arg differently than tuple arg
- arg_sets = [
- pytest.param(*_filter_exclusions(arg))
- for arg in [
- (arg,) if not isinstance(arg, tuple) else arg
- for arg in arg_sets
- ]
- ]
+
+ for arg in arg_sets:
+ if not isinstance(arg, tuple):
+ arg = (arg,)
+
+ fn_params, param_exclusions = _filter_exclusions(arg)
+
+ if param_exclusions:
+ has_exclusions = True
+
+ tobuild_pytest_params.append(
+ (fn_params, param_exclusions, None)
+ )
+
+ pytest_params = []
+ for parameters, param_exclusions, id_ in tobuild_pytest_params:
+ if has_exclusions:
+ parameters += (param_exclusions,)
+
+ param = pytest.param(*parameters, id=id_)
+ pytest_params.append(param)
def decorate(fn):
if inspect.isclass(fn):
+ if has_exclusions:
+ raise NotImplementedError(
+ "exclusions not supported for class level combinations"
+ )
if "_sa_parametrize" not in fn.__dict__:
fn._sa_parametrize = []
- fn._sa_parametrize.append((argnames, arg_sets))
+ fn._sa_parametrize.append((argnames, pytest_params))
return fn
else:
if argnames is None:
- _argnames = getargspec(fn).args[1:]
+ _argnames = getargspec(fn).args[1:] # type: Sequence(str)
else:
- _argnames = argnames
-
- if exclusion_combinations:
- for exclusion, combination in exclusion_combinations:
- combination_by_kw = {
- argname: val
- for argname, val in zip(_argnames, combination)
- }
- exclusion = exclusion.with_combination(
- **combination_by_kw
- )
- fn = exclusion(fn)
- return pytest.mark.parametrize(_argnames, arg_sets)(fn)
+ _argnames = re.split(
+ r", *", argnames
+ ) # type: Sequence(str)
+
+ if has_exclusions:
+ _argnames += ["_exclusions"]
+
+ @_pytest_fn_decorator
+ def check_exclusions(fn, *args, **kw):
+ _exclusions = args[-1]
+ if _exclusions:
+ exlu = exclusions.compound().add(*_exclusions)
+ fn = exlu(fn)
+ return fn(*args[0:-1], **kw)
+
+ def process_metadata(spec):
+ spec.args.append("_exclusions")
+
+ fn = check_exclusions(
+ fn, add_positional_parameters=("_exclusions",)
+ )
+
+ return pytest.mark.parametrize(_argnames, pytest_params)(fn)
return decorate
diff --git a/lib/sqlalchemy/testing/profiling.py b/lib/sqlalchemy/testing/profiling.py
index cc6557018..0026b5f8c 100644
--- a/lib/sqlalchemy/testing/profiling.py
+++ b/lib/sqlalchemy/testing/profiling.py
@@ -220,7 +220,7 @@ class ProfileStatsFile(object):
profile_f.close()
-def function_call_count(variance=0.05, times=1):
+def function_call_count(variance=0.05, times=1, warmup=0):
"""Assert a target for a test case's function call count.
The main purpose of this assertion is to detect changes in
@@ -239,6 +239,9 @@ def function_call_count(variance=0.05, times=1):
@decorator
def wrap(fn, *args, **kw):
+ for warm in range(warmup):
+ fn(*args, **kw)
+
timerange = range(times)
with count_functions(variance=variance):
for time in timerange:
diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py
index 03d76eccb..c12d1e6e8 100644
--- a/lib/sqlalchemy/testing/requirements.py
+++ b/lib/sqlalchemy/testing/requirements.py
@@ -1029,6 +1029,13 @@ class SuiteRequirements(Requirements):
)
@property
+ def python37(self):
+ return exclusions.skip_if(
+ lambda: sys.version_info < (3, 7),
+ "Python version 3.7 or greater is required.",
+ )
+
+ @property
def cpython(self):
return exclusions.only_if(
lambda: util.cpython, "cPython interpreter needed"
diff --git a/lib/sqlalchemy/util/compat.py b/lib/sqlalchemy/util/compat.py
index ea9ac0f11..8967955cd 100644
--- a/lib/sqlalchemy/util/compat.py
+++ b/lib/sqlalchemy/util/compat.py
@@ -270,7 +270,18 @@ else:
if py35:
- from inspect import formatannotation
+
+ def _formatannotation(annotation, base_module=None):
+ """vendored from python 3.7
+ """
+
+ if getattr(annotation, "__module__", None) == "typing":
+ return repr(annotation).replace("typing.", "")
+ if isinstance(annotation, type):
+ if annotation.__module__ in ("builtins", base_module):
+ return annotation.__qualname__
+ return annotation.__module__ + "." + annotation.__qualname__
+ return repr(annotation)
def inspect_formatargspec(
args,
@@ -285,7 +296,7 @@ if py35:
formatvarkw=lambda name: "**" + name,
formatvalue=lambda value: "=" + repr(value),
formatreturns=lambda text: " -> " + text,
- formatannotation=formatannotation,
+ formatannotation=_formatannotation,
):
"""Copy formatargspec from python 3.7 standard library.
diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py
index 402f8bb09..41a9698c7 100644
--- a/lib/sqlalchemy/util/langhelpers.py
+++ b/lib/sqlalchemy/util/langhelpers.py
@@ -170,7 +170,7 @@ def _exec_code_in_env(code, env, fn_name):
return env[fn_name]
-def public_factory(target, location):
+def public_factory(target, location, class_location=None):
"""Produce a wrapping function for the given cls or classmethod.
Rationale here is so that the __init__ method of the
@@ -183,14 +183,14 @@ def public_factory(target, location):
doc = (
"Construct a new :class:`.%s` object. \n\n"
"This constructor is mirrored as a public API function; "
- "see :func:`~%s` "
+ "see :func:`sqlalchemy%s` "
"for a full usage and argument description."
% (target.__name__, location)
)
else:
fn = callable_ = target
doc = (
- "This function is mirrored; see :func:`~%s` "
+ "This function is mirrored; see :func:`sqlalchemy%s` "
"for a description of arguments." % location
)
@@ -209,12 +209,38 @@ def %(name)s(%(args)s):
env = {"cls": callable_, "symbol": symbol}
exec(code, env)
decorated = env[location_name]
- decorated.__doc__ = fn.__doc__
+ if hasattr(fn, "_linked_to"):
+ linked_to, linked_to_location = fn._linked_to
+ linked_to_doc = linked_to.__doc__
+ if class_location is None:
+ class_location = "%s.%s" % (target.__module__, target.__name__)
+
+ linked_to_doc = inject_docstring_text(
+ linked_to_doc,
+ ".. container:: inherited_member\n\n "
+ "Inherited from :func:`sqlalchemy%s`; this constructor "
+ "creates a :class:`%s` object"
+ % (linked_to_location, class_location),
+ 0,
+ )
+ decorated.__doc__ = linked_to_doc
+ else:
+ decorated.__doc__ = fn.__doc__
+
decorated.__module__ = "sqlalchemy" + location.rsplit(".", 1)[0]
+ if decorated.__module__ not in sys.modules:
+ raise ImportError(
+ "public_factory location %s is not in sys.modules"
+ % (decorated.__module__,)
+ )
if compat.py2k or hasattr(fn, "__func__"):
fn.__func__.__doc__ = doc
+ if not hasattr(fn.__func__, "_linked_to"):
+ fn.__func__._linked_to = (decorated, location)
else:
fn.__doc__ = doc
+ if not hasattr(fn, "_linked_to"):
+ fn._linked_to = (decorated, location)
return decorated