run against SQLite with "backend" tests also running against a PostgreSQL
database::
- tox -e py36-sqlite-postgresql
+ tox -e py38-sqlite-postgresql
Or to run just "backend" tests (NOTE: Alembic has no tests marked this
way so this option is not important) against a MySQL databases::
- tox -e py36-mysql-backendonly
+ tox -e py38-mysql-backendonly
Running against backends other than SQLite requires that a database of that
vendor be available at a specific URL. See "Setting Up Databases" below
from .runtime import environment
from .runtime import migration
-__version__ = "1.4.4"
+__version__ = "1.5.0"
sys.modules["alembic.migration"] = migration
sys.modules["alembic.environment"] = environment
metadata_col,
):
- if not sqla_compat._dialect_supports_comments(autogen_context.dialect):
+ if not autogen_context.dialect.supports_comments:
return
metadata_comment = metadata_col.comment
metadata_table,
):
- if not sqla_compat._dialect_supports_comments(autogen_context.dialect):
+ if not autogen_context.dialect.supports_comments:
return
# if we're doing CREATE TABLE, comments will be created inline
if op.schema:
text += ",\nschema=%r" % _ident(op.schema)
- comment = sqla_compat._comment_attribute(table)
+ comment = table.comment
if comment:
text += ",\ncomment=%r" % _ident(comment)
for k in sorted(op.kw):
if column.system:
opts.append(("system", column.system))
- comment = sqla_compat._comment_attribute(column)
+ comment = column.comment
if comment:
opts.append(("comment", "%r" % comment))
config_args=util.immutabledict(),
attributes=None,
):
- """Construct a new :class:`.Config`
-
- """
+ """Construct a new :class:`.Config`"""
self.config_file_name = file_
self.config_ini_section = ini_section
self.output_buffer = output_buffer
self.file_config.set(section, name, value)
def get_section_option(self, section, name, default=None):
- """Return an option from the given section of the .ini file.
-
- """
+ """Return an option from the given section of the .ini file."""
if not self.file_config.has_section(section):
raise util.CommandError(
"No config file %r found, or file has no "
self._exec(schema.CreateIndex(index))
with_comment = (
- sqla_compat._dialect_supports_comments(self.dialect)
- and not self.dialect.inline_comments
+ self.dialect.supports_comments and not self.dialect.inline_comments
)
- comment = sqla_compat._comment_attribute(table)
+ comment = table.comment
if comment and with_comment:
self.create_table_comment(table)
for column in table.columns:
- comment = sqla_compat._comment_attribute(column)
+ comment = column.comment
if comment and with_comment:
self.create_column_comment(column)
inspector_params = self._tokenize_column_type(inspector_column)
metadata_params = self._tokenize_column_type(metadata_column)
- if not self._column_types_match(inspector_params, metadata_params,):
+ if not self._column_types_match(inspector_params, metadata_params):
return True
if not self._column_args_match(inspector_params, metadata_params):
return True
@Operations.register_operation("create_table_comment")
class CreateTableCommentOp(AlterTableOp):
- """Represent a COMMENT ON `table` operation.
- """
+ """Represent a COMMENT ON `table` operation."""
def __init__(
self, table_name, comment, schema=None, existing_comment=None
return operations.invoke(op)
def reverse(self):
- """Reverses the COMMENT ON operation against a table.
- """
+ """Reverses the COMMENT ON operation against a table."""
if self.existing_comment is None:
return DropTableCommentOp(
self.table_name,
@Operations.register_operation("drop_table_comment")
class DropTableCommentOp(AlterTableOp):
- """Represent an operation to remove the comment from a table.
- """
+ """Represent an operation to remove the comment from a table."""
def __init__(self, table_name, schema=None, existing_comment=None):
self.table_name = table_name
return operations.invoke(op)
def reverse(self):
- """Reverses the COMMENT ON operation against a table.
- """
+ """Reverses the COMMENT ON operation against a table."""
return CreateTableCommentOp(
self.table_name, self.existing_comment, schema=self.schema
)
.. versionadded:: 0.6.4
- """
+ """
op = cls(table, rows, multiinsert=multiinsert)
operations.invoke(op)
from . import ops
from .base import Operations
-from ..util import sqla_compat
@Operations.implementation_for(ops.AlterColumnOp)
operations.impl.create_index(index)
with_comment = (
- sqla_compat._dialect_supports_comments(operations.impl.dialect)
+ operations.impl.dialect.supports_comments
and not operations.impl.dialect.inline_comments
)
- comment = sqla_compat._comment_attribute(column)
+ comment = column.comment
if comment and with_comment:
operations.impl.create_column_comment(column)
def _run_hooks(path, hook_config):
- """Invoke hooks for a generated revision.
-
- """
+ """Invoke hooks for a generated revision."""
from .base import _split_on_space_comma
# the type / server default compare logic might not work on older
# SQLAlchemy versions as seems to be the case for SQLAlchemy 1.1 on Oracle
- __requires__ = ("alter_column", "sqlalchemy_12")
+ __requires__ = ("alter_column",)
def setUp(self):
self.conn = config.db.connect()
def reflects_fk_options(self):
return exclusions.closed()
- @property
- def sqlalchemy_issue_3740(self):
- """Fixes percent sign escaping for paramstyles that don't require it"""
- return exclusions.skip_if(
- lambda config: not util.sqla_120,
- "SQLAlchemy 1.2 or greater required",
- )
-
- @property
- def sqlalchemy_12(self):
- return exclusions.skip_if(
- lambda config: not util.sqla_1216,
- "SQLAlchemy 1.2.16 or greater required",
- )
-
@property
def sqlalchemy_13(self):
return exclusions.skip_if(
"SQLAlchemy 1.4 or greater required",
)
- @property
- def sqlalchemy_1115(self):
- return exclusions.skip_if(
- lambda config: not util.sqla_1115,
- "SQLAlchemy 1.1.15 or greater required",
- )
-
- @property
- def sqlalchemy_110(self):
- return exclusions.skip_if(
- lambda config: not util.sqla_110,
- "SQLAlchemy 1.1.0 or greater required",
- )
-
- @property
- def sqlalchemy_issue_4436(self):
- def check(config):
- vers = sqla_compat._vers
-
- if vers == (1, 3, 0, "b1"):
- return True
- elif vers >= (1, 2, 16):
- return False
- else:
- return True
-
- return exclusions.skip_if(
- check, "SQLAlchemy 1.2.16, 1.3.0b2 or greater required"
- )
-
@property
def python3(self):
return exclusions.skip_if(
@property
def comments(self):
return exclusions.only_if(
- lambda config: sqla_compat._dialect_supports_comments(
- config.db.dialect
- )
+ lambda config: config.db.dialect.supports_comments
)
- @property
- def comments_api(self):
- return exclusions.only_if(lambda config: util.sqla_120)
-
@property
def alter_column(self):
return exclusions.open()
from .pyfiles import pyc_file_from_path # noqa
from .pyfiles import template_to_file # noqa
from .sqla_compat import has_computed # noqa
-from .sqla_compat import sqla_110 # noqa
-from .sqla_compat import sqla_1115 # noqa
-from .sqla_compat import sqla_120 # noqa
-from .sqla_compat import sqla_1216 # noqa
from .sqla_compat import sqla_13 # noqa
from .sqla_compat import sqla_14 # noqa
-if not sqla_110:
- raise CommandError("SQLAlchemy 1.1.0 or greater is required. ")
+if not sqla_13:
+ raise CommandError("SQLAlchemy 1.3.0 or greater is required.")
import io
import sys
-py27 = sys.version_info >= (2, 7)
py2k = sys.version_info.major < 3
py3k = sys.version_info.major >= 3
-py35 = sys.version_info >= (3, 5)
py36 = sys.version_info >= (3, 6)
else:
import collections as collections_abc # noqa
-if py35:
+if py3k:
def _formatannotation(annotation, base_module=None):
- """vendored from python 3.7
- """
+ """vendored from python 3.7"""
if getattr(annotation, "__module__", None) == "typing":
return repr(annotation).replace("typing.", "")
if py2k:
from mako.util import parse_encoding
-if py35:
- import importlib.util
+if py3k:
import importlib.machinery
+ import importlib.util
+
def load_module_py(module_id, path):
spec = importlib.util.spec_from_file_location(module_id, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
-
-elif py3k:
- import importlib.machinery
-
- def load_module_py(module_id, path):
- module = importlib.machinery.SourceFileLoader(
- module_id, path
- ).load_module(module_id)
- del sys.modules[module_id]
- return module
-
- def load_module_pyc(module_id, path):
- module = importlib.machinery.SourcelessFileLoader(
- module_id, path
- ).load_module(module_id)
- del sys.modules[module_id]
- return module
-
-
-if py3k:
-
def get_bytecode_suffixes():
try:
return importlib.machinery.BYTECODE_SUFFIXES
return importlib.machinery.DEBUG_BYTECODE_SUFFIXES
def get_current_bytecode_suffixes():
- if py35:
+ if py3k:
suffixes = importlib.machinery.BYTECODE_SUFFIXES
else:
if sys.flags.optimize:
return suffixes
def has_pep3147():
-
- if py35:
- return True
- else:
- # TODO: not sure if we are supporting old versions of Python
- # the import here emits a deprecation warning which the test
- # suite only catches if imp wasn't imported alreadt
- # http://www.python.org/dev/peps/pep-3147/#detecting-pep-3147-availability
- import imp
-
- return hasattr(imp, "get_tag")
+ return True
else:
from .compat import binary_type
from .compat import collections_abc
-from .compat import py27
from .compat import string_types
log = logging.getLogger(__name__)
-if py27:
- # disable "no handler found" errors
- logging.getLogger("alembic").addHandler(logging.NullHandler())
+# disable "no handler found" errors
+logging.getLogger("alembic").addHandler(logging.NullHandler())
try:
from .compat import has_pep3147
from .compat import load_module_py
from .compat import load_module_pyc
-from .compat import py35
+from .compat import py3k
from .exc import CommandError
def pyc_file_from_path(path):
- """Given a python source path, locate the .pyc.
-
- """
+ """Given a python source path, locate the .pyc."""
if has_pep3147():
- if py35:
+ if py3k:
import importlib
candidate = importlib.util.cache_from_source(path)
_vers = tuple(
[_safe_int(x) for x in re.findall(r"(\d+|[abc]\d)", __version__)]
)
-sqla_110 = _vers >= (1, 1, 0)
-sqla_1115 = _vers >= (1, 1, 15)
-sqla_120 = _vers >= (1, 2, 0)
-sqla_1216 = _vers >= (1, 2, 16)
sqla_13 = _vers >= (1, 3)
sqla_14 = _vers >= (1, 4)
try:
return constraint.name is not None
-def _dialect_supports_comments(dialect):
- if sqla_120:
- return dialect.supports_comments
- else:
- return False
-
-
-def _comment_attribute(obj):
- """return the .comment attribute from a Table or Column"""
-
- if sqla_120:
- return obj.comment
- else:
- return None
-
-
def _is_mariadb(mysql_dialect):
if sqla_14:
return mysql_dialect.is_mariadb
==========
.. changelog::
- :version: 1.4.4
+ :version: 1.5.0
:include_notes_from: unreleased
.. changelog::
Alembic's install process will ensure that SQLAlchemy_
is installed, in addition to other dependencies. Alembic will work with
-SQLAlchemy as of version **0.9.0**, however more features are available with
-newer versions such as the 1.1 or 1.2 series.
+SQLAlchemy as of version **1.3.0**.
-.. versionchanged:: 1.0.0 Support for SQLAlchemy 0.8 and 0.7.9 was dropped.
+.. versionchanged:: 1.5.0 Support for SQLAlchemy older than 1.3.0 was dropped.
-Alembic supports Python versions 2.7, 3.5 and above.
+Alembic supports Python versions 2.7, 3.6 and above.
-.. versionchanged:: 1.0.0 Support for Python 2.6 and 3.3 was dropped.
-
-.. versionchanged:: 1.1.1 Support for Python 3.4 was dropped.
+.. versionchanged:: 1.5.0 Support for Python 3.5 was dropped.
Community
=========
--- /dev/null
+.. change::
+ :tags: change
+ :tickets: 711
+
+ Alembic 1.5.0 now supports **Python 2.7 and Python 3.6 and above**, as well
+ as **SQLAlchemy 1.3.0 and above**. Support is removed for Python 3
+ versions prior to 3.6 and SQLAlchemy versions prior to the 1.3 series.
readme = os.path.join(os.path.dirname(__file__), "README.rst")
requires = [
- "SQLAlchemy>=1.1.0",
+ "SQLAlchemy>=1.3.0",
"Mako",
"python-editor>=0.3",
"python-dateutil",
version=VERSION,
description="A database migration tool for SQLAlchemy.",
long_description=open(readme).read(),
- python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
+ python_requires=(
+ ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+ ),
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Database :: Front-Ends",
self._mysql_and_check_constraints_exist,
)
- @property
- def mysql_check_reflection_or_none(self):
- # succeed if:
- # 1. SQLAlchemy does not reflect CHECK constraints
- # 2. SQLAlchemy does reflect CHECK constraints, but MySQL does not.
- def go(config):
- return (
- not self._mysql_check_constraints_exist(config)
- or self.sqlalchemy_1115.enabled
- )
-
- return exclusions.succeeds_if(go)
-
- @property
- def mysql_timestamp_reflection(self):
- def go(config):
- return (
- not self._mariadb_102(config) or self.sqlalchemy_1115.enabled
- )
-
- return exclusions.only_if(go)
-
- def _mariadb_102(self, config):
- return (
- exclusions.against(config, ["mysql", "mariadb"])
- and sqla_compat._is_mariadb(config.db.dialect)
- and sqla_compat._mariadb_normalized_version_info(config.db.dialect)
- > (10, 2)
- )
-
def mysql_check_col_name_change(self, config):
# MySQL has check constraints that enforce an reflect, however
# they prevent a column's name from being changed due to a bug in
return norm_version_info >= (8, 0, 16)
else:
return False
-
- def _mysql_check_constraints_exist(self, config):
- # 1. we dont have mysql / mariadb or
- # 2. we have mysql / mariadb that enforces check constraints
- return not exclusions.against(
- config, ["mysql", "mariadb"]
- ) or self._mysql_and_check_constraints_exist(config)
-
- def _mysql_check_constraints_dont_exist(self, config):
- # 1. we have mysql / mariadb and
- # 2. they dont enforce check constraints
- return not self._mysql_check_constraints_exist(config)
-
- def _mysql_not_mariadb_102(self, config):
- return exclusions.against(config, ["mysql", "mariadb"]) and (
- not sqla_compat._is_mariadb(config.db.dialect)
- or sqla_compat._mariadb_normalized_version_info(config.db.dialect)
- < (10, 2)
- )
"nullable=False)",
)
- @config.requirements.comments_api
def test_render_col_with_comment(self):
c = Column("some_key", Integer, comment="This is a comment")
Table("some_table", MetaData(), c)
"comment='This is a comment')",
)
- @config.requirements.comments_api
def test_render_col_comment_with_quote(self):
c = Column("some_key", Integer, comment="This is a john's comment")
Table("some_table", MetaData(), c)
op_obj,
)
- @config.requirements.comments_api
def test_render_alter_column_modify_comment(self):
op_obj = ops.AlterColumnOp(
"sometable", "somecolumn", modify_comment="This is a comment"
"comment='This is a comment')",
)
- @config.requirements.comments_api
def test_render_alter_column_existing_comment(self):
op_obj = ops.AlterColumnOp(
"sometable", "somecolumn", existing_comment="This is a comment"
"existing_comment='This is a comment')",
)
- @config.requirements.comments_api
def test_render_col_drop_comment(self):
op_obj = ops.AlterColumnOp(
"sometable",
"existing_comment='This is a comment')",
)
- @config.requirements.comments_api
def test_render_table_with_comment(self):
m = MetaData()
t = Table(
")",
)
- @config.requirements.comments_api
def test_render_add_column_with_comment(self):
op_obj = ops.AddColumnOp(
"foo", Column("x", Integer, comment="This is a Column")
"nullable=True, comment='This is a Column'))",
)
- @config.requirements.comments_api
def test_render_create_table_comment_op(self):
op_obj = ops.CreateTableCommentOp("table_name", "comment")
eq_ignore_whitespace(
")",
)
- @config.requirements.comments_api
def test_render_create_table_comment_with_quote_op(self):
op_obj = ops.CreateTableCommentOp(
"table_name",
[(datetime.datetime(2012, 5, 18, 15, 32, 5),)],
)
- @config.requirements.sqlalchemy_12
def test_no_net_change_timestamp_w_default(self):
t = self._timestamp_w_expr_default_fixture()
def test_rename_column_boolean(self):
super(BatchRoundTripMySQLTest, self).test_rename_column_boolean()
- @config.requirements.mysql_check_reflection_or_none
def test_change_type_boolean_to_int(self):
super(BatchRoundTripMySQLTest, self).test_change_type_boolean_to_int()
- @config.requirements.mysql_check_reflection_or_none
def test_change_type_int_to_boolean(self):
super(BatchRoundTripMySQLTest, self).test_change_type_int_to_boolean()
ctx = MigrationContext(ctx.dialect, None, {})
is_(ctx.config, None)
- @config.requirements.sqlalchemy_issue_3740
def test_sql_mode_parameters(self):
env = self._fixture()
class MySQLOpTest(TestBase):
- @config.requirements.comments_api
def test_create_table_with_comment(self):
context = op_fixture("mysql")
op.create_table(
)
context.assert_contains("COMMENT='This is a table comment'")
- @config.requirements.comments_api
def test_create_table_with_column_comments(self):
context = op_fixture("mysql")
op.create_table(
"COMMENT='This is a table comment'"
)
- @config.requirements.comments_api
def test_add_column_with_comment(self):
context = op_fixture("mysql")
op.add_column("t", Column("q", Integer, comment="This is a comment"))
server_default="q",
)
- @config.requirements.comments_api
def test_alter_column_add_comment(self):
context = op_fixture("mysql")
op.alter_column(
"COMMENT 'This is a column comment'"
)
- @config.requirements.comments_api
def test_alter_column_add_comment_quoting(self):
context = op_fixture("mysql")
op.alter_column(
"COMMENT 'This is a ''column'' comment'"
)
- @config.requirements.comments_api
def test_alter_column_drop_comment(self):
context = op_fixture("mysql")
op.alter_column(
context.assert_("ALTER TABLE foo.t MODIFY c BOOL NULL")
- @config.requirements.comments_api
def test_alter_column_existing_comment(self):
context = op_fixture("mysql")
op.alter_column(
"COMMENT 'existing column comment'"
)
- @config.requirements.comments_api
def test_rename_column_existing_comment(self):
context = op_fixture("mysql")
op.alter_column(
"COMMENT 'existing column comment'"
)
- @config.requirements.comments_api
def test_alter_column_new_comment_replaces_existing(self):
context = op_fixture("mysql")
op.alter_column(
"COMMENT 'This is a column comment'"
)
- @config.requirements.comments_api
def test_create_table_comment(self):
# this is handled by SQLAlchemy's compilers
context = op_fixture("mysql")
op.create_table_comment("t2", comment="t2 table", schema="foo")
context.assert_("ALTER TABLE foo.t2 COMMENT 't2 table'")
- @config.requirements.comments_api
- @config.requirements.sqlalchemy_issue_4436
def test_drop_table_comment(self):
# this is handled by SQLAlchemy's compilers
context = op_fixture("mysql")
__only_on__ = "mysql"
__backend__ = True
- __requires__ = ("mysql_timestamp_reflection",)
-
@classmethod
def setup_class(cls):
cls.bind = config.db
"ALTER TABLE t MODIFY c INTEGER", "COMMENT ON COLUMN t.c IS ''"
)
- @config.requirements.comments_api
def test_create_table_comment(self):
# this is handled by SQLAlchemy's compilers
context = op_fixture("oracle")
op.create_table_comment("t2", comment="t2 table", schema="foo")
context.assert_("COMMENT ON TABLE foo.t2 IS 't2 table'")
- @config.requirements.comments_api
- @config.requirements.sqlalchemy_issue_4436
def test_drop_table_comment(self):
# this is handled by SQLAlchemy's compilers
context = op_fixture("oracle")
'USING gist ("SomeColumn" WITH >) WHERE ("SomeColumn" > 5)'
)
- @config.requirements.comments_api
def test_add_column_with_comment(self):
context = op_fixture("postgresql")
op.add_column("t", Column("q", Integer, comment="This is a comment"))
"COMMENT ON COLUMN t.q IS 'This is a comment'",
)
- @config.requirements.comments_api
def test_alter_column_with_comment(self):
context = op_fixture("postgresql")
op.alter_column(
"COMMENT ON COLUMN foo.t.c IS 'This is a column comment'",
)
- @config.requirements.comments_api
def test_alter_column_add_comment(self):
context = op_fixture("postgresql")
op.alter_column(
"COMMENT ON COLUMN foo.t.c IS 'This is a column comment'"
)
- @config.requirements.comments_api
def test_alter_column_add_comment_table_and_column_quoting(self):
context = op_fixture("postgresql")
op.alter_column(
'COMMENT ON COLUMN foo."T"."C" IS \'This is a column comment\''
)
- @config.requirements.comments_api
def test_alter_column_add_comment_quoting(self):
context = op_fixture("postgresql")
op.alter_column(
"COMMENT ON COLUMN foo.t.c IS 'This is a column ''comment'''"
)
- @config.requirements.comments_api
def test_alter_column_drop_comment(self):
context = op_fixture("postgresql")
op.alter_column(
context.assert_("COMMENT ON COLUMN foo.t.c IS NULL")
- @config.requirements.comments_api
def test_create_table_with_comment(self):
context = op_fixture("postgresql")
op.create_table(
"COMMENT ON TABLE t2 IS 't2 comment'",
)
- @config.requirements.comments_api
def test_create_table_with_column_comments(self):
context = op_fixture("postgresql")
op.create_table(
"COMMENT ON COLUMN t2.c2 IS 'c2 comment'",
)
- @config.requirements.comments_api
def test_create_table_comment(self):
# this is handled by SQLAlchemy's compilers
context = op_fixture("postgresql")
op.create_table_comment("t2", comment="t2 table", schema="foo")
context.assert_("COMMENT ON TABLE foo.t2 IS 't2 table'")
- @config.requirements.comments_api
def test_drop_table_comment(self):
# this is handled by SQLAlchemy's compilers
context = op_fixture("postgresql")
None, col, rendered, cols[0]["default"]
)
- @config.requirements.sqlalchemy_12
def test_compare_current_timestamp_func(self):
self._compare_default_roundtrip(
DateTime(), func.datetime("now", "localtime")
)
- @config.requirements.sqlalchemy_12
def test_compare_current_timestamp_func_now(self):
self._compare_default_roundtrip(DateTime(), func.now())
class NotQuiteTwinMergeTest(MigrationTest):
- """Test a variant of #297.
-
- """
+ """Test a variant of #297."""
@classmethod
def setup_class(cls):
deps=pytest>4.6
pytest-xdist
mock
- sqla11: {[tox]SQLA_REPO}@rel_1_1
- sqla12: {[tox]SQLA_REPO}@rel_1_2
sqla13: {[tox]SQLA_REPO}@rel_1_3
sqlamaster: {[tox]SQLA_REPO}@master
postgresql: psycopg2