From 06aee70ea2a936901422fdb48baefbdbd7d24fbb Mon Sep 17 00:00:00 2001 From: Georg Wicke-Arndt Date: Wed, 10 Jan 2024 13:35:14 -0500 Subject: [PATCH 01/45] Ignore newlines in expressions for Computed Fixes #1391 Closes: #1393 Pull-request: https://github.com/sqlalchemy/alembic/pull/1393 Pull-request-sha: 69959241d491d9e08c4fb0bc38328a232e89811b Change-Id: I4aa1e8c344aa87a7277bf1f57dde67a37f510bff --- alembic/autogenerate/compare.py | 2 +- alembic/testing/suite/test_autogen_computed.py | 1 + docs/build/unreleased/1391.rst | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 docs/build/unreleased/1391.rst diff --git a/alembic/autogenerate/compare.py b/alembic/autogenerate/compare.py index fcef531a..0d985196 100644 --- a/alembic/autogenerate/compare.py +++ b/alembic/autogenerate/compare.py @@ -983,7 +983,7 @@ def _normalize_computed_default(sqltext: str) -> str: """ - return re.sub(r"[ \(\)'\"`\[\]]", "", sqltext).lower() + return re.sub(r"[ \(\)'\"`\[\]\t\r\n]", "", sqltext).lower() def _compare_computed_default( diff --git a/alembic/testing/suite/test_autogen_computed.py b/alembic/testing/suite/test_autogen_computed.py index 01a89a1f..04a3caf0 100644 --- a/alembic/testing/suite/test_autogen_computed.py +++ b/alembic/testing/suite/test_autogen_computed.py @@ -124,6 +124,7 @@ def test_cant_change_computed_warning(self, test_case): lambda: (None, None), lambda: (sa.Computed("5"), sa.Computed("5")), lambda: (sa.Computed("bar*5"), sa.Computed("bar*5")), + lambda: (sa.Computed("bar*5"), sa.Computed("bar * \r\n\t5")), ( lambda: (sa.Computed("bar*5"), None), config.requirements.computed_doesnt_reflect_as_server_default, diff --git a/docs/build/unreleased/1391.rst b/docs/build/unreleased/1391.rst new file mode 100644 index 00000000..c0661fbe --- /dev/null +++ b/docs/build/unreleased/1391.rst @@ -0,0 +1,6 @@ +.. change:: + :tags: usecase, autogenerate + :tickets: 1391 + + Improve computed column compare function to support multi-line expressions. + Pull request courtesy of Georg Wicke-Arndt. From 8829bd64a70708ef9144604ea0f0a5179d07ccbe Mon Sep 17 00:00:00 2001 From: Giovanni Pellerano Date: Mon, 22 Jan 2024 14:16:55 -0500 Subject: [PATCH 02/45] Update license year to 2024 None Closes: #1400 Pull-request: https://github.com/sqlalchemy/alembic/pull/1400 Pull-request-sha: 477619ca80e04484ac5bf46c03e7c39df3ecd2bc Change-Id: I7f419a6c0d6b31ccd5731914657cdda41b8f4802 --- LICENSE | 4 ++-- docs/build/conf.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 74b9ce34..be8de008 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2009-2023 Michael Bayer. +Copyright 2009-2024 Michael Bayer. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/docs/build/conf.py b/docs/build/conf.py index ddcb754c..80a2082f 100644 --- a/docs/build/conf.py +++ b/docs/build/conf.py @@ -90,7 +90,7 @@ # General information about the project. project = "Alembic" -copyright = "2010-2023, Mike Bayer" # noqa +copyright = "2010-2024, Mike Bayer" # noqa # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From a22d21a43c2eb7a12944377168727ed76a51aaa9 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Wed, 14 Feb 2024 20:35:24 +0100 Subject: [PATCH 03/45] Update black to 24.1.1 Change-Id: Iebd9b9e866a6a58541c187e70d4f170fdf84daff --- .pre-commit-config.yaml | 2 +- alembic/autogenerate/api.py | 6 +- alembic/autogenerate/render.py | 40 +++++++----- alembic/config.py | 15 ++--- alembic/ddl/_autogen.py | 26 ++++---- alembic/ddl/base.py | 9 +-- alembic/ddl/impl.py | 15 +++-- alembic/ddl/mysql.py | 80 ++++++++++++++--------- alembic/ddl/oracle.py | 8 ++- alembic/operations/base.py | 9 +-- alembic/operations/ops.py | 6 +- alembic/operations/schemaobj.py | 10 +-- alembic/runtime/environment.py | 12 ++-- alembic/runtime/migration.py | 18 ++--- alembic/script/base.py | 18 ++--- alembic/script/revision.py | 43 +++++++----- alembic/testing/fixtures.py | 22 ++++--- alembic/testing/suite/test_environment.py | 6 +- alembic/util/langhelpers.py | 9 +-- alembic/util/sqla_compat.py | 6 +- reap_dbs.py | 1 + setup.cfg | 2 +- tests/test_autogen_indexes.py | 9 +-- tests/test_autogen_render.py | 1 - tests/test_batch.py | 26 ++++---- tests/test_mssql.py | 7 +- tests/test_version_traversal.py | 2 - tox.ini | 4 +- 28 files changed, 226 insertions(+), 186 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f1a8b418..ac4be898 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/python/black - rev: 23.3.0 + rev: 24.1.1 hooks: - id: black diff --git a/alembic/autogenerate/api.py b/alembic/autogenerate/api.py index aa8f32f6..4c039162 100644 --- a/alembic/autogenerate/api.py +++ b/alembic/autogenerate/api.py @@ -596,9 +596,9 @@ def _run_environment( migration_script = self.generated_revisions[-1] if not getattr(migration_script, "_needs_render", False): migration_script.upgrade_ops_list[-1].upgrade_token = upgrade_token - migration_script.downgrade_ops_list[ - -1 - ].downgrade_token = downgrade_token + migration_script.downgrade_ops_list[-1].downgrade_token = ( + downgrade_token + ) migration_script._needs_render = True else: migration_script._upgrade_ops.append( diff --git a/alembic/autogenerate/render.py b/alembic/autogenerate/render.py index 317a6dbe..61d56acf 100644 --- a/alembic/autogenerate/render.py +++ b/alembic/autogenerate/render.py @@ -187,9 +187,11 @@ def _render_create_table_comment( prefix=_alembic_autogenerate_prefix(autogen_context), tname=op.table_name, comment="%r" % op.comment if op.comment is not None else None, - existing="%r" % op.existing_comment - if op.existing_comment is not None - else None, + existing=( + "%r" % op.existing_comment + if op.existing_comment is not None + else None + ), schema="'%s'" % op.schema if op.schema is not None else None, indent=" ", ) @@ -216,9 +218,11 @@ def _render_drop_table_comment( return templ.format( prefix=_alembic_autogenerate_prefix(autogen_context), tname=op.table_name, - existing="%r" % op.existing_comment - if op.existing_comment is not None - else None, + existing=( + "%r" % op.existing_comment + if op.existing_comment is not None + else None + ), schema="'%s'" % op.schema if op.schema is not None else None, indent=" ", ) @@ -328,9 +332,11 @@ def _add_index(autogen_context: AutogenContext, op: ops.CreateIndexOp) -> str: _get_index_rendered_expressions(index, autogen_context) ), "unique": index.unique or False, - "schema": (", schema=%r" % _ident(index.table.schema)) - if index.table.schema - else "", + "schema": ( + (", schema=%r" % _ident(index.table.schema)) + if index.table.schema + else "" + ), "kwargs": ", " + ", ".join(opts) if opts else "", } return text @@ -592,9 +598,11 @@ def _get_index_rendered_expressions( idx: Index, autogen_context: AutogenContext ) -> List[str]: return [ - repr(_ident(getattr(exp, "name", None))) - if isinstance(exp, sa_schema.Column) - else _render_potential_expr(exp, autogen_context, is_index=True) + ( + repr(_ident(getattr(exp, "name", None))) + if isinstance(exp, sa_schema.Column) + else _render_potential_expr(exp, autogen_context, is_index=True) + ) for exp in idx.expressions ] @@ -1075,9 +1083,11 @@ def _render_check_constraint( ) return "%(prefix)sCheckConstraint(%(sqltext)s%(opts)s)" % { "prefix": _sqlalchemy_autogenerate_prefix(autogen_context), - "opts": ", " + (", ".join("%s=%s" % (k, v) for k, v in opts)) - if opts - else "", + "opts": ( + ", " + (", ".join("%s=%s" % (k, v) for k, v in opts)) + if opts + else "" + ), "sqltext": _render_potential_expr( constraint.sqltext, autogen_context, wrap_in_text=False ), diff --git a/alembic/config.py b/alembic/config.py index 4b2263fd..2c52e7cd 100644 --- a/alembic/config.py +++ b/alembic/config.py @@ -221,8 +221,7 @@ def get_template_directory(self) -> str: @overload def get_section( self, name: str, default: None = ... - ) -> Optional[Dict[str, str]]: - ... + ) -> Optional[Dict[str, str]]: ... # "default" here could also be a TypeVar # _MT = TypeVar("_MT", bound=Mapping[str, str]), @@ -230,14 +229,12 @@ def get_section( @overload def get_section( self, name: str, default: Dict[str, str] - ) -> Dict[str, str]: - ... + ) -> Dict[str, str]: ... @overload def get_section( self, name: str, default: Mapping[str, str] - ) -> Union[Dict[str, str], Mapping[str, str]]: - ... + ) -> Union[Dict[str, str], Mapping[str, str]]: ... def get_section( self, name: str, default: Optional[Mapping[str, str]] = None @@ -313,14 +310,12 @@ def get_section_option( return default @overload - def get_main_option(self, name: str, default: str) -> str: - ... + def get_main_option(self, name: str, default: str) -> str: ... @overload def get_main_option( self, name: str, default: Optional[str] = None - ) -> Optional[str]: - ... + ) -> Optional[str]: ... def get_main_option( self, name: str, default: Optional[str] = None diff --git a/alembic/ddl/_autogen.py b/alembic/ddl/_autogen.py index e22153c4..74715b18 100644 --- a/alembic/ddl/_autogen.py +++ b/alembic/ddl/_autogen.py @@ -287,18 +287,22 @@ def __init__( self.target_table, tuple(self.target_columns), ) + ( - (None if onupdate.lower() == "no action" else onupdate.lower()) - if onupdate - else None, - (None if ondelete.lower() == "no action" else ondelete.lower()) - if ondelete - else None, + ( + (None if onupdate.lower() == "no action" else onupdate.lower()) + if onupdate + else None + ), + ( + (None if ondelete.lower() == "no action" else ondelete.lower()) + if ondelete + else None + ), # convert initially + deferrable into one three-state value - "initially_deferrable" - if initially and initially.lower() == "deferred" - else "deferrable" - if deferrable - else "not deferrable", + ( + "initially_deferrable" + if initially and initially.lower() == "deferred" + else "deferrable" if deferrable else "not deferrable" + ), ) @util.memoized_property diff --git a/alembic/ddl/base.py b/alembic/ddl/base.py index 7a85a5c1..690c1537 100644 --- a/alembic/ddl/base.py +++ b/alembic/ddl/base.py @@ -40,7 +40,6 @@ class AlterTable(DDLElement): - """Represent an ALTER TABLE statement. Only the string name and optional schema name of the table @@ -238,9 +237,11 @@ def visit_column_default( return "%s %s %s" % ( alter_table(compiler, element.table_name, element.schema), alter_column(compiler, element.column_name), - "SET DEFAULT %s" % format_server_default(compiler, element.default) - if element.default is not None - else "DROP DEFAULT", + ( + "SET DEFAULT %s" % format_server_default(compiler, element.default) + if element.default is not None + else "DROP DEFAULT" + ), ) diff --git a/alembic/ddl/impl.py b/alembic/ddl/impl.py index bf202a48..d2983923 100644 --- a/alembic/ddl/impl.py +++ b/alembic/ddl/impl.py @@ -77,7 +77,6 @@ def __init__( class DefaultImpl(metaclass=ImplMeta): - """Provide the entrypoint for major migration operations, including database-specific behavioral variances. @@ -425,13 +424,15 @@ def bulk_insert( self._exec( sqla_compat._insert_inline(table).values( **{ - k: sqla_compat._literal_bindparam( - k, v, type_=table.c[k].type - ) - if not isinstance( - v, sqla_compat._literal_bindparam + k: ( + sqla_compat._literal_bindparam( + k, v, type_=table.c[k].type + ) + if not isinstance( + v, sqla_compat._literal_bindparam + ) + else v ) - else v for k, v in row.items() } ) diff --git a/alembic/ddl/mysql.py b/alembic/ddl/mysql.py index f312173e..3482f672 100644 --- a/alembic/ddl/mysql.py +++ b/alembic/ddl/mysql.py @@ -94,21 +94,29 @@ def alter_column( # type:ignore[override] column_name, schema=schema, newname=name if name is not None else column_name, - nullable=nullable - if nullable is not None - else existing_nullable - if existing_nullable is not None - else True, + nullable=( + nullable + if nullable is not None + else ( + existing_nullable + if existing_nullable is not None + else True + ) + ), type_=type_ if type_ is not None else existing_type, - default=server_default - if server_default is not False - else existing_server_default, - autoincrement=autoincrement - if autoincrement is not None - else existing_autoincrement, - comment=comment - if comment is not False - else existing_comment, + default=( + server_default + if server_default is not False + else existing_server_default + ), + autoincrement=( + autoincrement + if autoincrement is not None + else existing_autoincrement + ), + comment=( + comment if comment is not False else existing_comment + ), ) ) elif ( @@ -123,21 +131,29 @@ def alter_column( # type:ignore[override] column_name, schema=schema, newname=name if name is not None else column_name, - nullable=nullable - if nullable is not None - else existing_nullable - if existing_nullable is not None - else True, + nullable=( + nullable + if nullable is not None + else ( + existing_nullable + if existing_nullable is not None + else True + ) + ), type_=type_ if type_ is not None else existing_type, - default=server_default - if server_default is not False - else existing_server_default, - autoincrement=autoincrement - if autoincrement is not None - else existing_autoincrement, - comment=comment - if comment is not False - else existing_comment, + default=( + server_default + if server_default is not False + else existing_server_default + ), + autoincrement=( + autoincrement + if autoincrement is not None + else existing_autoincrement + ), + comment=( + comment if comment is not False else existing_comment + ), ) ) elif server_default is not False: @@ -368,9 +384,11 @@ def _mysql_alter_default( return "%s ALTER COLUMN %s %s" % ( alter_table(compiler, element.table_name, element.schema), format_column_name(compiler, element.column_name), - "SET DEFAULT %s" % format_server_default(compiler, element.default) - if element.default is not None - else "DROP DEFAULT", + ( + "SET DEFAULT %s" % format_server_default(compiler, element.default) + if element.default is not None + else "DROP DEFAULT" + ), ) diff --git a/alembic/ddl/oracle.py b/alembic/ddl/oracle.py index 54011740..eac99124 100644 --- a/alembic/ddl/oracle.py +++ b/alembic/ddl/oracle.py @@ -141,9 +141,11 @@ def visit_column_default( return "%s %s %s" % ( alter_table(compiler, element.table_name, element.schema), alter_column(compiler, element.column_name), - "DEFAULT %s" % format_server_default(compiler, element.default) - if element.default is not None - else "DEFAULT NULL", + ( + "DEFAULT %s" % format_server_default(compiler, element.default) + if element.default is not None + else "DEFAULT NULL" + ), ) diff --git a/alembic/operations/base.py b/alembic/operations/base.py index bafe441a..bd1b170d 100644 --- a/alembic/operations/base.py +++ b/alembic/operations/base.py @@ -406,8 +406,7 @@ def get_context(self) -> MigrationContext: return self.migration_context @overload - def invoke(self, operation: CreateTableOp) -> Table: - ... + def invoke(self, operation: CreateTableOp) -> Table: ... @overload def invoke( @@ -427,12 +426,10 @@ def invoke( DropTableOp, ExecuteSQLOp, ], - ) -> None: - ... + ) -> None: ... @overload - def invoke(self, operation: MigrateOperation) -> Any: - ... + def invoke(self, operation: MigrateOperation) -> Any: ... def invoke(self, operation: MigrateOperation) -> Any: """Given a :class:`.MigrateOperation`, invoke it in terms of diff --git a/alembic/operations/ops.py b/alembic/operations/ops.py index 7b65191c..0282d571 100644 --- a/alembic/operations/ops.py +++ b/alembic/operations/ops.py @@ -1371,9 +1371,9 @@ def to_table( info=self.info.copy() if self.info else {}, prefixes=list(self.prefixes) if self.prefixes else [], schema=self.schema, - _constraints_included=self._reverse._constraints_included - if self._reverse - else False, + _constraints_included=( + self._reverse._constraints_included if self._reverse else False + ), **self.table_kw, ) return t diff --git a/alembic/operations/schemaobj.py b/alembic/operations/schemaobj.py index 32b26e9b..59c1002f 100644 --- a/alembic/operations/schemaobj.py +++ b/alembic/operations/schemaobj.py @@ -223,10 +223,12 @@ def table(self, name: str, *columns, **kw) -> Table: t = sa_schema.Table(name, m, *cols, **kw) constraints = [ - sqla_compat._copy(elem, target_table=t) - if getattr(elem, "parent", None) is not t - and getattr(elem, "parent", None) is not None - else elem + ( + sqla_compat._copy(elem, target_table=t) + if getattr(elem, "parent", None) is not t + and getattr(elem, "parent", None) is not None + else elem + ) for elem in columns if isinstance(elem, (Constraint, Index)) ] diff --git a/alembic/runtime/environment.py b/alembic/runtime/environment.py index d64b2adc..a30972ec 100644 --- a/alembic/runtime/environment.py +++ b/alembic/runtime/environment.py @@ -108,7 +108,6 @@ class EnvironmentContext(util.ModuleClsProxy): - """A configurational facade made available in an ``env.py`` script. The :class:`.EnvironmentContext` acts as a *facade* to the more @@ -342,18 +341,17 @@ def get_tag_argument(self) -> Optional[str]: return self.context_opts.get("tag", None) # type: ignore[no-any-return] # noqa: E501 @overload - def get_x_argument(self, as_dictionary: Literal[False]) -> List[str]: - ... + def get_x_argument(self, as_dictionary: Literal[False]) -> List[str]: ... @overload - def get_x_argument(self, as_dictionary: Literal[True]) -> Dict[str, str]: - ... + def get_x_argument( + self, as_dictionary: Literal[True] + ) -> Dict[str, str]: ... @overload def get_x_argument( self, as_dictionary: bool = ... - ) -> Union[List[str], Dict[str, str]]: - ... + ) -> Union[List[str], Dict[str, str]]: ... def get_x_argument( self, as_dictionary: bool = False diff --git a/alembic/runtime/migration.py b/alembic/runtime/migration.py index 95c69bc6..6cfe5e23 100644 --- a/alembic/runtime/migration.py +++ b/alembic/runtime/migration.py @@ -86,7 +86,6 @@ def __exit__(self, type_: Any, value: Any, traceback: Any) -> None: class MigrationContext: - """Represent the database state made available to a migration script. @@ -218,9 +217,11 @@ def __init__( log.info("Generating static SQL") log.info( "Will assume %s DDL.", - "transactional" - if self.impl.transactional_ddl - else "non-transactional", + ( + "transactional" + if self.impl.transactional_ddl + else "non-transactional" + ), ) @classmethod @@ -345,9 +346,9 @@ def upgrade(): # except that it will not know it's in "autocommit" and will # emit deprecation warnings when an autocommit action takes # place. - self.connection = ( - self.impl.connection - ) = base_connection.execution_options(isolation_level="AUTOCOMMIT") + self.connection = self.impl.connection = ( + base_connection.execution_options(isolation_level="AUTOCOMMIT") + ) # sqlalchemy future mode will "autobegin" in any case, so take # control of that "transaction" here @@ -1006,8 +1007,7 @@ class MigrationStep: if TYPE_CHECKING: @property - def doc(self) -> Optional[str]: - ... + def doc(self) -> Optional[str]: ... @property def name(self) -> str: diff --git a/alembic/script/base.py b/alembic/script/base.py index 5945ca59..66564781 100644 --- a/alembic/script/base.py +++ b/alembic/script/base.py @@ -56,7 +56,6 @@ class ScriptDirectory: - """Provides operations upon an Alembic script directory. This object is useful to get information as to current revisions, @@ -732,9 +731,11 @@ def generate_revision( if depends_on: with self._catch_revision_errors(): resolved_depends_on = [ - dep - if dep in rev.branch_labels # maintain branch labels - else rev.revision # resolve partial revision identifiers + ( + dep + if dep in rev.branch_labels # maintain branch labels + else rev.revision + ) # resolve partial revision identifiers for rev, dep in [ (not_none(self.revision_map.get_revision(dep)), dep) for dep in util.to_list(depends_on) @@ -808,7 +809,6 @@ def _rev_path( class Script(revision.Revision): - """Represent a single revision file in a ``versions/`` directory. The :class:`.Script` instance is returned by methods @@ -930,9 +930,11 @@ def _head_only( if head_indicators or tree_indicators: text += "%s%s%s" % ( " (head)" if self._is_real_head else "", - " (effective head)" - if self.is_head and not self._is_real_head - else "", + ( + " (effective head)" + if self.is_head and not self._is_real_head + else "" + ), " (current)" if self._db_current_indicator else "", ) if tree_indicators: diff --git a/alembic/script/revision.py b/alembic/script/revision.py index 77a802cd..c3108e98 100644 --- a/alembic/script/revision.py +++ b/alembic/script/revision.py @@ -56,8 +56,7 @@ def __call__( inclusive: bool, implicit_base: bool, assert_relative_length: bool, - ) -> Tuple[Set[Revision], Tuple[Optional[_RevisionOrBase], ...]]: - ... + ) -> Tuple[Set[Revision], Tuple[Optional[_RevisionOrBase], ...]]: ... class RevisionError(Exception): @@ -720,9 +719,11 @@ def _shares_lineage( resolved_target = target resolved_test_against_revs = [ - self._revision_for_ident(test_against_rev) - if not isinstance(test_against_rev, Revision) - else test_against_rev + ( + self._revision_for_ident(test_against_rev) + if not isinstance(test_against_rev, Revision) + else test_against_rev + ) for test_against_rev in util.to_tuple( test_against_revs, default=() ) @@ -1016,9 +1017,9 @@ def get_ancestors(rev_id: str) -> Set[str]: # each time but it was getting complicated current_heads[current_candidate_idx] = heads_to_add[0] current_heads.extend(heads_to_add[1:]) - ancestors_by_idx[ - current_candidate_idx - ] = get_ancestors(heads_to_add[0]) + ancestors_by_idx[current_candidate_idx] = ( + get_ancestors(heads_to_add[0]) + ) ancestors_by_idx.extend( get_ancestors(head) for head in heads_to_add[1:] ) @@ -1183,9 +1184,13 @@ def _parse_downgrade_target( branch_label = symbol # Walk down the tree to find downgrade target. rev = self._walk( - start=self.get_revision(symbol) - if branch_label is None - else self.get_revision("%s@%s" % (branch_label, symbol)), + start=( + self.get_revision(symbol) + if branch_label is None + else self.get_revision( + "%s@%s" % (branch_label, symbol) + ) + ), steps=rel_int, no_overwalk=assert_relative_length, ) @@ -1303,9 +1308,13 @@ def _parse_upgrade_target( ) return ( self._walk( - start=self.get_revision(symbol) - if branch_label is None - else self.get_revision("%s@%s" % (branch_label, symbol)), + start=( + self.get_revision(symbol) + if branch_label is None + else self.get_revision( + "%s@%s" % (branch_label, symbol) + ) + ), steps=relative, no_overwalk=assert_relative_length, ), @@ -1694,15 +1703,13 @@ def is_merge_point(self) -> bool: @overload -def tuple_rev_as_scalar(rev: None) -> None: - ... +def tuple_rev_as_scalar(rev: None) -> None: ... @overload def tuple_rev_as_scalar( rev: Union[Tuple[_T, ...], List[_T]] -) -> Union[_T, Tuple[_T, ...], List[_T]]: - ... +) -> Union[_T, Tuple[_T, ...], List[_T]]: ... def tuple_rev_as_scalar( diff --git a/alembic/testing/fixtures.py b/alembic/testing/fixtures.py index b6cea632..3b5ce596 100644 --- a/alembic/testing/fixtures.py +++ b/alembic/testing/fixtures.py @@ -274,9 +274,11 @@ def _run_alter_col(self, from_, to_, compare=None): "x", column.name, existing_type=column.type, - existing_server_default=column.server_default - if column.server_default is not None - else False, + existing_server_default=( + column.server_default + if column.server_default is not None + else False + ), existing_nullable=True if column.nullable else False, # existing_comment=column.comment, nullable=to_.get("nullable", None), @@ -304,9 +306,13 @@ def _run_alter_col(self, from_, to_, compare=None): new_col["type"], new_col.get("default", None), compare.get("type", old_col["type"]), - compare["server_default"].text - if "server_default" in compare - else column.server_default.arg.text - if column.server_default is not None - else None, + ( + compare["server_default"].text + if "server_default" in compare + else ( + column.server_default.arg.text + if column.server_default is not None + else None + ) + ), ) diff --git a/alembic/testing/suite/test_environment.py b/alembic/testing/suite/test_environment.py index 8c86859a..df2d9afb 100644 --- a/alembic/testing/suite/test_environment.py +++ b/alembic/testing/suite/test_environment.py @@ -24,9 +24,9 @@ def _fixture(self, opts): self.context = MigrationContext.configure( dialect=conn.dialect, opts=opts ) - self.context.output_buffer = ( - self.context.impl.output_buffer - ) = io.StringIO() + self.context.output_buffer = self.context.impl.output_buffer = ( + io.StringIO() + ) else: self.context = MigrationContext.configure( connection=conn, opts=opts diff --git a/alembic/util/langhelpers.py b/alembic/util/langhelpers.py index 4a5bf09a..80d88cbc 100644 --- a/alembic/util/langhelpers.py +++ b/alembic/util/langhelpers.py @@ -234,20 +234,17 @@ def rev_id() -> str: @overload -def to_tuple(x: Any, default: Tuple[Any, ...]) -> Tuple[Any, ...]: - ... +def to_tuple(x: Any, default: Tuple[Any, ...]) -> Tuple[Any, ...]: ... @overload -def to_tuple(x: None, default: Optional[_T] = ...) -> _T: - ... +def to_tuple(x: None, default: Optional[_T] = ...) -> _T: ... @overload def to_tuple( x: Any, default: Optional[Tuple[Any, ...]] = None -) -> Tuple[Any, ...]: - ... +) -> Tuple[Any, ...]: ... def to_tuple( diff --git a/alembic/util/sqla_compat.py b/alembic/util/sqla_compat.py index 8489c19f..30b9b4c4 100644 --- a/alembic/util/sqla_compat.py +++ b/alembic/util/sqla_compat.py @@ -59,8 +59,7 @@ class _CompilerProtocol(Protocol): - def __call__(self, element: Any, compiler: Any, **kw: Any) -> str: - ... + def __call__(self, element: Any, compiler: Any, **kw: Any) -> str: ... def _safe_int(value: str) -> Union[int, str]: @@ -95,8 +94,7 @@ class _Unsupported: def compiles( element: Type[ClauseElement], *dialects: str - ) -> Callable[[_CompilerProtocol], _CompilerProtocol]: - ... + ) -> Callable[[_CompilerProtocol], _CompilerProtocol]: ... else: from sqlalchemy.ext.compiler import compiles diff --git a/reap_dbs.py b/reap_dbs.py index ae7ff858..6b2215df 100644 --- a/reap_dbs.py +++ b/reap_dbs.py @@ -10,6 +10,7 @@ database in process. """ + import logging import sys diff --git a/setup.cfg b/setup.cfg index fa957eca..3c516430 100644 --- a/setup.cfg +++ b/setup.cfg @@ -86,7 +86,7 @@ enable-extensions = G ignore = A003, D, - E203,E305,E711,E712,E721,E722,E741, + E203,E305,E704,E711,E712,E721,E722,E741, N801,N802,N806, RST304,RST303,RST299,RST399, W503,W504 diff --git a/tests/test_autogen_indexes.py b/tests/test_autogen_indexes.py index b06e7c90..d1e95e96 100644 --- a/tests/test_autogen_indexes.py +++ b/tests/test_autogen_indexes.py @@ -642,9 +642,11 @@ def test_unnamed_cols_changed(self): diffs = { ( cmd, - isinstance(obj, (UniqueConstraint, Index)) - if obj.name is not None - else False, + ( + isinstance(obj, (UniqueConstraint, Index)) + if obj.name is not None + else False + ), ) for cmd, obj in diffs } @@ -1800,7 +1802,6 @@ def test_remove_plain_index_is_reported(self): class NoUqReportsIndAsUqTest(NoUqReflectionIndexTest): - """this test suite simulates the condition where: a. the dialect doesn't report unique constraints diff --git a/tests/test_autogen_render.py b/tests/test_autogen_render.py index eeeb92ed..254b6ddd 100644 --- a/tests/test_autogen_render.py +++ b/tests/test_autogen_render.py @@ -53,7 +53,6 @@ class AutogenRenderTest(TestBase): - """test individual directives""" def setUp(self): diff --git a/tests/test_batch.py b/tests/test_batch.py index 9992af2c..2806dde1 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -329,19 +329,21 @@ def _assert_impl( ) args["tname_colnames"] = ", ".join( - "CAST(%(schema)stname.%(name)s AS %(type)s) AS %(cast_label)s" - % { - "schema": args["schema"], - "name": name, - "type": impl.new_table.c[name].type, - "cast_label": name if sqla_14 else "anon_1", - } - if ( - impl.new_table.c[name].type._type_affinity - is not impl.table.c[name].type._type_affinity + ( + "CAST(%(schema)stname.%(name)s AS %(type)s) AS %(cast_label)s" + % { + "schema": args["schema"], + "name": name, + "type": impl.new_table.c[name].type, + "cast_label": name if sqla_14 else "anon_1", + } + if ( + impl.new_table.c[name].type._type_affinity + is not impl.table.c[name].type._type_affinity + ) + else "%(schema)stname.%(name)s" + % {"schema": args["schema"], "name": name} ) - else "%(schema)stname.%(name)s" - % {"schema": args["schema"], "name": name} for name in colnames if name in impl.table.c ) diff --git a/tests/test_mssql.py b/tests/test_mssql.py index 693ab57d..fccde264 100644 --- a/tests/test_mssql.py +++ b/tests/test_mssql.py @@ -1,4 +1,5 @@ """Test op functions against MSSQL.""" + from __future__ import annotations from typing import Any @@ -118,9 +119,9 @@ def test_alter_column_type_and_nullability( expected_nullability = not existing_nullability args["nullable"] = expected_nullability else: - args[ - "existing_nullable" - ] = expected_nullability = existing_nullability + args["existing_nullable"] = expected_nullability = ( + existing_nullability + ) op.alter_column("t", "c", **args) diff --git a/tests/test_version_traversal.py b/tests/test_version_traversal.py index 09816dff..2fd07a95 100644 --- a/tests/test_version_traversal.py +++ b/tests/test_version_traversal.py @@ -554,7 +554,6 @@ def test_downgrade_no_effect_branched(self): class BranchFromMergepointTest(MigrationTest): - """this is a form that will come up frequently in the "many independent roots with cross-dependencies" case. @@ -617,7 +616,6 @@ def test_mergepoint_to_only_one_side_downgrade(self): class BranchFrom3WayMergepointTest(MigrationTest): - """this is a form that will come up frequently in the "many independent roots with cross-dependencies" case. diff --git a/tox.ini b/tox.ini index 1c1d9428..a1265d81 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ deps=pytest>4.6 backports.zoneinfo;python_version<"3.9" tzdata zimports - black==23.3.0 + black==24.1.1 greenlet>=1 @@ -97,7 +97,7 @@ deps= pydocstyle<4.0.0 # used by flake8-rst-docstrings pygments - black==23.3.0 + black==24.1.1 commands = flake8 ./alembic/ ./tests/ setup.py docs/build/conf.py {posargs} black --check setup.py tests alembic From 88769c545439336bec7379dc77f7e6f70d83d1ea Mon Sep 17 00:00:00 2001 From: kasium <15907922+kasium@users.noreply.github.com> Date: Thu, 22 Feb 2024 20:31:30 +0100 Subject: [PATCH 04/45] Fix type annotations in create_foreign_key (#1430) The constraint name parameter of create_foreign_key should be optional, but the batch function defined it as str instead of Optional[str]. Closes #1429 --- alembic/operations/base.py | 2 +- alembic/operations/ops.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/alembic/operations/base.py b/alembic/operations/base.py index bd1b170d..649e7f2b 100644 --- a/alembic/operations/base.py +++ b/alembic/operations/base.py @@ -1721,7 +1721,7 @@ def create_exclude_constraint( def create_foreign_key( self, - constraint_name: str, + constraint_name: Optional[str], referent_table: str, local_cols: List[str], remote_cols: List[str], diff --git a/alembic/operations/ops.py b/alembic/operations/ops.py index 0282d571..3a9c033c 100644 --- a/alembic/operations/ops.py +++ b/alembic/operations/ops.py @@ -681,7 +681,7 @@ def create_foreign_key( def batch_create_foreign_key( cls, operations: BatchOperations, - constraint_name: str, + constraint_name: Optional[str], referent_table: str, local_cols: List[str], remote_cols: List[str], From e0e8de1758b8f34bbb9f51019144dbd48efa9f13 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Wed, 14 Feb 2024 20:31:16 +0100 Subject: [PATCH 05/45] Improve commands doc strings Fixes: #1420 Change-Id: Id8626bd5bb87920abf83e5511130dbbfa8916465 --- alembic/command.py | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/alembic/command.py b/alembic/command.py index 37aa6e67..89c12354 100644 --- a/alembic/command.py +++ b/alembic/command.py @@ -49,7 +49,7 @@ def init( :param config: a :class:`.Config` object. - :param directory: string path of the target directory + :param directory: string path of the target directory. :param template: string name of the migration environment template to use. @@ -174,7 +174,7 @@ def revision( will be applied to the structure generated by the revision process where it can be altered programmatically. Note that unlike all the other parameters, this option is only available via programmatic - use of :func:`.command.revision` + use of :func:`.command.revision`. """ @@ -315,9 +315,11 @@ def merge( :param config: a :class:`.Config` instance - :param message: string message to apply to the revision + :param revisions: The revisions to merge. - :param branch_label: string label name to apply to the new revision + :param message: string message to apply to the revision. + + :param branch_label: string label name to apply to the new revision. :param rev_id: hardcoded revision identifier instead of generating a new one. @@ -370,9 +372,10 @@ def upgrade( :param config: a :class:`.Config` instance. - :param revision: string revision target or range for --sql mode + :param revision: string revision target or range for --sql mode. May be + ``"heads"`` to target the most recent revision(s). - :param sql: if True, use ``--sql`` mode + :param sql: if True, use ``--sql`` mode. :param tag: an arbitrary "tag" that can be intercepted by custom ``env.py`` scripts via the :meth:`.EnvironmentContext.get_tag_argument` @@ -413,9 +416,10 @@ def downgrade( :param config: a :class:`.Config` instance. - :param revision: string revision target or range for --sql mode + :param revision: string revision target or range for --sql mode. May + be ``"base"`` to target the first revision. - :param sql: if True, use ``--sql`` mode + :param sql: if True, use ``--sql`` mode. :param tag: an arbitrary "tag" that can be intercepted by custom ``env.py`` scripts via the :meth:`.EnvironmentContext.get_tag_argument` @@ -449,12 +453,13 @@ def downgrade(rev, context): script.run_env() -def show(config, rev): +def show(config: Config, rev: str) -> None: """Show the revision(s) denoted by the given symbol. :param config: a :class:`.Config` instance. - :param revision: string revision target + :param rev: string revision target. May be ``"current"`` to show the + revision(s) currently applied in the database. """ @@ -484,7 +489,7 @@ def history( :param config: a :class:`.Config` instance. - :param rev_range: string revision range + :param rev_range: string revision range. :param verbose: output in verbose mode. @@ -543,7 +548,9 @@ def _display_current_history(rev, context): _display_history(config, script, base, head) -def heads(config, verbose=False, resolve_dependencies=False): +def heads( + config: Config, verbose: bool = False, resolve_dependencies: bool = False +) -> None: """Show current available heads in the script directory. :param config: a :class:`.Config` instance. @@ -568,7 +575,7 @@ def heads(config, verbose=False, resolve_dependencies=False): ) -def branches(config, verbose=False): +def branches(config: Config, verbose: bool = False) -> None: """Show current branch points. :param config: a :class:`.Config` instance. @@ -638,7 +645,9 @@ def stamp( :param config: a :class:`.Config` instance. :param revision: target revision or list of revisions. May be a list - to indicate stamping of multiple branch heads. + to indicate stamping of multiple branch heads; may be ``"base"`` + to remove all revisions from the table or ``"heads"`` to stamp the + most recent revision(s). .. note:: this parameter is called "revisions" in the command line interface. @@ -728,7 +737,7 @@ def ensure_version(config: Config, sql: bool = False) -> None: :param config: a :class:`.Config` instance. - :param sql: use ``--sql`` mode + :param sql: use ``--sql`` mode. .. versionadded:: 1.7.6 From 287ee90d6a4100ef5e2761290f6c066dc047e613 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 3 Mar 2024 22:23:40 -0500 Subject: [PATCH 06/45] add rel_2_0 target Change-Id: I19317c5f67c0e7269352f938d7f4351dd1c8115c --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index a1265d81..3b4c1ff7 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ deps=pytest>4.6 sqla13: pytest<7 sqla13: {[tox]SQLA_REPO}@rel_1_3#egg=sqlalchemy sqla14: {[tox]SQLA_REPO}@rel_1_4#egg=sqlalchemy + sqla20: {[tox]SQLA_REPO}@rel_2_0#egg=sqlalchemy sqlamain: {[tox]SQLA_REPO}#egg=sqlalchemy postgresql: psycopg2>=2.7 mysql: mysqlclient>=1.4.0 From d90922d4afbc751b629c3cdf614d273eb143ced4 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 3 Mar 2024 22:40:53 -0500 Subject: [PATCH 07/45] block pytest 8 something has changed and teardown() is no longer called. SQLAlchemy seems to also be pinned below pytest 8 (which we need to fix) so this is likely related Change-Id: I784b3abde67528c30af06a01b3a02d481a29276f --- tests/test_command.py | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_command.py b/tests/test_command.py index c665f955..2dfe6697 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -65,6 +65,7 @@ def teardown_class(cls): clear_staging_env() def teardown(self): + breakpoint() self.cfg.set_main_option("revision_environment", "false") @classmethod diff --git a/tox.ini b/tox.ini index 3b4c1ff7..76de26a0 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ SQLA_REPO = {env:SQLA_REPO:git+https://github.com/sqlalchemy/sqlalchemy.git} [testenv] cov_args=--cov=alembic --cov-report term --cov-report xml -deps=pytest>4.6 +deps=pytest>4.6,8 pytest-xdist sqla13: pytest<7 sqla13: {[tox]SQLA_REPO}@rel_1_3#egg=sqlalchemy From c14bcd6f6514b1f3174098dfee9f8234dba1aea7 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 3 Mar 2024 23:17:45 -0500 Subject: [PATCH 08/45] Revert "block pytest 8" This reverts commit d90922d4afbc751b629c3cdf614d273eb143ced4. an errant breakpoint() got in there. --- tests/test_command.py | 1 - tox.ini | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_command.py b/tests/test_command.py index 2dfe6697..c665f955 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -65,7 +65,6 @@ def teardown_class(cls): clear_staging_env() def teardown(self): - breakpoint() self.cfg.set_main_option("revision_environment", "false") @classmethod diff --git a/tox.ini b/tox.ini index 76de26a0..3b4c1ff7 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ SQLA_REPO = {env:SQLA_REPO:git+https://github.com/sqlalchemy/sqlalchemy.git} [testenv] cov_args=--cov=alembic --cov-report term --cov-report xml -deps=pytest>4.6,8 +deps=pytest>4.6 pytest-xdist sqla13: pytest<7 sqla13: {[tox]SQLA_REPO}@rel_1_3#egg=sqlalchemy From 6bdb9043868d4bd04ebe3fe8a4991735d5f87ed3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 3 Mar 2024 23:11:50 -0500 Subject: [PATCH 09/45] use SQLAlchemy's xdist methods Fixes to support pytest 8.1 for the test suite. the use of teardown() was based on pytest's nose compat, which is removed. their xdist style tests use the name "setup_method()" and "teardown_method()" now. We have SQLAlchemy's pytestplugin in use which uses pytest fixtures to invoke our own xdist style setUp and tearDown methods, which we are already using here, so use those for this one test. Fixes: #1435 Change-Id: I4c49e81fca6bfa957594714009531fe12691ace5 --- docs/build/unreleased/1435.rst | 5 +++++ tests/test_command.py | 15 +++++++-------- tox.ini | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 docs/build/unreleased/1435.rst diff --git a/docs/build/unreleased/1435.rst b/docs/build/unreleased/1435.rst new file mode 100644 index 00000000..8f9e4cd0 --- /dev/null +++ b/docs/build/unreleased/1435.rst @@ -0,0 +1,5 @@ +.. change:: + :tags: bug, tests + :tickets: 1435 + + Fixes to support pytest 8.1 for the test suite. diff --git a/tests/test_command.py b/tests/test_command.py index c665f955..04a624ad 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -64,7 +64,7 @@ def setup_class(cls): def teardown_class(cls): clear_staging_env() - def teardown(self): + def tearDown(self): self.cfg.set_main_option("revision_environment", "false") @classmethod @@ -206,13 +206,12 @@ def test_history_indicate_current(self): class RevisionEnvironmentTest(_BufMixin, TestBase): - @classmethod - def setup(cls): - cls.env = staging_env() - cls.cfg = _sqlite_testing_config() - cls._setup_env_file() + def setUp(self): + self.env = staging_env() + self.cfg = _sqlite_testing_config() + self._setup_env_file() - def teardown(self): + def tearDown(self): self.cfg.set_main_option("revision_environment", "false") clear_staging_env() @@ -1144,7 +1143,7 @@ def setup_class(cls): cls.cfg = _sqlite_testing_config() cls.a, cls.b, cls.c = three_rev_fixture(cls.cfg) - def teardown(self): + def tearDown(self): os.environ.pop("ALEMBIC_CONFIG", None) @classmethod diff --git a/tox.ini b/tox.ini index 3b4c1ff7..4b0f082b 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ SQLA_REPO = {env:SQLA_REPO:git+https://github.com/sqlalchemy/sqlalchemy.git} [testenv] cov_args=--cov=alembic --cov-report term --cov-report xml -deps=pytest>4.6 +deps=pytest>4.6,<8.2 pytest-xdist sqla13: pytest<7 sqla13: {[tox]SQLA_REPO}@rel_1_3#egg=sqlalchemy From b6e7e2f27ebc599c47d29eed925c7e7e1481da20 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Mon, 18 Mar 2024 22:29:01 +0100 Subject: [PATCH 10/45] Clarify how script_location can be os agnostic Fixes: #1431 Change-Id: Iafe70621911614d197e5e5ecf74afecd6f4df10e --- alembic/templates/async/alembic.ini.mako | 6 +++--- alembic/templates/generic/alembic.ini.mako | 4 ++-- alembic/templates/multidb/alembic.ini.mako | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/alembic/templates/async/alembic.ini.mako b/alembic/templates/async/alembic.ini.mako index 0e5f43fd..3558295e 100644 --- a/alembic/templates/async/alembic.ini.mako +++ b/alembic/templates/async/alembic.ini.mako @@ -1,7 +1,8 @@ # A generic, single database configuration. [alembic] -# path to migration scripts +# path to migration scripts. +# Use forward slashes (/) also on windows to provide an os agnostic path script_location = ${script_location} # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s @@ -20,8 +21,7 @@ prepend_sys_path = . # leave blank for localtime # timezone = -# max length of characters to apply to the -# "slug" field +# max length of characters to apply to the "slug" field # truncate_slug_length = 40 # set to 'true' to run the environment during diff --git a/alembic/templates/generic/alembic.ini.mako b/alembic/templates/generic/alembic.ini.mako index 29245dd3..b3fe0535 100644 --- a/alembic/templates/generic/alembic.ini.mako +++ b/alembic/templates/generic/alembic.ini.mako @@ -2,6 +2,7 @@ [alembic] # path to migration scripts +# Use forward slashes (/) also on windows to provide an os agnostic path script_location = ${script_location} # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s @@ -22,8 +23,7 @@ prepend_sys_path = . # leave blank for localtime # timezone = -# max length of characters to apply to the -# "slug" field +# max length of characters to apply to the "slug" field # truncate_slug_length = 40 # set to 'true' to run the environment during diff --git a/alembic/templates/multidb/alembic.ini.mako b/alembic/templates/multidb/alembic.ini.mako index c7fbe482..6e901999 100644 --- a/alembic/templates/multidb/alembic.ini.mako +++ b/alembic/templates/multidb/alembic.ini.mako @@ -2,6 +2,7 @@ [alembic] # path to migration scripts +# Use forward slashes (/) also on windows to provide an os agnostic path script_location = ${script_location} # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s @@ -22,8 +23,7 @@ prepend_sys_path = . # leave blank for localtime # timezone = -# max length of characters to apply to the -# "slug" field +# max length of characters to apply to the "slug" field # truncate_slug_length = 40 # set to 'true' to run the environment during From 0bc8c6343a0007c601e790665dcc767d5866c841 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 19 Mar 2024 08:14:53 -0400 Subject: [PATCH 11/45] add additional seealsos for schema name References: #1447 Change-Id: I1b045cd811f793f1ef19da46ee4cfd3bd737dc88 --- docs/build/autogenerate.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/build/autogenerate.rst b/docs/build/autogenerate.rst index a6397177..519aa5bc 100644 --- a/docs/build/autogenerate.rst +++ b/docs/build/autogenerate.rst @@ -246,6 +246,14 @@ The list of objects that are scanned in the target database connection include: :meth:`~sqlalchemy.engine.reflection.Inspector.get_foreign_keys` (as of this writing, CHECK constraints and primary key constraints are not yet included). +.. seealso:: + + :ref:`sqla:schema_table_schema_name` - in depth introduction to how + SQLAlchemy interprets schema names + + :ref:`sqla:postgresql_schema_reflection` - important notes specific to the + PostgreSQL database + Omitting Schema Names from the Autogenerate Process ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 0171748e247978779c2eb5ab0907f3af0869456e Mon Sep 17 00:00:00 2001 From: kasium <15907922+kasium@users.noreply.github.com> Date: Thu, 4 Apr 2024 14:11:58 -0400 Subject: [PATCH 12/45] Fix constraint_name type in create_primary_key The constraint name in create_primary_key should be optional, but for batch operations is is required according to the type annotations ### Description Changed the type annotation to `Optional[str]` ### Checklist This pull request is: - [ ] A documentation / typographical error fix - Good to go, no issue or tests are needed - [X] A short code fix - please include the issue number, and create an issue if none exists, which must include a complete example of the issue. one line code fixes without an issue and demonstration will not be accepted. - Please include: `Fixes: #` in the commit message - please include tests. one line code fixes without tests will not be accepted. - [ ] A new feature implementation - please include the issue number, and create an issue if none exists, which must include a complete example of how the feature would look. - Please include: `Fixes: #` in the commit message - please include tests. **Have a nice day!** Closes: #1452 Pull-request: https://github.com/sqlalchemy/alembic/pull/1452 Pull-request-sha: 8afb2bf3fbddc1b04a9da7c5cc5553dea2c1c593 Change-Id: Ic7bbbbfda85dafccdf44c73a6233140aa7e96a2d --- alembic/operations/base.py | 2 +- alembic/operations/ops.py | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/alembic/operations/base.py b/alembic/operations/base.py index 649e7f2b..27dd3b9e 100644 --- a/alembic/operations/base.py +++ b/alembic/operations/base.py @@ -1771,7 +1771,7 @@ def create_index( ... def create_primary_key( - self, constraint_name: str, columns: List[str] + self, constraint_name: Optional[str], columns: List[str] ) -> None: """Issue a "create primary key" instruction using the current batch migration context. diff --git a/alembic/operations/ops.py b/alembic/operations/ops.py index 3a9c033c..e6f1fb64 100644 --- a/alembic/operations/ops.py +++ b/alembic/operations/ops.py @@ -349,7 +349,7 @@ def create_primary_key( def batch_create_primary_key( cls, operations: BatchOperations, - constraint_name: str, + constraint_name: Optional[str], columns: List[str], ) -> None: """Issue a "create primary key" instruction using the diff --git a/setup.cfg b/setup.cfg index 3c516430..70daeadd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -84,7 +84,7 @@ where = tests enable-extensions = G # E203 is due to https://github.com/PyCQA/pycodestyle/issues/373 ignore = - A003, + A003,A005 D, E203,E305,E704,E711,E712,E721,E722,E741, N801,N802,N806, From 44965f05e91ee5d424d9dde6566650c1bf26b516 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 17 Apr 2024 13:21:16 -0400 Subject: [PATCH 13/45] dont duplicate ModelOne; block A005 this is already in the fixtures. block new flake8 A005 warning nobody asked for Change-Id: Ic4f3ec3d1eee5333edb3f48ac95b09ad1b8fdbdf --- setup.cfg | 2 +- tests/test_autogen_diffs.py | 99 +------------------------------------ 2 files changed, 2 insertions(+), 99 deletions(-) diff --git a/setup.cfg b/setup.cfg index 3c516430..70daeadd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -84,7 +84,7 @@ where = tests enable-extensions = G # E203 is due to https://github.com/PyCQA/pycodestyle/issues/373 ignore = - A003, + A003,A005 D, E203,E305,E704,E711,E712,E721,E722,E741, N801,N802,N806, diff --git a/tests/test_autogen_diffs.py b/tests/test_autogen_diffs.py index b0490f9e..174cfac5 100644 --- a/tests/test_autogen_diffs.py +++ b/tests/test_autogen_diffs.py @@ -55,6 +55,7 @@ from alembic.testing.suite._autogen_fixtures import _default_object_filters from alembic.testing.suite._autogen_fixtures import AutogenFixtureTest from alembic.testing.suite._autogen_fixtures import AutogenTest +from alembic.testing.suite._autogen_fixtures import ModelOne from alembic.util import CommandError # TODO: we should make an adaptation of CompareMetadataToInspectorTest that is @@ -315,104 +316,6 @@ def _include_object(obj, name, type_, reflected, compare_to): eq_(len(diffs), 0) -class ModelOne: - __requires__ = ("unique_constraint_reflection",) - - schema = None - - @classmethod - def _get_db_schema(cls): - schema = cls.schema - - m = MetaData(schema=schema) - - Table( - "user", - m, - Column("id", Integer, primary_key=True), - Column("name", String(50)), - Column("a1", Text), - Column("pw", String(50)), - Index("pw_idx", "pw"), - ) - - Table( - "address", - m, - Column("id", Integer, primary_key=True), - Column("email_address", String(100), nullable=False), - ) - - Table( - "order", - m, - Column("order_id", Integer, primary_key=True), - Column( - "amount", - Numeric(8, 2), - nullable=False, - server_default=text("0"), - ), - CheckConstraint("amount >= 0", name="ck_order_amount"), - ) - - Table( - "extra", - m, - Column("x", CHAR), - Column("uid", Integer, ForeignKey("user.id")), - ) - - return m - - @classmethod - def _get_model_schema(cls): - schema = cls.schema - - m = MetaData(schema=schema) - - Table( - "user", - m, - Column("id", Integer, primary_key=True), - Column("name", String(50), nullable=False), - Column("a1", Text, server_default="x"), - ) - - Table( - "address", - m, - Column("id", Integer, primary_key=True), - Column("email_address", String(100), nullable=False), - Column("street", String(50)), - UniqueConstraint("email_address", name="uq_email"), - ) - - Table( - "order", - m, - Column("order_id", Integer, primary_key=True), - Column( - "amount", - Numeric(10, 2), - nullable=True, - server_default=text("0"), - ), - Column("user_id", Integer, ForeignKey("user.id")), - CheckConstraint("amount > -1", name="ck_order_amount"), - ) - - Table( - "item", - m, - Column("id", Integer, primary_key=True), - Column("description", String(100)), - Column("order_id", Integer, ForeignKey("order.order_id")), - CheckConstraint("len(description) > 5"), - ) - return m - - class AutogenerateDiffTest(ModelOne, AutogenTest, TestBase): __only_on__ = "sqlite" From 7a0af05fe3f663f2dbf6f2de168af1d1d42c84e1 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Wed, 24 Apr 2024 21:29:43 +0200 Subject: [PATCH 14/45] fix typo in docs Fixes: #1463 Change-Id: Ic7aa3ba1b3fd40e3563f5c419ed3cf6cbe0d985d --- docs/build/front.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build/front.rst b/docs/build/front.rst index 4f1bbe2e..f583a544 100644 --- a/docs/build/front.rst +++ b/docs/build/front.rst @@ -50,7 +50,7 @@ The install will add the ``alembic`` command to the virtual environment. All operations with Alembic in terms of this specific virtual environment will then proceed through the usage of this command, as in:: - $ /path/to/your/project/.venv/bin/alembic init . + $ /path/to/your/project/.venv/bin/alembic init alembic The next step is **optional**. If our project itself has a ``setup.py`` file, we can also install it in the local virtual environment in From ade17cfd8dd1a12d6babceb24126511ddab1085a Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Wed, 24 Apr 2024 22:13:10 +0200 Subject: [PATCH 15/45] Add missing space in error message Fixes: #1464 Change-Id: I9dd5a6c48c685d347ffa35f12afb79845c347003 --- alembic/script/base.py | 2 +- tests/test_script_production.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/alembic/script/base.py b/alembic/script/base.py index 66564781..9a955b6b 100644 --- a/alembic/script/base.py +++ b/alembic/script/base.py @@ -609,7 +609,7 @@ def _generate_create_date(self) -> datetime.datetime: if self.timezone is not None: if ZoneInfo is None: raise util.CommandError( - "Python >= 3.9 is required for timezone support or" + "Python >= 3.9 is required for timezone support or " "the 'backports.zoneinfo' package must be installed." ) # First, assume correct capitalization diff --git a/tests/test_script_production.py b/tests/test_script_production.py index 7b7db814..a6618be2 100644 --- a/tests/test_script_production.py +++ b/tests/test_script_production.py @@ -297,7 +297,7 @@ def test_no_zoneinfo_module(self): with patch("alembic.script.base.ZoneInfo", new=None): with expect_raises_message( CommandError, - "Python >= 3.9 is required for timezone support or" + "Python >= 3.9 is required for timezone support or " "the 'backports.zoneinfo' package must be installed.", ): self._test_tz( From 34dbe6afa27db5288629e1ec6fe5fbcd675a3b2f Mon Sep 17 00:00:00 2001 From: zhouyizhen Date: Wed, 5 Jun 2024 15:28:53 -0400 Subject: [PATCH 16/45] Fix postgres detect serial in autogenerate (#1479) Fixes: https://github.com/sqlalchemy/alembic/issues/1479 ### Description In https://github.com/sqlalchemy/alembic/issues/73, it tries to detact postgresql serial in autogenerate, so it won't take `nextval('seq'::regclass)` as server default for that column. But it takes not effect for tables not in search path. This PR fixed it. ### Checklist This pull request is: - [ ] A documentation / typographical error fix - Good to go, no issue or tests are needed - [x] A short code fix - please include the issue number, and create an issue if none exists, which must include a complete example of the issue. one line code fixes without an issue and demonstration will not be accepted. - Please include: `Fixes: #` in the commit message - please include tests. one line code fixes without tests will not be accepted. - [ ] A new feature implementation - please include the issue number, and create an issue if none exists, which must include a complete example of how the feature would look. - Please include: `Fixes: #` in the commit message - please include tests. **Have a nice day!** Closes: #1486 Pull-request: https://github.com/sqlalchemy/alembic/pull/1486 Pull-request-sha: 24df8f906d281df92c531df5a9e1f64d8cdb8527 Change-Id: I50276875bfb1d4f920f0fcd20136337ae09b5384 --- alembic/ddl/postgresql.py | 3 ++- docs/build/unreleased/1479.rst | 6 +++++ tests/test_postgresql.py | 44 +++++++++++++++++++++------------- 3 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 docs/build/unreleased/1479.rst diff --git a/alembic/ddl/postgresql.py b/alembic/ddl/postgresql.py index 6507fcbd..de64a4e0 100644 --- a/alembic/ddl/postgresql.py +++ b/alembic/ddl/postgresql.py @@ -218,7 +218,8 @@ def autogen_column_reflect(self, inspector, table, column_info): "join pg_class t on t.oid=d.refobjid " "join pg_attribute a on a.attrelid=t.oid and " "a.attnum=d.refobjsubid " - "where c.relkind='S' and c.relname=:seqname" + "where c.relkind='S' and " + "c.oid=cast(:seqname as regclass)" ), seqname=seq_match.group(1), ).first() diff --git a/docs/build/unreleased/1479.rst b/docs/build/unreleased/1479.rst new file mode 100644 index 00000000..5a653201 --- /dev/null +++ b/docs/build/unreleased/1479.rst @@ -0,0 +1,6 @@ +.. change:: + :tags: bug, autogenerate, postgresql + :tickets: 1479 + + Fixed the detection of serial column in autogenerate with tables + not under default schema on PostgreSQL diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py index 5741b080..e42ea9d3 100644 --- a/tests/test_postgresql.py +++ b/tests/test_postgresql.py @@ -859,8 +859,8 @@ def teardown_class(cls): clear_staging_env() @provide_metadata - def _expect_default(self, c_expected, col, seq=None): - Table("t", self.metadata, col) + def _expect_default(self, c_expected, col, schema=None, seq=None): + Table("t", self.metadata, col, schema=schema) self.autogen_context.metadata = self.metadata @@ -871,7 +871,7 @@ def _expect_default(self, c_expected, col, seq=None): insp = inspect(config.db) uo = ops.UpgradeOps(ops=[]) - _compare_tables({(None, "t")}, set(), insp, uo, self.autogen_context) + _compare_tables({(schema, "t")}, set(), insp, uo, self.autogen_context) diffs = uo.as_diffs() tab = diffs[0][1] @@ -884,12 +884,12 @@ def _expect_default(self, c_expected, col, seq=None): insp = inspect(config.db) uo = ops.UpgradeOps(ops=[]) - m2 = MetaData() + m2 = MetaData(schema=schema) Table("t", m2, Column("x", BigInteger())) self.autogen_context.metadata = m2 _compare_tables( - {(None, "t")}, - {(None, "t")}, + {(schema, "t")}, + {(schema, "t")}, insp, uo, self.autogen_context, @@ -903,35 +903,47 @@ def _expect_default(self, c_expected, col, seq=None): c_expected, ) - def test_serial(self): - self._expect_default(None, Column("x", Integer, primary_key=True)) + @testing.combinations((None,), ("test_schema",)) + def test_serial(self, schema): + self._expect_default( + None, Column("x", Integer, primary_key=True), schema + ) - def test_separate_seq(self): - seq = Sequence("x_id_seq") + @testing.combinations((None,), ("test_schema",)) + def test_separate_seq(self, schema): + seq = Sequence("x_id_seq", schema=schema) + seq_name = seq.name if schema is None else f"{schema}.{seq.name}" self._expect_default( - "nextval('x_id_seq'::regclass)", + f"nextval('{seq_name}'::regclass)", Column( "x", Integer, server_default=seq.next_value(), primary_key=True ), + schema, seq, ) - def test_numeric(self): - seq = Sequence("x_id_seq") + @testing.combinations((None,), ("test_schema",)) + def test_numeric(self, schema): + seq = Sequence("x_id_seq", schema=schema) + seq_name = seq.name if schema is None else f"{schema}.{seq.name}" self._expect_default( - "nextval('x_id_seq'::regclass)", + f"nextval('{seq_name}'::regclass)", Column( "x", Numeric(8, 2), server_default=seq.next_value(), primary_key=True, ), + schema, seq, ) - def test_no_default(self): + @testing.combinations((None,), ("test_schema",)) + def test_no_default(self, schema): self._expect_default( - None, Column("x", Integer, autoincrement=False, primary_key=True) + None, + Column("x", Integer, autoincrement=False, primary_key=True), + schema, ) From b8e2c3589455d46eaef7b34773e50f04a0725fe8 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 19 Jun 2024 11:48:25 -0400 Subject: [PATCH 17/45] pin setuptools below 69.3 and prepare for "build" for releases Change-Id: Ib70446cc3c7d7d8acb264ffa2237a0c7aac5a0f5 --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b9b1f44a..628e6761 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,9 @@ [build-system] build-backend = "setuptools.build_meta" requires = [ - "setuptools>=47", + # avoid moving to https://github.com/pypa/setuptools/issues/3593 + # until we're ready + "setuptools>=61.0,<69.3", ] [tool.black] From c57a5b7b4d88296bbfc73c1dd770a9122bc1002e Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 26 Jun 2024 11:37:04 -0400 Subject: [PATCH 18/45] - 1.13.2 --- docs/build/changelog.rst | 38 +++++++++++++++++++++++++++++++++- docs/build/conf.py | 4 ++-- docs/build/unreleased/1384.rst | 6 ------ docs/build/unreleased/1391.rst | 6 ------ docs/build/unreleased/1394.rst | 8 ------- docs/build/unreleased/1435.rst | 5 ----- docs/build/unreleased/1479.rst | 6 ------ 7 files changed, 39 insertions(+), 34 deletions(-) delete mode 100644 docs/build/unreleased/1384.rst delete mode 100644 docs/build/unreleased/1391.rst delete mode 100644 docs/build/unreleased/1394.rst delete mode 100644 docs/build/unreleased/1435.rst delete mode 100644 docs/build/unreleased/1479.rst diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 93e37479..b240948f 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -5,7 +5,43 @@ Changelog .. changelog:: :version: 1.13.2 - :include_notes_from: unreleased + :released: June 26, 2024 + + .. change:: + :tags: bug, commands + :tickets: 1384 + + Fixed bug in alembic command stdout where long messages were not properly + wrapping at the terminal width. Pull request courtesy Saif Hakim. + + .. change:: + :tags: usecase, autogenerate + :tickets: 1391 + + Improve computed column compare function to support multi-line expressions. + Pull request courtesy of Georg Wicke-Arndt. + + .. change:: + :tags: bug, execution + :tickets: 1394 + + Fixed internal issue where Alembic would call ``connection.execute()`` + sending an empty tuple to indicate "no params". In SQLAlchemy 2.1 this + case will be deprecated as "empty sequence" is ambiguous as to its intent. + + + .. change:: + :tags: bug, tests + :tickets: 1435 + + Fixes to support pytest 8.1 for the test suite. + + .. change:: + :tags: bug, autogenerate, postgresql + :tickets: 1479 + + Fixed the detection of serial column in autogenerate with tables + not under default schema on PostgreSQL .. changelog:: :version: 1.13.1 diff --git a/docs/build/conf.py b/docs/build/conf.py index 80a2082f..90a334e6 100644 --- a/docs/build/conf.py +++ b/docs/build/conf.py @@ -99,8 +99,8 @@ # The short X.Y version. version = alembic.__version__ # The full version, including alpha/beta/rc tags. -release = "1.13.1" -release_date = "December 20, 2023" +release = "1.13.2" +release_date = "June 26, 2024" # The language for content autogenerated by Sphinx. Refer to documentation diff --git a/docs/build/unreleased/1384.rst b/docs/build/unreleased/1384.rst deleted file mode 100644 index 91e6ea23..00000000 --- a/docs/build/unreleased/1384.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. change:: - :tags: bug, commands - :tickets: 1384 - - Fixed bug in alembic command stdout where long messages were not properly - wrapping at the terminal width. Pull request courtesy Saif Hakim. diff --git a/docs/build/unreleased/1391.rst b/docs/build/unreleased/1391.rst deleted file mode 100644 index c0661fbe..00000000 --- a/docs/build/unreleased/1391.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. change:: - :tags: usecase, autogenerate - :tickets: 1391 - - Improve computed column compare function to support multi-line expressions. - Pull request courtesy of Georg Wicke-Arndt. diff --git a/docs/build/unreleased/1394.rst b/docs/build/unreleased/1394.rst deleted file mode 100644 index 7e597709..00000000 --- a/docs/build/unreleased/1394.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. change:: - :tags: bug, execution - :tickets: 1394 - - Fixed internal issue where Alembic would call ``connection.execute()`` - sending an empty tuple to indicate "no params". In SQLAlchemy 2.1 this - case will be deprecated as "empty sequence" is ambiguous as to its intent. - diff --git a/docs/build/unreleased/1435.rst b/docs/build/unreleased/1435.rst deleted file mode 100644 index 8f9e4cd0..00000000 --- a/docs/build/unreleased/1435.rst +++ /dev/null @@ -1,5 +0,0 @@ -.. change:: - :tags: bug, tests - :tickets: 1435 - - Fixes to support pytest 8.1 for the test suite. diff --git a/docs/build/unreleased/1479.rst b/docs/build/unreleased/1479.rst deleted file mode 100644 index 5a653201..00000000 --- a/docs/build/unreleased/1479.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. change:: - :tags: bug, autogenerate, postgresql - :tickets: 1479 - - Fixed the detection of serial column in autogenerate with tables - not under default schema on PostgreSQL From 1a89cd5236803e669a3a9d79796b51282773b42a Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 26 Jun 2024 11:46:34 -0400 Subject: [PATCH 19/45] Version 1.13.3 placeholder --- alembic/__init__.py | 2 +- docs/build/changelog.rst | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/alembic/__init__.py b/alembic/__init__.py index 2d69db2e..acf69a62 100644 --- a/alembic/__init__.py +++ b/alembic/__init__.py @@ -1,4 +1,4 @@ from . import context from . import op -__version__ = "1.13.2" +__version__ = "1.13.3" diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index b240948f..513cabf2 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -3,6 +3,10 @@ Changelog ========== +.. changelog:: + :version: 1.13.3 + :include_notes_from: unreleased + .. changelog:: :version: 1.13.2 :released: June 26, 2024 From 963586cc78ede5705e7467624c7b345b22651015 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 26 Jun 2024 14:34:53 -0400 Subject: [PATCH 20/45] switch to book theme Change-Id: I034a5a9511fa2c0eecedb979ea965d53632416d1 --- docs/build/Makefile | 2 +- docs/build/_static/site_custom_css.css | 3 +++ docs/build/conf.py | 13 +++++++------ docs/build/requirements.txt | 3 +++ 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/build/Makefile b/docs/build/Makefile index c87a87a4..e2641609 100644 --- a/docs/build/Makefile +++ b/docs/build/Makefile @@ -4,7 +4,7 @@ # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -AUTOBUILD = sphinx-autobuild --port 8080 --watch ../../lib +AUTOBUILD = sphinx-autobuild --port 8080 --watch ../../alembic PAPER = BUILDDIR = output diff --git a/docs/build/_static/site_custom_css.css b/docs/build/_static/site_custom_css.css index e69de29b..8703512e 100644 --- a/docs/build/_static/site_custom_css.css +++ b/docs/build/_static/site_custom_css.css @@ -0,0 +1,3 @@ +.sidebar-toggle { + display: none !important +} diff --git a/docs/build/conf.py b/docs/build/conf.py index 90a334e6..859a9ef8 100644 --- a/docs/build/conf.py +++ b/docs/build/conf.py @@ -146,9 +146,7 @@ # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = "nature" - -html_style = "nature_override.css" +html_theme = "sphinx_book_theme" # Theme options are theme-specific and customize the look and feel of a theme @@ -181,6 +179,8 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +html_css_files = ["site_custom_css.css"] + # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' @@ -192,10 +192,11 @@ # Custom sidebar templates, maps document names to template names. html_sidebars = { "**": [ + "navbar-logo.html", "site_custom_sidebars.html", - "localtoc.html", - "searchbox.html", - "relations.html", + "icon-links.html", + "search-button-field.html", + "sbt-sidebar-nav.html", ] } diff --git a/docs/build/requirements.txt b/docs/build/requirements.txt index bf162865..6925fcd3 100644 --- a/docs/build/requirements.txt +++ b/docs/build/requirements.txt @@ -7,3 +7,6 @@ Mako importlib-metadata;python_version<"3.9" importlib-resources;python_version<"3.9" sphinx_copybutton==0.5.1 +sphinx-book-theme + + From 6153e6bfd5a2092020e2a2cd9a46d9f87d5a2463 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 1 Jul 2024 09:23:21 -0400 Subject: [PATCH 21/45] remove sidebar toggle here, it's in site build Change-Id: Ida2ac231d708679e57d7c59ee426a59c72b4f89c --- docs/build/_static/site_custom_css.css | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/build/_static/site_custom_css.css b/docs/build/_static/site_custom_css.css index 8703512e..e69de29b 100644 --- a/docs/build/_static/site_custom_css.css +++ b/docs/build/_static/site_custom_css.css @@ -1,3 +0,0 @@ -.sidebar-toggle { - display: none !important -} From ff91bbc0f6c208f8e0337bc62dbe57f7532e76d6 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Fri, 26 Jul 2024 21:35:52 +0200 Subject: [PATCH 22/45] add compatibility with mypy 1.11 Change-Id: Ieaa150c8aec70d54e87aa3355f5fc37c232f47ae --- alembic/util/sqla_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alembic/util/sqla_compat.py b/alembic/util/sqla_compat.py index 30b9b4c4..d4ed0fdd 100644 --- a/alembic/util/sqla_compat.py +++ b/alembic/util/sqla_compat.py @@ -527,7 +527,7 @@ def __init__(self, table: Table, text: TextClause) -> None: self.fake_column = schema.Column(self.text.text, sqltypes.NULLTYPE) table.append_column(self.fake_column) - def get_children(self): + def get_children(self, **kw): return [self.fake_column] From 252f7b994d62f0cb2ff85654368d99cfacba5d7b Mon Sep 17 00:00:00 2001 From: "Pavel V. Pristupa" Date: Tue, 30 Jul 2024 01:45:20 -0400 Subject: [PATCH 23/45] Enhance version_path_separator behaviour by adding a newline option ### Description version_path_separator now consists a new option "newline" which allows you to specify multiple version locations across multiple lines like this: ``` version_locations = /foo/versions /bar/versions /baz/versions version_path_separator = newline ``` ### Checklist This pull request is: - [ ] A documentation / typographical error fix - Good to go, no issue or tests are needed - [ ] A short code fix - please include the issue number, and create an issue if none exists, which must include a complete example of the issue. one line code fixes without an issue and demonstration will not be accepted. - Please include: `Fixes: #` in the commit message - please include tests. one line code fixes without tests will not be accepted. - [x] A new feature implementation - please include the issue number, and create an issue if none exists, which must include a complete example of how the feature would look. - Please include: `Fixes: #` in the commit message - please include tests. **Have a nice day!** Closes: #1510 Pull-request: https://github.com/sqlalchemy/alembic/pull/1510 Pull-request-sha: 6155da71472fa2727beabd0aeaec1bdc07378b1b Change-Id: I364906906a9c7164e8f7fa5f51f3097ab118cc65 --- alembic/script/base.py | 8 ++++++-- alembic/templates/async/alembic.ini.mako | 1 + alembic/templates/generic/alembic.ini.mako | 1 + alembic/templates/multidb/alembic.ini.mako | 1 + alembic/testing/assertions.py | 17 +++++++++++++---- docs/build/tutorial.rst | 1 + docs/build/unreleased/1509.rst | 5 +++++ tests/test_config.py | 11 +++++++++-- 8 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 docs/build/unreleased/1509.rst diff --git a/alembic/script/base.py b/alembic/script/base.py index 9a955b6b..30df6ddb 100644 --- a/alembic/script/base.py +++ b/alembic/script/base.py @@ -187,6 +187,7 @@ def from_config(cls, config: Config) -> ScriptDirectory: split_on_path = { None: None, "space": " ", + "newline": "\n", "os": os.pathsep, ":": ":", ";": ";", @@ -200,7 +201,8 @@ def from_config(cls, config: Config) -> ScriptDirectory: raise ValueError( "'%s' is not a valid value for " "version_path_separator; " - "expected 'space', 'os', ':', ';'" % version_path_separator + "expected 'space', 'newline', 'os', ':', ';'" + % version_path_separator ) from ke else: if split_char is None: @@ -210,7 +212,9 @@ def from_config(cls, config: Config) -> ScriptDirectory: ) else: version_locations = [ - x for x in version_locations_str.split(split_char) if x + x.strip() + for x in version_locations_str.split(split_char) + if x ] else: version_locations = None diff --git a/alembic/templates/async/alembic.ini.mako b/alembic/templates/async/alembic.ini.mako index 3558295e..46a0904e 100644 --- a/alembic/templates/async/alembic.ini.mako +++ b/alembic/templates/async/alembic.ini.mako @@ -47,6 +47,7 @@ prepend_sys_path = . # version_path_separator = : # version_path_separator = ; # version_path_separator = space +# version_path_separator = newline version_path_separator = os # Use os.pathsep. Default configuration used for new projects. # set to 'true' to search source files recursively diff --git a/alembic/templates/generic/alembic.ini.mako b/alembic/templates/generic/alembic.ini.mako index b3fe0535..dd4ea588 100644 --- a/alembic/templates/generic/alembic.ini.mako +++ b/alembic/templates/generic/alembic.ini.mako @@ -49,6 +49,7 @@ prepend_sys_path = . # version_path_separator = : # version_path_separator = ; # version_path_separator = space +# version_path_separator = newline version_path_separator = os # Use os.pathsep. Default configuration used for new projects. # set to 'true' to search source files recursively diff --git a/alembic/templates/multidb/alembic.ini.mako b/alembic/templates/multidb/alembic.ini.mako index 6e901999..d5cc86f1 100644 --- a/alembic/templates/multidb/alembic.ini.mako +++ b/alembic/templates/multidb/alembic.ini.mako @@ -49,6 +49,7 @@ prepend_sys_path = . # version_path_separator = : # version_path_separator = ; # version_path_separator = space +# version_path_separator = newline version_path_separator = os # Use os.pathsep. Default configuration used for new projects. # set to 'true' to search source files recursively diff --git a/alembic/testing/assertions.py b/alembic/testing/assertions.py index ec9593b7..e071697c 100644 --- a/alembic/testing/assertions.py +++ b/alembic/testing/assertions.py @@ -74,7 +74,9 @@ class _ErrorContainer: @contextlib.contextmanager -def _expect_raises(except_cls, msg=None, check_context=False): +def _expect_raises( + except_cls, msg=None, check_context=False, text_exact=False +): ec = _ErrorContainer() if check_context: are_we_already_in_a_traceback = sys.exc_info()[0] @@ -85,7 +87,10 @@ def _expect_raises(except_cls, msg=None, check_context=False): ec.error = err success = True if msg is not None: - assert re.search(msg, str(err), re.UNICODE), f"{msg} !~ {err}" + if text_exact: + assert str(err) == msg, f"{msg} != {err}" + else: + assert re.search(msg, str(err), re.UNICODE), f"{msg} !~ {err}" if check_context and not are_we_already_in_a_traceback: _assert_proper_exception_context(err) print(str(err).encode("utf-8")) @@ -98,8 +103,12 @@ def expect_raises(except_cls, check_context=True): return _expect_raises(except_cls, check_context=check_context) -def expect_raises_message(except_cls, msg, check_context=True): - return _expect_raises(except_cls, msg=msg, check_context=check_context) +def expect_raises_message( + except_cls, msg, check_context=True, text_exact=False +): + return _expect_raises( + except_cls, msg=msg, check_context=check_context, text_exact=text_exact + ) def eq_ignore_whitespace(a, b, msg=None): diff --git a/docs/build/tutorial.rst b/docs/build/tutorial.rst index f1cd4916..6cc16697 100644 --- a/docs/build/tutorial.rst +++ b/docs/build/tutorial.rst @@ -174,6 +174,7 @@ The file generated with the "generic" configuration looks like:: # version_path_separator = : # version_path_separator = ; # version_path_separator = space + # version_path_separator = newline version_path_separator = os # Use os.pathsep. Default configuration used for new projects. # set to 'true' to search source files recursively diff --git a/docs/build/unreleased/1509.rst b/docs/build/unreleased/1509.rst new file mode 100644 index 00000000..df3bf97d --- /dev/null +++ b/docs/build/unreleased/1509.rst @@ -0,0 +1,5 @@ +.. change:: + :tags: feature, environment + :tickets: 1509 + + Enhance ``version_locations`` parsing to handle paths containing newlines. diff --git a/tests/test_config.py b/tests/test_config.py index 73ce2aa9..a98994c4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -134,6 +134,12 @@ def test_attributes_constructor(self): "/foo /bar", ["/foo", "/bar"], ), + ( + "multiline string 1", + "newline", + " /foo \n/bar ", + ["/foo", "/bar"], + ), ( "Linux pathsep 1", ":", @@ -171,7 +177,7 @@ def test_attributes_constructor(self): "/foo|/bar", ValueError( "'|' is not a valid value for version_path_separator; " - "expected 'space', 'os', ':', ';'" + "expected 'space', 'newline', 'os', ':', ';'" ), ), id_="iaaa", @@ -188,7 +194,8 @@ def test_version_locations(self, separator, string_value, expected_result): cfg.set_main_option("version_locations", string_value) if isinstance(expected_result, ValueError): - with expect_raises_message(ValueError, expected_result.args[0]): + message = str(expected_result) + with expect_raises_message(ValueError, message, text_exact=True): ScriptDirectory.from_config(cfg) else: s = ScriptDirectory.from_config(cfg) From 6bf8238afbcd67a60c92de99113e635f23325c14 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 3 Aug 2024 16:44:22 -0400 Subject: [PATCH 24/45] add a test for FK w/ naming convention; update mypy thing There seems to be some dependency for mypy stated in tox for unclear reasons that no longer exists, remove it References: https://github.com/sqlalchemy/alembic/discussions/1029#discussioncomment-10232170 Change-Id: Ied1a578f99ece0875e5d964b4f47a7759c9b2267 --- tests/test_autogen_render.py | 12 ++++++++++++ tox.ini | 1 - 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/test_autogen_render.py b/tests/test_autogen_render.py index 254b6ddd..7907f7ec 100644 --- a/tests/test_autogen_render.py +++ b/tests/test_autogen_render.py @@ -2448,6 +2448,18 @@ def test_inline_fk(self): "name=op.f('fk_ct_t_c_q')))", ) + def test_added_fk(self): + t = Table("t", self.metadata, Column("c", Integer, ForeignKey("q.id"))) + + fk = list(t.foreign_key_constraints)[0] + eq_ignore_whitespace( + autogenerate.render._render_foreign_key( + fk, self.autogen_context, self.metadata + ), + "sa.ForeignKeyConstraint(['c'], ['q.id'], " + "name=op.f('fk_ct_t_c_q'))", + ) + def test_render_check_constraint_renamed(self): """test that constraints from autogenerate render with the naming convention name explicitly. These names should diff --git a/tox.ini b/tox.ini index 4b0f082b..f1bfcac3 100644 --- a/tox.ini +++ b/tox.ini @@ -75,7 +75,6 @@ deps= mypy sqlalchemy>=2 mako - types-pkg-resources # is imported in alembic/testing and mypy complains if it's not installed. pytest commands = mypy ./alembic/ --exclude alembic/templates From 858abd31cec0b17533c13abf7ea45758df282d44 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 19 Aug 2024 11:09:41 -0400 Subject: [PATCH 25/45] add mysql/mariadb to multi-tenant recipe we can use USE just as easily as search_path here, so add that. Change-Id: I0af8b7c15c9647c613ba6e0aae99173745df29af --- docs/build/cookbook.rst | 42 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/docs/build/cookbook.rst b/docs/build/cookbook.rst index fd84db0a..a9121730 100644 --- a/docs/build/cookbook.rst +++ b/docs/build/cookbook.rst @@ -776,8 +776,8 @@ recreated again within the downgrade for this migration:: .. _cookbook_postgresql_multi_tenancy: -Rudimental Schema-Level Multi Tenancy for PostgreSQL Databases -============================================================== +Rudimental Schema-Level Multi Tenancy for PostgreSQL, MySQL, Other Databases +============================================================================ **Multi tenancy** refers to an application that accommodates for many clients simultaneously. Within the scope of a database migrations tool, @@ -793,6 +793,11 @@ is to install tenants within **individual PostgreSQL schemas**. When using PostgreSQL's schemas, a special variable ``search_path`` is offered that is intended to assist with targeting of different schemas. +When using MySQL or MariaDB databases, a similar command is available at the +SQL level called the``use`` command. This command may be used in a similar +fashion as that of PostgreSQL's ``search_path`` variable to achieve a similar +effect. + .. note:: SQLAlchemy includes a system of directing a common set of ``Table`` metadata to many schemas called `schema_translate_map `_. Alembic at the time of this writing lacks adequate support for this feature. The recipe below @@ -801,7 +806,7 @@ intended to assist with targeting of different schemas. The recipe below can be altered for flexibility. The primary purpose of this recipe is to illustrate how to point the Alembic process towards one PostgreSQL -schema or another. +or MySQL/MariaDB schema or another. 1. The model metadata used as the target for autogenerate must not include any schema name for tables; the schema must be non-present or set to ``None``. @@ -841,12 +846,20 @@ schema or another. current_tenant = context.get_x_argument(as_dictionary=True).get("tenant") with connectable.connect() as connection: - # set search path on the connection, which ensures that - # PostgreSQL will emit all CREATE / ALTER / DROP statements - # in terms of this schema by default - connection.execute(text('set search_path to "%s"' % current_tenant)) - # in SQLAlchemy v2+ the search path change needs to be committed - connection.commit() + if connection.dialect.name == "postgresql": + # set search path on the connection, which ensures that + # PostgreSQL will emit all CREATE / ALTER / DROP statements + # in terms of this schema by default + + connection.execute(text('set search_path to "%s"' % current_tenant)) + # in SQLAlchemy v2+ the search path change needs to be committed + connection.commit() + elif connection.dialect.name in ("mysql", "mariadb"): + # set "USE" on the connection, which ensures that + # MySQL/MariaDB will emit all CREATE / ALTER / DROP statements + # in terms of this schema by default + + connection.execute(text('USE %s' % current_tenant)) # make use of non-supported SQLAlchemy attribute to ensure # the dialect reflects tables in terms of the current tenant name @@ -860,17 +873,18 @@ schema or another. with context.begin_transaction(): context.run_migrations() - The current tenant is set using the PostgreSQL ``search_path`` variable on - the connection. Note above we must employ a **non-supported SQLAlchemy - workaround** at the moment which is to hardcode the SQLAlchemy dialect's - default schema name to our target schema. + The current tenant is set using the PostgreSQL ``search_path`` variable, or + the MySQL/MariaDB ``USE`` statement, on the connection. Note above we must + employ a **non-supported SQLAlchemy workaround** at the moment which is to + hardcode the SQLAlchemy dialect's default schema name to our target schema. It is also important to note that the above changes **remain on the connection permanently unless reversed explicitly**. If the alembic application simply exits above, there is no issue. However if the application attempts to continue using the above connection for other purposes, it may be necessary to reset these variables back to the default, which for PostgreSQL is usually - the name "public" however may be different based on configuration. + the name "public" however may be different based on configuration, and + for MySQL/MariaDB is typically the "database" portion of the database URL. 4. Alembic operations will now proceed in terms of whichever schema we pass From 30292b38b73d94d4cea743137c4261cf9a305e1f Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 19 Aug 2024 11:13:27 -0400 Subject: [PATCH 26/45] some updates to the commit for multi-tenant I just did Change-Id: I5e9c03697af5d65f68c37bf4afd6caaf73ce270a --- docs/build/cookbook.rst | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/build/cookbook.rst b/docs/build/cookbook.rst index a9121730..ce5fb543 100644 --- a/docs/build/cookbook.rst +++ b/docs/build/cookbook.rst @@ -789,14 +789,19 @@ the approach must involve running Alembic multiple times against different database URLs. One common approach to multi-tenancy, particularly on the PostgreSQL database, -is to install tenants within **individual PostgreSQL schemas**. When using -PostgreSQL's schemas, a special variable ``search_path`` is offered that is -intended to assist with targeting of different schemas. - -When using MySQL or MariaDB databases, a similar command is available at the -SQL level called the``use`` command. This command may be used in a similar -fashion as that of PostgreSQL's ``search_path`` variable to achieve a similar -effect. +is to install tenants within **individual PostgreSQL schemas**; similarly +when using MySQL/MariaDB, **individual MySQL/MariaDB databases** are addressed +in the same way as "schemas" on PostgreSQL. + +When using PostgreSQL's schemas, a special variable ``search_path`` is offered +that is intended to assist with targeting of different schemas. When using +MySQL or MariaDB databases, a similar command is available at the SQL level +called the ``USE`` command. This command may be used in a similar fashion as +that of PostgreSQL's ``search_path`` variable to achieve a similar effect. + +Overall, this recipe can be used on **any database that supports runtime +modification of the current "tenant" via SQL commands on a particular +connection**. .. note:: SQLAlchemy includes a system of directing a common set of ``Table`` metadata to many schemas called `schema_translate_map `_. Alembic at the time From 434a7886164f97ee70b89b8963dd941c9874b7aa Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 19 Aug 2024 12:27:55 -0400 Subject: [PATCH 27/45] add cool github /pypi link stuff Change-Id: I57ec7a22a1d14992cc0f70d527a584a737022e21 --- docs/build/conf.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/build/conf.py b/docs/build/conf.py index 859a9ef8..549b6dd6 100644 --- a/docs/build/conf.py +++ b/docs/build/conf.py @@ -153,7 +153,27 @@ # further. For a list of options available for each theme, see the # documentation. # NOTE: use nature_override.css, not this -# html_theme_options = {} + +# mike got these just by copying from +# https://github.com/executablebooks/sphinx-book-theme/blob/master/docs/conf.py#L103. +# none of this seems to be clearly documented +html_theme_options = { + "repository_url": "https://github.com/sqlalchemy/alembic", + "repository_branch": "main", + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/sqlalchemy/alembic", + "icon": "fa-brands fa-github", + }, + { + "name": "PyPI", + "url": "https://pypi.org/project/alembic/", + "icon": "https://img.shields.io/pypi/dw/alembic", + "type": "url", + }, + ], +} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] From 9d6e212b77c8ce5ea1164b6d67f2ba491beba413 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 2 Sep 2024 11:33:26 -0400 Subject: [PATCH 28/45] unpin setuptools The pin for ``setuptools<69.3`` in ``pyproject.toml`` has been removed. This pin was to prevent a sudden change to :pep:`625` in setuptools from taking place which changes the file name of SQLAlchemy's source distribution on pypi to be an all lower case name, and the change was extended to all SQLAlchemy projects to prevent any further surprises. However, the presence of this pin is now holding back environments that otherwise want to use a newer setuptools, so we've decided to move forward with this change, with the assumption that build environments will have largely accommodated the setuptools change by now. Change-Id: I0cd9ab0512004669a8f0aa0cb7f560d89a2da2bd --- docs/build/unreleased/unpin_setuptools.rst | 15 +++++++++++++++ pyproject.toml | 4 +--- 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 docs/build/unreleased/unpin_setuptools.rst diff --git a/docs/build/unreleased/unpin_setuptools.rst b/docs/build/unreleased/unpin_setuptools.rst new file mode 100644 index 00000000..a0e0766f --- /dev/null +++ b/docs/build/unreleased/unpin_setuptools.rst @@ -0,0 +1,15 @@ +.. change:: + :tags: change, general + + The pin for ``setuptools<69.3`` in ``pyproject.toml`` has been removed. + This pin was to prevent a sudden change to :pep:`625` in setuptools from + taking place which changes the file name of SQLAlchemy's source + distribution on pypi to be an all lower case name, and the change was + extended to all SQLAlchemy projects to prevent any further surprises. + However, the presence of this pin is now holding back environments that + otherwise want to use a newer setuptools, so we've decided to move forward + with this change, with the assumption that build environments will have + largely accommodated the setuptools change by now. + + + diff --git a/pyproject.toml b/pyproject.toml index 628e6761..2d2edede 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,7 @@ [build-system] build-backend = "setuptools.build_meta" requires = [ - # avoid moving to https://github.com/pypa/setuptools/issues/3593 - # until we're ready - "setuptools>=61.0,<69.3", + "setuptools>=61.0", ] [tool.black] From 8fae3e1d0813b35b1ffae22ef38f7bea7c93381f Mon Sep 17 00:00:00 2001 From: Aaron Griffin Date: Tue, 10 Sep 2024 10:36:13 -0400 Subject: [PATCH 29/45] Support if_exists and if_not_exists on create/drop table commands Added support for :paramref:`.Operations.create_table.if_not_exists` and :paramref:`.Operations.drop_table.if_exists`, adding similar functionality to render IF [NOT] EXISTS for table operations in a similar way as with indexes. Pull request courtesy Aaron Griffin. Fixes: #1520 Closes: #1521 Pull-request: https://github.com/sqlalchemy/alembic/pull/1521 Pull-request-sha: 469be01c6b5f9f42dc26017040a6fc54c4caef54 Change-Id: I5dcf44d9e906cdb84c32c4bfb6a1c63cde6324fd --- alembic/ddl/impl.py | 8 ++++---- alembic/op.pyi | 21 +++++++++++++++++++-- alembic/operations/base.py | 21 +++++++++++++++++++-- alembic/operations/ops.py | 18 ++++++++++++++++-- alembic/operations/toimpl.py | 16 ++++++++++++++-- docs/build/unreleased/1520.rst | 9 +++++++++ tests/test_op.py | 20 ++++++++++++++++++++ 7 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 docs/build/unreleased/1520.rst diff --git a/alembic/ddl/impl.py b/alembic/ddl/impl.py index d2983923..25746889 100644 --- a/alembic/ddl/impl.py +++ b/alembic/ddl/impl.py @@ -362,11 +362,11 @@ def rename_table( base.RenameTable(old_table_name, new_table_name, schema=schema) ) - def create_table(self, table: Table) -> None: + def create_table(self, table: Table, **kw: Any) -> None: table.dispatch.before_create( table, self.connection, checkfirst=False, _ddl_runner=self ) - self._exec(schema.CreateTable(table)) + self._exec(schema.CreateTable(table, **kw)) table.dispatch.after_create( table, self.connection, checkfirst=False, _ddl_runner=self ) @@ -385,11 +385,11 @@ def create_table(self, table: Table) -> None: if comment and with_comment: self.create_column_comment(column) - def drop_table(self, table: Table) -> None: + def drop_table(self, table: Table, **kw: Any) -> None: table.dispatch.before_drop( table, self.connection, checkfirst=False, _ddl_runner=self ) - self._exec(schema.DropTable(table)) + self._exec(schema.DropTable(table, **kw)) table.dispatch.after_drop( table, self.connection, checkfirst=False, _ddl_runner=self ) diff --git a/alembic/op.pyi b/alembic/op.pyi index 83deac1e..92044469 100644 --- a/alembic/op.pyi +++ b/alembic/op.pyi @@ -747,7 +747,12 @@ def create_primary_key( """ -def create_table(table_name: str, *columns: SchemaItem, **kw: Any) -> Table: +def create_table( + table_name: str, + *columns: SchemaItem, + if_not_exists: Optional[bool] = None, + **kw: Any, +) -> Table: r"""Issue a "create table" instruction using the current migration context. @@ -818,6 +823,10 @@ def create_table(table_name: str, *columns: SchemaItem, **kw: Any) -> Table: quoting of the schema outside of the default behavior, use the SQLAlchemy construct :class:`~sqlalchemy.sql.elements.quoted_name`. + :param if_not_exists: If True, adds IF NOT EXISTS operator when + creating the new table. + + .. versionadded:: 1.13.3 :param \**kw: Other keyword arguments are passed to the underlying :class:`sqlalchemy.schema.Table` object created for the command. @@ -998,7 +1007,11 @@ def drop_index( """ def drop_table( - table_name: str, *, schema: Optional[str] = None, **kw: Any + table_name: str, + *, + schema: Optional[str] = None, + if_exists: Optional[bool] = None, + **kw: Any, ) -> None: r"""Issue a "drop table" instruction using the current migration context. @@ -1013,6 +1026,10 @@ def drop_table( quoting of the schema outside of the default behavior, use the SQLAlchemy construct :class:`~sqlalchemy.sql.elements.quoted_name`. + :param if_exists: If True, adds IF EXISTS operator when + dropping the table. + + .. versionadded:: 1.13.3 :param \**kw: Other keyword arguments are passed to the underlying :class:`sqlalchemy.schema.Table` object created for the command. diff --git a/alembic/operations/base.py b/alembic/operations/base.py index 27dd3b9e..9b52fa6f 100644 --- a/alembic/operations/base.py +++ b/alembic/operations/base.py @@ -1175,7 +1175,11 @@ def create_primary_key( ... def create_table( - self, table_name: str, *columns: SchemaItem, **kw: Any + self, + table_name: str, + *columns: SchemaItem, + if_not_exists: Optional[bool] = None, + **kw: Any, ) -> Table: r"""Issue a "create table" instruction using the current migration context. @@ -1247,6 +1251,10 @@ def create_table( quoting of the schema outside of the default behavior, use the SQLAlchemy construct :class:`~sqlalchemy.sql.elements.quoted_name`. + :param if_not_exists: If True, adds IF NOT EXISTS operator when + creating the new table. + + .. versionadded:: 1.13.3 :param \**kw: Other keyword arguments are passed to the underlying :class:`sqlalchemy.schema.Table` object created for the command. @@ -1438,7 +1446,12 @@ def drop_index( ... def drop_table( - self, table_name: str, *, schema: Optional[str] = None, **kw: Any + self, + table_name: str, + *, + schema: Optional[str] = None, + if_exists: Optional[bool] = None, + **kw: Any, ) -> None: r"""Issue a "drop table" instruction using the current migration context. @@ -1453,6 +1466,10 @@ def drop_table( quoting of the schema outside of the default behavior, use the SQLAlchemy construct :class:`~sqlalchemy.sql.elements.quoted_name`. + :param if_exists: If True, adds IF EXISTS operator when + dropping the table. + + .. versionadded:: 1.13.3 :param \**kw: Other keyword arguments are passed to the underlying :class:`sqlalchemy.schema.Table` object created for the command. diff --git a/alembic/operations/ops.py b/alembic/operations/ops.py index e6f1fb64..60b856a8 100644 --- a/alembic/operations/ops.py +++ b/alembic/operations/ops.py @@ -1159,6 +1159,7 @@ def __init__( columns: Sequence[SchemaItem], *, schema: Optional[str] = None, + if_not_exists: Optional[bool] = None, _namespace_metadata: Optional[MetaData] = None, _constraints_included: bool = False, **kw: Any, @@ -1166,6 +1167,7 @@ def __init__( self.table_name = table_name self.columns = columns self.schema = schema + self.if_not_exists = if_not_exists self.info = kw.pop("info", {}) self.comment = kw.pop("comment", None) self.prefixes = kw.pop("prefixes", None) @@ -1228,6 +1230,7 @@ def create_table( operations: Operations, table_name: str, *columns: SchemaItem, + if_not_exists: Optional[bool] = None, **kw: Any, ) -> Table: r"""Issue a "create table" instruction using the current migration @@ -1300,6 +1303,10 @@ def create_table( quoting of the schema outside of the default behavior, use the SQLAlchemy construct :class:`~sqlalchemy.sql.elements.quoted_name`. + :param if_not_exists: If True, adds IF NOT EXISTS operator when + creating the new table. + + .. versionadded:: 1.13.3 :param \**kw: Other keyword arguments are passed to the underlying :class:`sqlalchemy.schema.Table` object created for the command. @@ -1307,7 +1314,7 @@ def create_table( to the parameters given. """ - op = cls(table_name, columns, **kw) + op = cls(table_name, columns, if_not_exists=if_not_exists, **kw) return operations.invoke(op) @@ -1320,11 +1327,13 @@ def __init__( table_name: str, *, schema: Optional[str] = None, + if_exists: Optional[bool] = None, table_kw: Optional[MutableMapping[Any, Any]] = None, _reverse: Optional[CreateTableOp] = None, ) -> None: self.table_name = table_name self.schema = schema + self.if_exists = if_exists self.table_kw = table_kw or {} self.comment = self.table_kw.pop("comment", None) self.info = self.table_kw.pop("info", None) @@ -1385,6 +1394,7 @@ def drop_table( table_name: str, *, schema: Optional[str] = None, + if_exists: Optional[bool] = None, **kw: Any, ) -> None: r"""Issue a "drop table" instruction using the current @@ -1400,11 +1410,15 @@ def drop_table( quoting of the schema outside of the default behavior, use the SQLAlchemy construct :class:`~sqlalchemy.sql.elements.quoted_name`. + :param if_exists: If True, adds IF EXISTS operator when + dropping the table. + + .. versionadded:: 1.13.3 :param \**kw: Other keyword arguments are passed to the underlying :class:`sqlalchemy.schema.Table` object created for the command. """ - op = cls(table_name, schema=schema, table_kw=kw) + op = cls(table_name, schema=schema, if_exists=if_exists, table_kw=kw) operations.invoke(op) diff --git a/alembic/operations/toimpl.py b/alembic/operations/toimpl.py index 4759f7fd..4b960049 100644 --- a/alembic/operations/toimpl.py +++ b/alembic/operations/toimpl.py @@ -79,8 +79,14 @@ def _count_constraint(constraint): @Operations.implementation_for(ops.DropTableOp) def drop_table(operations: "Operations", operation: "ops.DropTableOp") -> None: + kw = {} + if operation.if_exists is not None: + if not sqla_14: + raise NotImplementedError("SQLAlchemy 1.4+ required") + + kw["if_exists"] = operation.if_exists operations.impl.drop_table( - operation.to_table(operations.migration_context) + operation.to_table(operations.migration_context), **kw ) @@ -127,8 +133,14 @@ def drop_index(operations: "Operations", operation: "ops.DropIndexOp") -> None: def create_table( operations: "Operations", operation: "ops.CreateTableOp" ) -> "Table": + kw = {} + if operation.if_not_exists is not None: + if not sqla_14: + raise NotImplementedError("SQLAlchemy 1.4+ required") + + kw["if_not_exists"] = operation.if_not_exists table = operation.to_table(operations.migration_context) - operations.impl.create_table(table) + operations.impl.create_table(table, **kw) return table diff --git a/docs/build/unreleased/1520.rst b/docs/build/unreleased/1520.rst new file mode 100644 index 00000000..4a0b763b --- /dev/null +++ b/docs/build/unreleased/1520.rst @@ -0,0 +1,9 @@ +.. change:: + :tags: usecase, operations + :tickets: 1520 + + Added support for :paramref:`.Operations.create_table.if_not_exists` and + :paramref:`.Operations.drop_table.if_exists`, adding similar functionality + to render IF [NOT] EXISTS for table operations in a similar way as with + indexes. Pull request courtesy Aaron Griffin. + diff --git a/tests/test_op.py b/tests/test_op.py index 688799c9..cbb30ea5 100644 --- a/tests/test_op.py +++ b/tests/test_op.py @@ -907,6 +907,12 @@ def test_drop_table_schema(self): op.drop_table("tb_test", schema="foo") context.assert_("DROP TABLE foo.tb_test") + @config.requirements.sqlalchemy_14 + def test_drop_table_if_exists(self): + context = op_fixture() + op.drop_table("tb_test", if_exists=True) + context.assert_("DROP TABLE IF EXISTS tb_test") + def test_create_table_selfref(self): context = op_fixture() op.create_table( @@ -1079,6 +1085,20 @@ def test_create_table_two_fk(self): "FOREIGN KEY(foo_bar) REFERENCES foo (bar))" ) + @config.requirements.sqlalchemy_14 + def test_create_table_if_not_exists(self): + context = op_fixture() + op.create_table( + "some_table", + Column("id", Integer, primary_key=True), + if_not_exists=True, + ) + context.assert_( + "CREATE TABLE IF NOT EXISTS some_table (" + "id INTEGER NOT NULL, " + "PRIMARY KEY (id))" + ) + def test_execute_delete(self): context = op_fixture() From 3db374f19760320507a96443e29d19835654b2ec Mon Sep 17 00:00:00 2001 From: Louis-Amaury Chaib Date: Mon, 23 Sep 2024 08:23:02 -0400 Subject: [PATCH 30/45] Render `if_not_exists` option for CreateTableOp, CreateIndexOp, DropTableOp and DropIndexOp Render ``if_exists`` and ``if_not_exists`` parameters in :class:`.CreateTableOp`, :class:`.CreateIndexOp`, :class:`.DropTableOp` and :class:`.DropIndexOp` in an autogenerate context. While Alembic does not set these parameters during an autogenerate run, they can be enabled using a custom :class:`.Rewriter` in the ``env.py`` file, where they will now be part of the rendered Python code in revision files. Pull request courtesy of Louis-Amaury Chaib (@lachaib). Closes: #1446 Pull-request: https://github.com/sqlalchemy/alembic/pull/1446 Pull-request-sha: 90c9735767af1cf3ba7e40e71dfa0fb30efc1ee8 Change-Id: I6b0a5ffaf7e2d1a0a1e1f1e80ed0ee168ae2bd09 --- alembic/autogenerate/render.py | 11 ++++++++ docs/build/unreleased/1446.rst | 10 +++++++ tests/test_autogen_render.py | 50 ++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 docs/build/unreleased/1446.rst diff --git a/alembic/autogenerate/render.py b/alembic/autogenerate/render.py index 61d56acf..38bdbfca 100644 --- a/alembic/autogenerate/render.py +++ b/alembic/autogenerate/render.py @@ -279,6 +279,9 @@ def _add_table(autogen_context: AutogenContext, op: ops.CreateTableOp) -> str: prefixes = ", ".join("'%s'" % p for p in table._prefixes) text += ",\nprefixes=[%s]" % prefixes + if op.if_not_exists is not None: + text += ",\nif_not_exists=%r" % bool(op.if_not_exists) + text += "\n)" return text @@ -291,6 +294,10 @@ def _drop_table(autogen_context: AutogenContext, op: ops.DropTableOp) -> str: } if op.schema: text += ", schema=%r" % _ident(op.schema) + + if op.if_exists is not None: + text += ", if_exists=%r" % bool(op.if_exists) + text += ")" return text @@ -324,6 +331,8 @@ def _add_index(autogen_context: AutogenContext, op: ops.CreateIndexOp) -> str: assert index.table is not None opts = _render_dialect_kwargs_items(autogen_context, index) + if op.if_not_exists is not None: + opts.append("if_not_exists=%r" % bool(op.if_not_exists)) text = tmpl % { "prefix": _alembic_autogenerate_prefix(autogen_context), "name": _render_gen_name(autogen_context, index.name), @@ -356,6 +365,8 @@ def _drop_index(autogen_context: AutogenContext, op: ops.DropIndexOp) -> str: "table_name=%(table_name)r%(schema)s%(kwargs)s)" ) opts = _render_dialect_kwargs_items(autogen_context, index) + if op.if_exists is not None: + opts.append("if_exists=%r" % bool(op.if_exists)) text = tmpl % { "prefix": _alembic_autogenerate_prefix(autogen_context), "name": _render_gen_name(autogen_context, op.index_name), diff --git a/docs/build/unreleased/1446.rst b/docs/build/unreleased/1446.rst new file mode 100644 index 00000000..fb54481c --- /dev/null +++ b/docs/build/unreleased/1446.rst @@ -0,0 +1,10 @@ +.. change:: + :tags: usecase, autogenerate + + Render ``if_exists`` and ``if_not_exists`` parameters in + :class:`.CreateTableOp`, :class:`.CreateIndexOp`, :class:`.DropTableOp` and + :class:`.DropIndexOp` in an autogenerate context. While Alembic does not + set these parameters during an autogenerate run, they can be enabled using + a custom :class:`.Rewriter` in the ``env.py`` file, where they will now be + part of the rendered Python code in revision files. Pull request courtesy + of Louis-Amaury Chaib (@lachaib). diff --git a/tests/test_autogen_render.py b/tests/test_autogen_render.py index 7907f7ec..14a33194 100644 --- a/tests/test_autogen_render.py +++ b/tests/test_autogen_render.py @@ -93,6 +93,20 @@ def test_render_add_index(self): "['active', 'code'], unique=False)", ) + def test_render_add_index_if_not_exists(self): + """ + autogenerate.render._add_index + """ + t = self.table() + idx = Index("test_active_code_idx", t.c.active, t.c.code) + op_obj = ops.CreateIndexOp.from_index(idx) + op_obj.if_not_exists = True + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.create_index('test_active_code_idx', 'test', " + "['active', 'code'], unique=False, if_not_exists=True)", + ) + @testing.emits_warning("Can't validate argument ") def test_render_add_index_custom_kwarg(self): t = self.table() @@ -212,6 +226,20 @@ def test_drop_index(self): "op.drop_index('test_active_code_idx', table_name='test')", ) + def test_drop_index_if_exists(self): + """ + autogenerate.render._drop_index + """ + t = self.table() + idx = Index("test_active_code_idx", t.c.active, t.c.code) + op_obj = ops.DropIndexOp.from_index(idx) + op_obj.if_exists = True + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.drop_index('test_active_code_idx', table_name='test', " + "if_exists=True)", + ) + def test_drop_index_text(self): """ autogenerate.render._drop_index @@ -989,6 +1017,19 @@ def test_render_addtl_args(self): "mysql_engine='InnoDB',sqlite_autoincrement=True)", ) + def test_render_if_not_exists(self): + t = self.table() + op_obj = ops.CreateTableOp.from_table(t) + op_obj.if_not_exists = True + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.create_table('test'," + "sa.Column('id', sa.Integer(), nullable=False)," + "sa.Column('active', sa.Boolean(), nullable=True)," + "sa.Column('code', sa.String(length=255), nullable=True)," + "sa.PrimaryKeyConstraint('id'),if_not_exists=True)", + ) + def test_render_drop_table(self): op_obj = ops.DropTableOp.from_table(Table("sometable", MetaData())) eq_ignore_whitespace( @@ -1005,6 +1046,15 @@ def test_render_drop_table_w_schema(self): "op.drop_table('sometable', schema='foo')", ) + def test_render_drop_table_if_exists(self): + t = self.table() + op_obj = ops.DropTableOp.from_table(t) + op_obj.if_exists = True + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.drop_table('test', if_exists=True)", + ) + def test_render_table_no_implicit_check(self): m = MetaData() t = Table("test", m, Column("x", Boolean())) From 91ebb708bd9b26e95f48ceb86158a71c1cfab9c4 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 23 Sep 2024 09:58:48 -0400 Subject: [PATCH 31/45] changelog edit Change-Id: Iee173287d358ce75c3082242ba2d200871b58fee --- docs/build/unreleased/1509.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build/unreleased/1509.rst b/docs/build/unreleased/1509.rst index df3bf97d..ce84dd07 100644 --- a/docs/build/unreleased/1509.rst +++ b/docs/build/unreleased/1509.rst @@ -1,5 +1,5 @@ .. change:: - :tags: feature, environment + :tags: usecase, environment :tickets: 1509 Enhance ``version_locations`` parsing to handle paths containing newlines. From 2fa5d6540d7350e04a8beb73dfb0460ea6704313 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 23 Sep 2024 10:23:44 -0400 Subject: [PATCH 32/45] add mypy marker to pytest; pytest opts in pyproject.toml this is to avoid warnings generated by SQLAlchemy's test config Change-Id: I91026a4bbd36eead3856e9394dc7c1d85b703e47 --- pyproject.toml | 10 ++++++++++ setup.cfg | 6 ------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2d2edede..eedcd327 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,16 @@ requires = [ [tool.black] line-length = 79 +[tool.pytest.ini_options] +addopts = "--tb native -v -r sfxX -p no:warnings -p no:logging --maxfail=100" +python_files = "tests/test_*.py" +markers = [ + "backend: tests that should run on all backends; typically dialect-sensitive", + "mypy: mypy integration / plugin tests (not used by Alembic currently)", +] + + + [tool.mypy] exclude = [ diff --git a/setup.cfg b/setup.cfg index 70daeadd..e3a51f25 100644 --- a/setup.cfg +++ b/setup.cfg @@ -116,12 +116,6 @@ mssql=mssql+pyodbc://scott:tiger^5HHH@mssql2017:1433/test?driver=ODBC+Driver+18+ oracle=oracle://scott:tiger@127.0.0.1:1521 oracle8=oracle://scott:tiger@127.0.0.1:1521/?use_ansi=0 -[alembic] -[tool:pytest] -addopts= --tb native -v -r sfxX -p no:warnings -p no:logging --maxfail=100 -python_files=tests/test_*.py -markers = - backend: tests that should run on all backends; typically dialect-sensitive From b43a1966180f592d6be2a52361230d988e68353d Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 23 Sep 2024 10:51:00 -0400 Subject: [PATCH 33/45] - 1.13.3 --- docs/build/changelog.rst | 45 +++++++++++++++++++++- docs/build/conf.py | 4 +- docs/build/unreleased/1446.rst | 10 ----- docs/build/unreleased/1509.rst | 5 --- docs/build/unreleased/1520.rst | 9 ----- docs/build/unreleased/unpin_setuptools.rst | 15 -------- 6 files changed, 46 insertions(+), 42 deletions(-) delete mode 100644 docs/build/unreleased/1446.rst delete mode 100644 docs/build/unreleased/1509.rst delete mode 100644 docs/build/unreleased/1520.rst delete mode 100644 docs/build/unreleased/unpin_setuptools.rst diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 513cabf2..8ec8f32e 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -5,7 +5,50 @@ Changelog .. changelog:: :version: 1.13.3 - :include_notes_from: unreleased + :released: September 23, 2024 + + .. change:: + :tags: usecase, autogenerate + + Render ``if_exists`` and ``if_not_exists`` parameters in + :class:`.CreateTableOp`, :class:`.CreateIndexOp`, :class:`.DropTableOp` and + :class:`.DropIndexOp` in an autogenerate context. While Alembic does not + set these parameters during an autogenerate run, they can be enabled using + a custom :class:`.Rewriter` in the ``env.py`` file, where they will now be + part of the rendered Python code in revision files. Pull request courtesy + of Louis-Amaury Chaib (@lachaib). + + .. change:: + :tags: usecase, environment + :tickets: 1509 + + Enhance ``version_locations`` parsing to handle paths containing newlines. + + .. change:: + :tags: usecase, operations + :tickets: 1520 + + Added support for :paramref:`.Operations.create_table.if_not_exists` and + :paramref:`.Operations.drop_table.if_exists`, adding similar functionality + to render IF [NOT] EXISTS for table operations in a similar way as with + indexes. Pull request courtesy Aaron Griffin. + + + .. change:: + :tags: change, general + + The pin for ``setuptools<69.3`` in ``pyproject.toml`` has been removed. + This pin was to prevent a sudden change to :pep:`625` in setuptools from + taking place which changes the file name of SQLAlchemy's source + distribution on pypi to be an all lower case name, and the change was + extended to all SQLAlchemy projects to prevent any further surprises. + However, the presence of this pin is now holding back environments that + otherwise want to use a newer setuptools, so we've decided to move forward + with this change, with the assumption that build environments will have + largely accommodated the setuptools change by now. + + + .. changelog:: :version: 1.13.2 diff --git a/docs/build/conf.py b/docs/build/conf.py index 549b6dd6..915c690c 100644 --- a/docs/build/conf.py +++ b/docs/build/conf.py @@ -99,8 +99,8 @@ # The short X.Y version. version = alembic.__version__ # The full version, including alpha/beta/rc tags. -release = "1.13.2" -release_date = "June 26, 2024" +release = "1.13.3" +release_date = "September 23, 2024" # The language for content autogenerated by Sphinx. Refer to documentation diff --git a/docs/build/unreleased/1446.rst b/docs/build/unreleased/1446.rst deleted file mode 100644 index fb54481c..00000000 --- a/docs/build/unreleased/1446.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. change:: - :tags: usecase, autogenerate - - Render ``if_exists`` and ``if_not_exists`` parameters in - :class:`.CreateTableOp`, :class:`.CreateIndexOp`, :class:`.DropTableOp` and - :class:`.DropIndexOp` in an autogenerate context. While Alembic does not - set these parameters during an autogenerate run, they can be enabled using - a custom :class:`.Rewriter` in the ``env.py`` file, where they will now be - part of the rendered Python code in revision files. Pull request courtesy - of Louis-Amaury Chaib (@lachaib). diff --git a/docs/build/unreleased/1509.rst b/docs/build/unreleased/1509.rst deleted file mode 100644 index ce84dd07..00000000 --- a/docs/build/unreleased/1509.rst +++ /dev/null @@ -1,5 +0,0 @@ -.. change:: - :tags: usecase, environment - :tickets: 1509 - - Enhance ``version_locations`` parsing to handle paths containing newlines. diff --git a/docs/build/unreleased/1520.rst b/docs/build/unreleased/1520.rst deleted file mode 100644 index 4a0b763b..00000000 --- a/docs/build/unreleased/1520.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. change:: - :tags: usecase, operations - :tickets: 1520 - - Added support for :paramref:`.Operations.create_table.if_not_exists` and - :paramref:`.Operations.drop_table.if_exists`, adding similar functionality - to render IF [NOT] EXISTS for table operations in a similar way as with - indexes. Pull request courtesy Aaron Griffin. - diff --git a/docs/build/unreleased/unpin_setuptools.rst b/docs/build/unreleased/unpin_setuptools.rst deleted file mode 100644 index a0e0766f..00000000 --- a/docs/build/unreleased/unpin_setuptools.rst +++ /dev/null @@ -1,15 +0,0 @@ -.. change:: - :tags: change, general - - The pin for ``setuptools<69.3`` in ``pyproject.toml`` has been removed. - This pin was to prevent a sudden change to :pep:`625` in setuptools from - taking place which changes the file name of SQLAlchemy's source - distribution on pypi to be an all lower case name, and the change was - extended to all SQLAlchemy projects to prevent any further surprises. - However, the presence of this pin is now holding back environments that - otherwise want to use a newer setuptools, so we've decided to move forward - with this change, with the assumption that build environments will have - largely accommodated the setuptools change by now. - - - From 9cc68f5e89183e3c5634ac892bf7819dfc90a8d5 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 23 Sep 2024 10:52:36 -0400 Subject: [PATCH 34/45] Version 1.13.4 placeholder --- alembic/__init__.py | 2 +- docs/build/changelog.rst | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/alembic/__init__.py b/alembic/__init__.py index acf69a62..f698f868 100644 --- a/alembic/__init__.py +++ b/alembic/__init__.py @@ -1,4 +1,4 @@ from . import context from . import op -__version__ = "1.13.3" +__version__ = "1.13.4" diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 8ec8f32e..51a8a5e2 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -3,6 +3,10 @@ Changelog ========== +.. changelog:: + :version: 1.13.4 + :include_notes_from: unreleased + .. changelog:: :version: 1.13.3 :released: September 23, 2024 From bd50ba325b10a1a9cdb452fe40c96f959825797c Mon Sep 17 00:00:00 2001 From: Zubarev Grigoriy Date: Sat, 5 Oct 2024 17:14:02 -0400 Subject: [PATCH 35/45] Use logging.WARNING instead of logging.WARN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `WARN` constant is an undocumented alias for `WARNING`. Whilst it’s not deprecated, it’s not mentioned at all in the documentation. This references one of `flake8-logging` plugins [rule](https://github.com/adamchainz/flake8-logging?tab=readme-ov-file#log009-warn-is-undocumented-use-warning-instead). ### Description Changed all uses of `logging.WARN` to `logging.WARNING`. ### Checklist This pull request is: - [x] A documentation / typographical error fix - Good to go, no issue or tests are needed - [ ] A short code fix - please include the issue number, and create an issue if none exists, which must include a complete example of the issue. one line code fixes without an issue and demonstration will not be accepted. - Please include: `Fixes: #` in the commit message - please include tests. one line code fixes without tests will not be accepted. - [ ] A new feature implementation - please include the issue number, and create an issue if none exists, which must include a complete example of how the feature would look. - Please include: `Fixes: #` in the commit message - please include tests. **Have a nice day!** Closes: #1548 Pull-request: https://github.com/sqlalchemy/alembic/pull/1548 Pull-request-sha: 249b9a2942037f8da9f2db5736d1d988435b6fe5 Change-Id: I7c64ce9bb18af551c761ed9216713ee02cbbb83b --- alembic/templates/async/alembic.ini.mako | 4 ++-- alembic/templates/generic/alembic.ini.mako | 4 ++-- alembic/templates/multidb/alembic.ini.mako | 4 ++-- alembic/testing/env.py | 8 ++++---- docs/build/tutorial.rst | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/alembic/templates/async/alembic.ini.mako b/alembic/templates/async/alembic.ini.mako index 46a0904e..7eee9132 100644 --- a/alembic/templates/async/alembic.ini.mako +++ b/alembic/templates/async/alembic.ini.mako @@ -90,12 +90,12 @@ keys = console keys = generic [logger_root] -level = WARN +level = WARNING handlers = console qualname = [logger_sqlalchemy] -level = WARN +level = WARNING handlers = qualname = sqlalchemy.engine diff --git a/alembic/templates/generic/alembic.ini.mako b/alembic/templates/generic/alembic.ini.mako index dd4ea588..f1f76cae 100644 --- a/alembic/templates/generic/alembic.ini.mako +++ b/alembic/templates/generic/alembic.ini.mako @@ -92,12 +92,12 @@ keys = console keys = generic [logger_root] -level = WARN +level = WARNING handlers = console qualname = [logger_sqlalchemy] -level = WARN +level = WARNING handlers = qualname = sqlalchemy.engine diff --git a/alembic/templates/multidb/alembic.ini.mako b/alembic/templates/multidb/alembic.ini.mako index d5cc86f1..bf383ea1 100644 --- a/alembic/templates/multidb/alembic.ini.mako +++ b/alembic/templates/multidb/alembic.ini.mako @@ -97,12 +97,12 @@ keys = console keys = generic [logger_root] -level = WARN +level = WARNING handlers = console qualname = [logger_sqlalchemy] -level = WARN +level = WARNING handlers = qualname = sqlalchemy.engine diff --git a/alembic/testing/env.py b/alembic/testing/env.py index 5df7ef82..c37b4d30 100644 --- a/alembic/testing/env.py +++ b/alembic/testing/env.py @@ -118,7 +118,7 @@ def _sqlite_testing_config(sourceless=False, future=False): keys = console [logger_root] -level = WARN +level = WARNING handlers = console qualname = @@ -171,7 +171,7 @@ def _multi_dir_testing_config(sourceless=False, extra_version_location=""): keys = console [logger_root] -level = WARN +level = WARNING handlers = console qualname = @@ -216,7 +216,7 @@ def _no_sql_testing_config(dialect="postgresql", directives=""): keys = console [logger_root] -level = WARN +level = WARNING handlers = console qualname = @@ -497,7 +497,7 @@ def _multidb_testing_config(engines): keys = console [logger_root] -level = WARN +level = WARNING handlers = console qualname = diff --git a/docs/build/tutorial.rst b/docs/build/tutorial.rst index 6cc16697..5fed9f4a 100644 --- a/docs/build/tutorial.rst +++ b/docs/build/tutorial.rst @@ -217,12 +217,12 @@ The file generated with the "generic" configuration looks like:: keys = generic [logger_root] - level = WARN + level = WARNING handlers = console qualname = [logger_sqlalchemy] - level = WARN + level = WARNING handlers = qualname = sqlalchemy.engine From 431fd0783e2e414cac5b0d0f002d7adc71df3f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciek=20Bry=C5=84ski?= Date: Thu, 31 Oct 2024 12:47:35 -0400 Subject: [PATCH 36/45] Fix mypy linting issues ### Description Fixing mypy linting issues for new mypy version. ### Checklist This pull request is: - [ ] A documentation / typographical error fix - Good to go, no issue or tests are needed - [x] A short code fix - please include the issue number, and create an issue if none exists, which must include a complete example of the issue. one line code fixes without an issue and demonstration will not be accepted. - Please include: `Fixes: #` in the commit message - please include tests. one line code fixes without tests will not be accepted. - [ ] A new feature implementation - please include the issue number, and create an issue if none exists, which must include a complete example of how the feature would look. - Please include: `Fixes: #` in the commit message - please include tests. **Have a nice day!** Closes: #1564 Pull-request: https://github.com/sqlalchemy/alembic/pull/1564 Pull-request-sha: a79d47d3acf7ae628ead7ca829bfe1c6f864c3c9 Change-Id: I3be88b6328dc07c46e6b1ddf8c85480dc88cfe95 --- alembic/ddl/base.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/alembic/ddl/base.py b/alembic/ddl/base.py index 690c1537..6fbe9524 100644 --- a/alembic/ddl/base.py +++ b/alembic/ddl/base.py @@ -175,7 +175,7 @@ def __init__( self.comment = comment -@compiles(RenameTable) # type: ignore[misc] +@compiles(RenameTable) def visit_rename_table( element: RenameTable, compiler: DDLCompiler, **kw ) -> str: @@ -185,7 +185,7 @@ def visit_rename_table( ) -@compiles(AddColumn) # type: ignore[misc] +@compiles(AddColumn) def visit_add_column(element: AddColumn, compiler: DDLCompiler, **kw) -> str: return "%s %s" % ( alter_table(compiler, element.table_name, element.schema), @@ -193,7 +193,7 @@ def visit_add_column(element: AddColumn, compiler: DDLCompiler, **kw) -> str: ) -@compiles(DropColumn) # type: ignore[misc] +@compiles(DropColumn) def visit_drop_column(element: DropColumn, compiler: DDLCompiler, **kw) -> str: return "%s %s" % ( alter_table(compiler, element.table_name, element.schema), @@ -201,7 +201,7 @@ def visit_drop_column(element: DropColumn, compiler: DDLCompiler, **kw) -> str: ) -@compiles(ColumnNullable) # type: ignore[misc] +@compiles(ColumnNullable) def visit_column_nullable( element: ColumnNullable, compiler: DDLCompiler, **kw ) -> str: @@ -212,7 +212,7 @@ def visit_column_nullable( ) -@compiles(ColumnType) # type: ignore[misc] +@compiles(ColumnType) def visit_column_type(element: ColumnType, compiler: DDLCompiler, **kw) -> str: return "%s %s %s" % ( alter_table(compiler, element.table_name, element.schema), @@ -221,7 +221,7 @@ def visit_column_type(element: ColumnType, compiler: DDLCompiler, **kw) -> str: ) -@compiles(ColumnName) # type: ignore[misc] +@compiles(ColumnName) def visit_column_name(element: ColumnName, compiler: DDLCompiler, **kw) -> str: return "%s RENAME %s TO %s" % ( alter_table(compiler, element.table_name, element.schema), @@ -230,7 +230,7 @@ def visit_column_name(element: ColumnName, compiler: DDLCompiler, **kw) -> str: ) -@compiles(ColumnDefault) # type: ignore[misc] +@compiles(ColumnDefault) def visit_column_default( element: ColumnDefault, compiler: DDLCompiler, **kw ) -> str: @@ -245,7 +245,7 @@ def visit_column_default( ) -@compiles(ComputedColumnDefault) # type: ignore[misc] +@compiles(ComputedColumnDefault) def visit_computed_column( element: ComputedColumnDefault, compiler: DDLCompiler, **kw ): @@ -255,7 +255,7 @@ def visit_computed_column( ) -@compiles(IdentityColumnDefault) # type: ignore[misc] +@compiles(IdentityColumnDefault) def visit_identity_column( element: IdentityColumnDefault, compiler: DDLCompiler, **kw ): From ffccbc7f2db1b472c01c3db293d41598d5b29f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciek=20Bry=C5=84ski?= Date: Thu, 31 Oct 2024 17:29:40 -0400 Subject: [PATCH 37/45] Add ability to configure alembic_version table in DialectImpl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added a new hook to the :class:`.DefaultImpl` :meth:`.DefaultImpl.version_table_impl`. This allows third party dialects to define the exact structure of the alembic_version table, to include use cases where the table requires special directives and/or additional columns so that it may function correctly on a particular backend. This is not intended as a user-expansion hook, only a dialect implementation hook to produce a working alembic_version table. Pull request courtesy Maciek Bryński. This will be 1.14 so this also version bumps Fixes: #1560 Closes: #1563 Pull-request: https://github.com/sqlalchemy/alembic/pull/1563 Pull-request-sha: e70fdc8f4e405cabf5099c2100763d7b24da3be8 Change-Id: I5e565dff60a979526608d2a1c0c620fbca269a3f --- alembic/ddl/impl.py | 41 ++++++++++++++++++++++++++++-- alembic/runtime/migration.py | 29 +++++++++------------ docs/build/changelog.rst | 2 +- docs/build/unreleased/1560.rst | 12 +++++++++ tests/test_version_table.py | 46 ++++++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 docs/build/unreleased/1560.rst diff --git a/alembic/ddl/impl.py b/alembic/ddl/impl.py index 25746889..2609a62d 100644 --- a/alembic/ddl/impl.py +++ b/alembic/ddl/impl.py @@ -21,7 +21,12 @@ from typing import Union from sqlalchemy import cast +from sqlalchemy import Column +from sqlalchemy import MetaData +from sqlalchemy import PrimaryKeyConstraint from sqlalchemy import schema +from sqlalchemy import String +from sqlalchemy import Table from sqlalchemy import text from . import _autogen @@ -43,11 +48,9 @@ from sqlalchemy.sql import Executable from sqlalchemy.sql.elements import ColumnElement from sqlalchemy.sql.elements import quoted_name - from sqlalchemy.sql.schema import Column from sqlalchemy.sql.schema import Constraint from sqlalchemy.sql.schema import ForeignKeyConstraint from sqlalchemy.sql.schema import Index - from sqlalchemy.sql.schema import Table from sqlalchemy.sql.schema import UniqueConstraint from sqlalchemy.sql.selectable import TableClause from sqlalchemy.sql.type_api import TypeEngine @@ -136,6 +139,40 @@ def static_output(self, text: str) -> None: self.output_buffer.write(text + "\n\n") self.output_buffer.flush() + def version_table_impl( + self, + *, + version_table: str, + version_table_schema: Optional[str], + version_table_pk: bool, + **kw: Any, + ) -> Table: + """Generate a :class:`.Table` object which will be used as the + structure for the Alembic version table. + + Third party dialects may override this hook to provide an alternate + structure for this :class:`.Table`; requirements are only that it + be named based on the ``version_table`` parameter and contains + at least a single string-holding column named ``version_num``. + + .. versionadded:: 1.14 + + """ + vt = Table( + version_table, + MetaData(), + Column("version_num", String(32), nullable=False), + schema=version_table_schema, + ) + if version_table_pk: + vt.append_constraint( + PrimaryKeyConstraint( + "version_num", name=f"{version_table}_pkc" + ) + ) + + return vt + def requires_recreate_in_batch( self, batch_op: BatchOperationsImpl ) -> bool: diff --git a/alembic/runtime/migration.py b/alembic/runtime/migration.py index 6cfe5e23..28f01c3b 100644 --- a/alembic/runtime/migration.py +++ b/alembic/runtime/migration.py @@ -24,10 +24,6 @@ from sqlalchemy import Column from sqlalchemy import literal_column -from sqlalchemy import MetaData -from sqlalchemy import PrimaryKeyConstraint -from sqlalchemy import String -from sqlalchemy import Table from sqlalchemy.engine import Engine from sqlalchemy.engine import url as sqla_url from sqlalchemy.engine.strategies import MockEngineStrategy @@ -36,6 +32,7 @@ from .. import util from ..util import sqla_compat from ..util.compat import EncodedIO +from ..util.sqla_compat import _select if TYPE_CHECKING: from sqlalchemy.engine import Dialect @@ -190,18 +187,6 @@ def __init__( self.version_table_schema = version_table_schema = opts.get( "version_table_schema", None ) - self._version = Table( - version_table, - MetaData(), - Column("version_num", String(32), nullable=False), - schema=version_table_schema, - ) - if opts.get("version_table_pk", True): - self._version.append_constraint( - PrimaryKeyConstraint( - "version_num", name="%s_pkc" % version_table - ) - ) self._start_from_rev: Optional[str] = opts.get("starting_rev") self.impl = ddl.DefaultImpl.get_by_dialect(dialect)( @@ -212,6 +197,13 @@ def __init__( self.output_buffer, opts, ) + + self._version = self.impl.version_table_impl( + version_table=version_table, + version_table_schema=version_table_schema, + version_table_pk=opts.get("version_table_pk", True), + ) + log.info("Context impl %s.", self.impl.__class__.__name__) if self.as_sql: log.info("Generating static SQL") @@ -540,7 +532,10 @@ def get_current_heads(self) -> Tuple[str, ...]: return () assert self.connection is not None return tuple( - row[0] for row in self.connection.execute(self._version.select()) + row[0] + for row in self.connection.execute( + _select(self._version.c.version_num) + ) ) def _ensure_version_table(self, purge: bool = False) -> None: diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 51a8a5e2..2d33a186 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -4,7 +4,7 @@ Changelog ========== .. changelog:: - :version: 1.13.4 + :version: 1.14.0 :include_notes_from: unreleased .. changelog:: diff --git a/docs/build/unreleased/1560.rst b/docs/build/unreleased/1560.rst new file mode 100644 index 00000000..e808b307 --- /dev/null +++ b/docs/build/unreleased/1560.rst @@ -0,0 +1,12 @@ +.. change:: + :tags: usecase, runtime + :tickets: 1560 + + Added a new hook to the :class:`.DefaultImpl` + :meth:`.DefaultImpl.version_table_impl`. This allows third party dialects + to define the exact structure of the alembic_version table, to include use + cases where the table requires special directives and/or additional columns + so that it may function correctly on a particular backend. This is not + intended as a user-expansion hook, only a dialect implementation hook to + produce a working alembic_version table. Pull request courtesy Maciek + Bryński. diff --git a/tests/test_version_table.py b/tests/test_version_table.py index 5ad3c21d..ca569366 100644 --- a/tests/test_version_table.py +++ b/tests/test_version_table.py @@ -1,10 +1,15 @@ from sqlalchemy import Column from sqlalchemy import inspect +from sqlalchemy import Integer from sqlalchemy import MetaData +from sqlalchemy import PrimaryKeyConstraint from sqlalchemy import String from sqlalchemy import Table +from sqlalchemy.dialects import registry +from sqlalchemy.engine import default from alembic import migration +from alembic.ddl import impl from alembic.testing import assert_raises from alembic.testing import assert_raises_message from alembic.testing import config @@ -373,3 +378,44 @@ def test_delete_multi_match_no_sane_rowcount(self): self.connection.dialect, "supports_sane_rowcount", False ): self.updater.update_to_step(_down("a", None, True)) + + +registry.register("custom_version", __name__, "CustomVersionDialect") + + +class CustomVersionDialect(default.DefaultDialect): + name = "custom_version" + + +class CustomVersionTableImpl(impl.DefaultImpl): + __dialect__ = "custom_version" + + def version_table_impl( + self, + *, + version_table, + version_table_schema, + version_table_pk, + **kw, + ): + vt = Table( + version_table, + MetaData(), + Column("id", Integer, autoincrement=True), + Column("version_num", String(32), nullable=False), + schema=version_table_schema, + ) + if version_table_pk: + vt.append_constraint( + PrimaryKeyConstraint("id", name=f"{version_table}_pkc") + ) + return vt + + +class CustomVersionTableTest(TestMigrationContext): + + def test_custom_version_table(self): + context = migration.MigrationContext.configure( + dialect_name="custom_version", + ) + eq_(len(context._version.columns), 2) From 9332a0ebab197c7c96466cb5dee4e8705b31295f Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 4 Nov 2024 13:42:11 -0500 Subject: [PATCH 38/45] 1.14 Change-Id: Ie1070d8cafe93a94b77b9fa861ac8284c44f06af --- alembic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alembic/__init__.py b/alembic/__init__.py index f698f868..637b2d4e 100644 --- a/alembic/__init__.py +++ b/alembic/__init__.py @@ -1,4 +1,4 @@ from . import context from . import op -__version__ = "1.13.4" +__version__ = "1.14.0" From 0c70a0bb3f64aff5c6c729497f17b2bf63d882fa Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 4 Nov 2024 13:42:48 -0500 Subject: [PATCH 39/45] - 1.14.0 --- docs/build/changelog.rst | 15 ++++++++++++++- docs/build/conf.py | 4 ++-- docs/build/unreleased/1560.rst | 12 ------------ 3 files changed, 16 insertions(+), 15 deletions(-) delete mode 100644 docs/build/unreleased/1560.rst diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 2d33a186..da36ab9c 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -5,7 +5,20 @@ Changelog .. changelog:: :version: 1.14.0 - :include_notes_from: unreleased + :released: November 4, 2024 + + .. change:: + :tags: usecase, runtime + :tickets: 1560 + + Added a new hook to the :class:`.DefaultImpl` + :meth:`.DefaultImpl.version_table_impl`. This allows third party dialects + to define the exact structure of the alembic_version table, to include use + cases where the table requires special directives and/or additional columns + so that it may function correctly on a particular backend. This is not + intended as a user-expansion hook, only a dialect implementation hook to + produce a working alembic_version table. Pull request courtesy Maciek + Bryński. .. changelog:: :version: 1.13.3 diff --git a/docs/build/conf.py b/docs/build/conf.py index 915c690c..57c90325 100644 --- a/docs/build/conf.py +++ b/docs/build/conf.py @@ -99,8 +99,8 @@ # The short X.Y version. version = alembic.__version__ # The full version, including alpha/beta/rc tags. -release = "1.13.3" -release_date = "September 23, 2024" +release = "1.14.0" +release_date = "November 4, 2024" # The language for content autogenerated by Sphinx. Refer to documentation diff --git a/docs/build/unreleased/1560.rst b/docs/build/unreleased/1560.rst deleted file mode 100644 index e808b307..00000000 --- a/docs/build/unreleased/1560.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. change:: - :tags: usecase, runtime - :tickets: 1560 - - Added a new hook to the :class:`.DefaultImpl` - :meth:`.DefaultImpl.version_table_impl`. This allows third party dialects - to define the exact structure of the alembic_version table, to include use - cases where the table requires special directives and/or additional columns - so that it may function correctly on a particular backend. This is not - intended as a user-expansion hook, only a dialect implementation hook to - produce a working alembic_version table. Pull request courtesy Maciek - Bryński. From a5bb01fa28183d2052a42f2a19dae69e272a8bfc Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 4 Nov 2024 13:44:38 -0500 Subject: [PATCH 40/45] Version 1.14.1 placeholder --- alembic/__init__.py | 2 +- docs/build/changelog.rst | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/alembic/__init__.py b/alembic/__init__.py index 637b2d4e..243d3a8d 100644 --- a/alembic/__init__.py +++ b/alembic/__init__.py @@ -1,4 +1,4 @@ from . import context from . import op -__version__ = "1.14.0" +__version__ = "1.14.1" diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index da36ab9c..80450759 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -3,6 +3,10 @@ Changelog ========== +.. changelog:: + :version: 1.14.1 + :include_notes_from: unreleased + .. changelog:: :version: 1.14.0 :released: November 4, 2024 From 4f022ab57953f0ad398df850e76cd6de81ed8874 Mon Sep 17 00:00:00 2001 From: Peter Cock Date: Tue, 5 Nov 2024 18:29:26 +0000 Subject: [PATCH 41/45] Add missing imports to example (#1569) --- docs/build/naming.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/build/naming.rst b/docs/build/naming.rst index 55ccbde5..89296343 100644 --- a/docs/build/naming.rst +++ b/docs/build/naming.rst @@ -164,6 +164,8 @@ parameter in ``env.py``, which is normally configured when autogenerate is used:: # in your application's model: + from sqlalchemy import MetaData + from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): metadata = MetaData(naming_convention={ From b6b44c1e90c96b55939b28d73f427eff7b9cb620 Mon Sep 17 00:00:00 2001 From: A <146010486+gxxeel@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:29:48 +0300 Subject: [PATCH 42/45] Update tutorial.rst for ruff post_write_hooks (#1535) Due ruff package update - `ruff ` has been removed. Use `ruff check ` instead. --- docs/build/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build/tutorial.rst b/docs/build/tutorial.rst index 5fed9f4a..a5779d29 100644 --- a/docs/build/tutorial.rst +++ b/docs/build/tutorial.rst @@ -204,7 +204,7 @@ The file generated with the "generic" configuration looks like:: # hooks = ruff # ruff.type = exec # ruff.executable = %(here)s/.venv/bin/ruff - # ruff.options = --fix REVISION_SCRIPT_FILENAME + # ruff.options = check --fix REVISION_SCRIPT_FILENAME # Logging configuration [loggers] From 191c14a544bffcae43a516389ec7634afa524103 Mon Sep 17 00:00:00 2001 From: DanCardin Date: Wed, 6 Nov 2024 12:10:12 -0500 Subject: [PATCH 43/45] fix: Correct the AutogenContext.metadata typing to include Sequence[MetaData]. ### Description The type annotation for AutogenContext.metadata is currently `Optional[MetaData]`, but `target_metadata` is `Union[MetaData, Sequence[MetaData], None]`. Seems like setting `target_metadata` to `[]` directly translates into the list that `AutogenContext` receives, and that the code is already coercing the potential single/sequence to always be a list. My alembic plugin wasn't aware that this **could** be a list, and as such wasn't handling the possibility properly. ### Checklist This pull request is: - [x] A documentation / typographical error fix - Good to go, no issue or tests are needed - [ ] A short code fix - please include the issue number, and create an issue if none exists, which must include a complete example of the issue. one line code fixes without an issue and demonstration will not be accepted. - Please include: `Fixes: #` in the commit message - please include tests. one line code fixes without tests will not be accepted. - [ ] A new feature implementation - please include the issue number, and create an issue if none exists, which must include a complete example of how the feature would look. - Please include: `Fixes: #` in the commit message - please include tests. **Have a nice day!** Closes: #1547 Pull-request: https://github.com/sqlalchemy/alembic/pull/1547 Pull-request-sha: 1bdfa18739e3d156f267953c71267feeded9543b Change-Id: Ib3114b19c10983114b834676ada69d7475e82fe5 --- alembic/autogenerate/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alembic/autogenerate/api.py b/alembic/autogenerate/api.py index 4c039162..811462e8 100644 --- a/alembic/autogenerate/api.py +++ b/alembic/autogenerate/api.py @@ -277,7 +277,7 @@ class AutogenContext: """Maintains configuration and state that's specific to an autogenerate operation.""" - metadata: Optional[MetaData] = None + metadata: Union[MetaData, Sequence[MetaData], None] = None """The :class:`~sqlalchemy.schema.MetaData` object representing the destination. @@ -332,7 +332,7 @@ class AutogenContext: def __init__( self, migration_context: MigrationContext, - metadata: Optional[MetaData] = None, + metadata: Union[MetaData, Sequence[MetaData], None] = None, opts: Optional[Dict[str, Any]] = None, autogenerate: bool = True, ) -> None: From 2e95cec38ce3950c4b5cd4d7154bc1b2ba025532 Mon Sep 17 00:00:00 2001 From: Danipulok Date: Wed, 6 Nov 2024 12:06:50 -0500 Subject: [PATCH 44/45] fix(requirements): add `tzdata` to `tz` extras; fixes #1556 Add `tzdata` to `tz` extras, since `zoneinfo` on its own does not contain any timezones. Fixes: #1556 Closes: #1558 Pull-request: https://github.com/sqlalchemy/alembic/pull/1558 Pull-request-sha: 9c60d7c1a2d4171511828ae60de0649905297a17 Change-Id: Ibf6d863a7cd277c6abffcf2853f452a5b16c5fd5 --- alembic/templates/async/alembic.ini.mako | 2 +- alembic/templates/generic/alembic.ini.mako | 2 +- alembic/templates/multidb/alembic.ini.mako | 2 +- docs/build/tutorial.rst | 8 ++++---- docs/build/unreleased/1556.rst | 6 ++++++ setup.cfg | 1 + 6 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 docs/build/unreleased/1556.rst diff --git a/alembic/templates/async/alembic.ini.mako b/alembic/templates/async/alembic.ini.mako index 7eee9132..02ddb9e1 100644 --- a/alembic/templates/async/alembic.ini.mako +++ b/alembic/templates/async/alembic.ini.mako @@ -15,7 +15,7 @@ prepend_sys_path = . # timezone to use when rendering the date within the migration file # as well as the filename. -# If specified, requires the python>=3.9 or backports.zoneinfo library. +# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. # Any required deps can installed by adding `alembic[tz]` to the pip requirements # string value is passed to ZoneInfo() # leave blank for localtime diff --git a/alembic/templates/generic/alembic.ini.mako b/alembic/templates/generic/alembic.ini.mako index f1f76cae..fb709232 100644 --- a/alembic/templates/generic/alembic.ini.mako +++ b/alembic/templates/generic/alembic.ini.mako @@ -17,7 +17,7 @@ prepend_sys_path = . # timezone to use when rendering the date within the migration file # as well as the filename. -# If specified, requires the python>=3.9 or backports.zoneinfo library. +# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. # Any required deps can installed by adding `alembic[tz]` to the pip requirements # string value is passed to ZoneInfo() # leave blank for localtime diff --git a/alembic/templates/multidb/alembic.ini.mako b/alembic/templates/multidb/alembic.ini.mako index bf383ea1..687da570 100644 --- a/alembic/templates/multidb/alembic.ini.mako +++ b/alembic/templates/multidb/alembic.ini.mako @@ -17,7 +17,7 @@ prepend_sys_path = . # timezone to use when rendering the date within the migration file # as well as the filename. -# If specified, requires the python>=3.9 or backports.zoneinfo library. +# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. # Any required deps can installed by adding `alembic[tz]` to the pip requirements # string value is passed to ZoneInfo() # leave blank for localtime diff --git a/docs/build/tutorial.rst b/docs/build/tutorial.rst index a5779d29..347e856b 100644 --- a/docs/build/tutorial.rst +++ b/docs/build/tutorial.rst @@ -141,7 +141,7 @@ The file generated with the "generic" configuration looks like:: # timezone to use when rendering the date within the migration file # as well as the filename. - # If specified, requires the python>=3.9 or backports.zoneinfo library. + # If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. # Any required deps can installed by adding `alembic[tz]` to the pip requirements # string value is passed to ZoneInfo() # leave blank for localtime @@ -299,9 +299,9 @@ This file contains the following features: * ``timezone`` - an optional timezone name (e.g. ``UTC``, ``EST5EDT``, etc.) that will be applied to the timestamp which renders inside the migration file's comment as well as within the filename. This option requires Python>=3.9 - or installing the ``backports.zoneinfo`` library. If ``timezone`` is specified, - the create date object is no longer derived from ``datetime.datetime.now()`` - and is instead generated as:: + or installing the ``backports.zoneinfo`` library and the ``tzdata`` library. + If ``timezone`` is specified, the create date object is no longer derived + from ``datetime.datetime.now()`` and is instead generated as:: datetime.datetime.utcnow().replace( tzinfo=datetime.timezone.utc diff --git a/docs/build/unreleased/1556.rst b/docs/build/unreleased/1556.rst new file mode 100644 index 00000000..d0b96b13 --- /dev/null +++ b/docs/build/unreleased/1556.rst @@ -0,0 +1,6 @@ +.. change:: + :tags: bug, environment + :tickets: 1556 + + Added `tzdata` to `tz` extras, which is required on some platforms such as + Windows. Pull request courtesy Danipulok. diff --git a/setup.cfg b/setup.cfg index e3a51f25..89c095f7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,7 @@ install_requires = [options.extras_require] tz = backports.zoneinfo;python_version<"3.9" + tzdata [options.package_data] alembic = *.pyi, py.typed From f826aada4c605ac3bb52e85add7718477c0751a2 Mon Sep 17 00:00:00 2001 From: Aaron Griffin Date: Wed, 6 Nov 2024 15:40:56 -0500 Subject: [PATCH 45/45] Improve write_pyi usage Fixes #1524 ### Description Due to some problems when running write_pyi, this includes the following changes: 1. move the sys.path.append up before alembic is imported - this is using global imports otherwise 2. use tox to run write_pyi in order to avoid globally installed dependencies ### Checklist This pull request is: - [ ] A documentation / typographical error fix - Good to go, no issue or tests are needed - [x] A short code fix - please include the issue number, and create an issue if none exists, which must include a complete example of the issue. one line code fixes without an issue and demonstration will not be accepted. - Please include: `Fixes: #` in the commit message - please include tests. one line code fixes without tests will not be accepted. - [ ] A new feature implementation - please include the issue number, and create an issue if none exists, which must include a complete example of how the feature would look. - Please include: `Fixes: #` in the commit message - please include tests. **Have a nice day!** Closes: #1525 Pull-request: https://github.com/sqlalchemy/alembic/pull/1525 Pull-request-sha: 679ea5c67ebbf5062891d50dabf6323cf5348d82 Change-Id: I2d837a5dfe3cb252a0f0a6937310741045103da6 --- tools/write_pyi.py | 4 ++-- tox.ini | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tools/write_pyi.py b/tools/write_pyi.py index 363d727e..b2ad2300 100644 --- a/tools/write_pyi.py +++ b/tools/write_pyi.py @@ -11,12 +11,12 @@ import textwrap import typing +sys.path.append(str(Path(__file__).parent.parent)) + from alembic.autogenerate.api import AutogenContext from alembic.ddl.impl import DefaultImpl from alembic.runtime.migration import MigrationInfo -sys.path.append(str(Path(__file__).parent.parent)) - if True: # avoid flake/zimports messing with the order from alembic.operations.base import BatchOperations from alembic.operations.base import Operations diff --git a/tox.ini b/tox.ini index f1bfcac3..77439be3 100644 --- a/tox.ini +++ b/tox.ini @@ -101,3 +101,12 @@ deps= commands = flake8 ./alembic/ ./tests/ setup.py docs/build/conf.py {posargs} black --check setup.py tests alembic + +[testenv:write_pyi] +basepython = python3 +deps= + sqlalchemy>=2 + mako + zimports + black==24.1.1 +commands = python tools/write_pyi.py