From 660ff51df0433607b12c58a12c7355107c1773f5 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 8 Feb 2020 14:53:21 -0500 Subject: [PATCH] Fixes for public_factory and mysql/pg dml functions * ensure that the location indicated by public_factory is importable * adjust all of sqlalchemy.sql.expression locations to be correct * support the case where a public_factory is against a function that has another public_factory already, and already replaced the __init__ on the target class * Use mysql.insert(), postgresql.insert(), don't include .dml in the class path. Change-Id: Iac285289455d8d7102349df3814f7cedc758e639 --- doc/build/dialects/mysql.rst | 4 +- doc/build/dialects/postgresql.rst | 4 +- lib/sqlalchemy/dialects/mssql/base.py | 33 ++++---- lib/sqlalchemy/dialects/mysql/base.py | 10 +-- lib/sqlalchemy/dialects/mysql/dml.py | 7 +- lib/sqlalchemy/dialects/postgresql/base.py | 12 +-- lib/sqlalchemy/dialects/postgresql/dml.py | 7 +- lib/sqlalchemy/sql/expression.py | 88 +++++++++++----------- lib/sqlalchemy/util/langhelpers.py | 34 ++++++++- 9 files changed, 122 insertions(+), 77 deletions(-) diff --git a/doc/build/dialects/mysql.rst b/doc/build/dialects/mysql.rst index bfd345bc04..760a0a91d6 100644 --- a/doc/build/dialects/mysql.rst +++ b/doc/build/dialects/mysql.rst @@ -159,9 +159,9 @@ construction arguments, are as follows: MySQL DML Constructs ------------------------- -.. autofunction:: sqlalchemy.dialects.mysql.dml.insert +.. autofunction:: sqlalchemy.dialects.mysql.insert -.. autoclass:: sqlalchemy.dialects.mysql.dml.Insert +.. autoclass:: sqlalchemy.dialects.mysql.Insert :members: diff --git a/doc/build/dialects/postgresql.rst b/doc/build/dialects/postgresql.rst index e03fba8d30..35ed285eb2 100644 --- a/doc/build/dialects/postgresql.rst +++ b/doc/build/dialects/postgresql.rst @@ -181,9 +181,9 @@ For example:: PostgreSQL DML Constructs ------------------------- -.. autofunction:: sqlalchemy.dialects.postgresql.dml.insert +.. autofunction:: sqlalchemy.dialects.postgresql.insert -.. autoclass:: sqlalchemy.dialects.postgresql.dml.Insert +.. autoclass:: sqlalchemy.dialects.postgresql.Insert :members: psycopg2 diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 609a60f7ce..6f3e8fb44a 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -1220,7 +1220,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 +1641,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 +1661,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 +1684,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: @@ -2476,7 +2480,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/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 6e84c9da1d..e0bf167931 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 b43b364fab..531b31bc33 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 82c619c858..ceefc20b01 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 ec40ae87e2..626f810186 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/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 9cd6a179ad..7ea0e2ad33 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/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 402f8bb093..41a9698c7d 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 -- 2.47.2