alembic 1.15.1__py3-none-any.whl → 1.16.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alembic/__init__.py +1 -1
- alembic/autogenerate/compare.py +60 -7
- alembic/autogenerate/render.py +28 -4
- alembic/command.py +112 -37
- alembic/config.py +574 -222
- alembic/ddl/base.py +36 -8
- alembic/ddl/impl.py +24 -7
- alembic/ddl/mssql.py +3 -1
- alembic/ddl/mysql.py +8 -4
- alembic/ddl/postgresql.py +6 -2
- alembic/ddl/sqlite.py +1 -1
- alembic/op.pyi +25 -6
- alembic/operations/base.py +21 -4
- alembic/operations/ops.py +53 -10
- alembic/operations/toimpl.py +20 -3
- alembic/script/base.py +123 -136
- alembic/script/revision.py +1 -1
- alembic/script/write_hooks.py +20 -21
- alembic/templates/async/alembic.ini.mako +40 -16
- alembic/templates/generic/alembic.ini.mako +39 -17
- alembic/templates/multidb/alembic.ini.mako +42 -17
- alembic/templates/pyproject/README +1 -0
- alembic/templates/pyproject/alembic.ini.mako +44 -0
- alembic/templates/pyproject/env.py +78 -0
- alembic/templates/pyproject/pyproject.toml.mako +76 -0
- alembic/templates/pyproject/script.py.mako +28 -0
- alembic/testing/__init__.py +2 -0
- alembic/testing/assertions.py +4 -0
- alembic/testing/env.py +56 -1
- alembic/testing/fixtures.py +28 -1
- alembic/testing/suite/_autogen_fixtures.py +113 -0
- alembic/util/__init__.py +1 -0
- alembic/util/compat.py +56 -0
- alembic/util/messaging.py +4 -0
- alembic/util/pyfiles.py +56 -19
- {alembic-1.15.1.dist-info → alembic-1.16.0.dist-info}/METADATA +5 -4
- {alembic-1.15.1.dist-info → alembic-1.16.0.dist-info}/RECORD +41 -36
- {alembic-1.15.1.dist-info → alembic-1.16.0.dist-info}/WHEEL +1 -1
- {alembic-1.15.1.dist-info → alembic-1.16.0.dist-info}/entry_points.txt +0 -0
- {alembic-1.15.1.dist-info → alembic-1.16.0.dist-info/licenses}/LICENSE +0 -0
- {alembic-1.15.1.dist-info → alembic-1.16.0.dist-info}/top_level.txt +0 -0
alembic/__init__.py
CHANGED
alembic/autogenerate/compare.py
CHANGED
@@ -24,6 +24,7 @@ from sqlalchemy import schema as sa_schema
|
|
24
24
|
from sqlalchemy import text
|
25
25
|
from sqlalchemy import types as sqltypes
|
26
26
|
from sqlalchemy.sql import expression
|
27
|
+
from sqlalchemy.sql.elements import conv
|
27
28
|
from sqlalchemy.sql.schema import ForeignKeyConstraint
|
28
29
|
from sqlalchemy.sql.schema import Index
|
29
30
|
from sqlalchemy.sql.schema import UniqueConstraint
|
@@ -216,7 +217,7 @@ def _compare_tables(
|
|
216
217
|
(inspector),
|
217
218
|
# fmt: on
|
218
219
|
)
|
219
|
-
inspector.reflect_table(t, include_columns=None)
|
220
|
+
_InspectorConv(inspector).reflect_table(t, include_columns=None)
|
220
221
|
if autogen_context.run_object_filters(t, tname, "table", True, None):
|
221
222
|
modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
|
222
223
|
|
@@ -246,7 +247,8 @@ def _compare_tables(
|
|
246
247
|
_compat_autogen_column_reflect(inspector),
|
247
248
|
# fmt: on
|
248
249
|
)
|
249
|
-
inspector.reflect_table(t, include_columns=None)
|
250
|
+
_InspectorConv(inspector).reflect_table(t, include_columns=None)
|
251
|
+
|
250
252
|
conn_column_info[(s, tname)] = t
|
251
253
|
|
252
254
|
for s, tname in sorted(existing_tables, key=lambda x: (x[0] or "", x[1])):
|
@@ -438,6 +440,55 @@ def _compare_columns(
|
|
438
440
|
_C = TypeVar("_C", bound=Union[UniqueConstraint, ForeignKeyConstraint, Index])
|
439
441
|
|
440
442
|
|
443
|
+
class _InspectorConv:
|
444
|
+
__slots__ = ("inspector",)
|
445
|
+
|
446
|
+
def __init__(self, inspector):
|
447
|
+
self.inspector = inspector
|
448
|
+
|
449
|
+
def _apply_reflectinfo_conv(self, consts):
|
450
|
+
if not consts:
|
451
|
+
return consts
|
452
|
+
for const in consts:
|
453
|
+
if const["name"] is not None and not isinstance(
|
454
|
+
const["name"], conv
|
455
|
+
):
|
456
|
+
const["name"] = conv(const["name"])
|
457
|
+
return consts
|
458
|
+
|
459
|
+
def _apply_constraint_conv(self, consts):
|
460
|
+
if not consts:
|
461
|
+
return consts
|
462
|
+
for const in consts:
|
463
|
+
if const.name is not None and not isinstance(const.name, conv):
|
464
|
+
const.name = conv(const.name)
|
465
|
+
return consts
|
466
|
+
|
467
|
+
def get_indexes(self, *args, **kw):
|
468
|
+
return self._apply_reflectinfo_conv(
|
469
|
+
self.inspector.get_indexes(*args, **kw)
|
470
|
+
)
|
471
|
+
|
472
|
+
def get_unique_constraints(self, *args, **kw):
|
473
|
+
return self._apply_reflectinfo_conv(
|
474
|
+
self.inspector.get_unique_constraints(*args, **kw)
|
475
|
+
)
|
476
|
+
|
477
|
+
def get_foreign_keys(self, *args, **kw):
|
478
|
+
return self._apply_reflectinfo_conv(
|
479
|
+
self.inspector.get_foreign_keys(*args, **kw)
|
480
|
+
)
|
481
|
+
|
482
|
+
def reflect_table(self, table, *, include_columns):
|
483
|
+
self.inspector.reflect_table(table, include_columns=include_columns)
|
484
|
+
|
485
|
+
# I had a cool version of this using _ReflectInfo, however that doesn't
|
486
|
+
# work in 1.4 and it's not public API in 2.x. Then this is just a two
|
487
|
+
# liner. So there's no competition...
|
488
|
+
self._apply_constraint_conv(table.constraints)
|
489
|
+
self._apply_constraint_conv(table.indexes)
|
490
|
+
|
491
|
+
|
441
492
|
@comparators.dispatch_for("table")
|
442
493
|
def _compare_indexes_and_uniques(
|
443
494
|
autogen_context: AutogenContext,
|
@@ -473,9 +524,10 @@ def _compare_indexes_and_uniques(
|
|
473
524
|
if conn_table is not None:
|
474
525
|
# 1b. ... and from connection, if the table exists
|
475
526
|
try:
|
476
|
-
conn_uniques = inspector.get_unique_constraints(
|
527
|
+
conn_uniques = _InspectorConv(inspector).get_unique_constraints(
|
477
528
|
tname, schema=schema
|
478
529
|
)
|
530
|
+
|
479
531
|
supports_unique_constraints = True
|
480
532
|
except NotImplementedError:
|
481
533
|
pass
|
@@ -498,7 +550,7 @@ def _compare_indexes_and_uniques(
|
|
498
550
|
if uq.get("duplicates_index"):
|
499
551
|
unique_constraints_duplicate_unique_indexes = True
|
500
552
|
try:
|
501
|
-
conn_indexes = inspector.get_indexes(
|
553
|
+
conn_indexes = _InspectorConv(inspector).get_indexes(
|
502
554
|
tname, schema=schema
|
503
555
|
)
|
504
556
|
except NotImplementedError:
|
@@ -1178,7 +1230,9 @@ def _compare_foreign_keys(
|
|
1178
1230
|
|
1179
1231
|
conn_fks_list = [
|
1180
1232
|
fk
|
1181
|
-
for fk in inspector.get_foreign_keys(
|
1233
|
+
for fk in _InspectorConv(inspector).get_foreign_keys(
|
1234
|
+
tname, schema=schema
|
1235
|
+
)
|
1182
1236
|
if autogen_context.run_name_filters(
|
1183
1237
|
fk["name"],
|
1184
1238
|
"foreign_key_constraint",
|
@@ -1187,8 +1241,7 @@ def _compare_foreign_keys(
|
|
1187
1241
|
]
|
1188
1242
|
|
1189
1243
|
conn_fks = {
|
1190
|
-
_make_foreign_key(const, conn_table)
|
1191
|
-
for const in conn_fks_list
|
1244
|
+
_make_foreign_key(const, conn_table) for const in conn_fks_list
|
1192
1245
|
}
|
1193
1246
|
|
1194
1247
|
impl = autogen_context.migration_context.impl
|
alembic/autogenerate/render.py
CHANGED
@@ -442,7 +442,7 @@ def _drop_constraint(
|
|
442
442
|
name = _render_gen_name(autogen_context, op.constraint_name)
|
443
443
|
schema = _ident(op.schema) if op.schema else None
|
444
444
|
type_ = _ident(op.constraint_type) if op.constraint_type else None
|
445
|
-
|
445
|
+
if_exists = op.if_exists
|
446
446
|
params_strs = []
|
447
447
|
params_strs.append(repr(name))
|
448
448
|
if not autogen_context._has_batch:
|
@@ -451,32 +451,47 @@ def _drop_constraint(
|
|
451
451
|
params_strs.append(f"schema={schema!r}")
|
452
452
|
if type_ is not None:
|
453
453
|
params_strs.append(f"type_={type_!r}")
|
454
|
+
if if_exists is not None:
|
455
|
+
params_strs.append(f"if_exists={if_exists}")
|
454
456
|
|
455
457
|
return f"{prefix}drop_constraint({', '.join(params_strs)})"
|
456
458
|
|
457
459
|
|
458
460
|
@renderers.dispatch_for(ops.AddColumnOp)
|
459
461
|
def _add_column(autogen_context: AutogenContext, op: ops.AddColumnOp) -> str:
|
460
|
-
schema, tname, column =
|
462
|
+
schema, tname, column, if_not_exists = (
|
463
|
+
op.schema,
|
464
|
+
op.table_name,
|
465
|
+
op.column,
|
466
|
+
op.if_not_exists,
|
467
|
+
)
|
461
468
|
if autogen_context._has_batch:
|
462
469
|
template = "%(prefix)sadd_column(%(column)s)"
|
463
470
|
else:
|
464
471
|
template = "%(prefix)sadd_column(%(tname)r, %(column)s"
|
465
472
|
if schema:
|
466
473
|
template += ", schema=%(schema)r"
|
474
|
+
if if_not_exists is not None:
|
475
|
+
template += ", if_not_exists=%(if_not_exists)r"
|
467
476
|
template += ")"
|
468
477
|
text = template % {
|
469
478
|
"prefix": _alembic_autogenerate_prefix(autogen_context),
|
470
479
|
"tname": tname,
|
471
480
|
"column": _render_column(column, autogen_context),
|
472
481
|
"schema": schema,
|
482
|
+
"if_not_exists": if_not_exists,
|
473
483
|
}
|
474
484
|
return text
|
475
485
|
|
476
486
|
|
477
487
|
@renderers.dispatch_for(ops.DropColumnOp)
|
478
488
|
def _drop_column(autogen_context: AutogenContext, op: ops.DropColumnOp) -> str:
|
479
|
-
schema, tname, column_name =
|
489
|
+
schema, tname, column_name, if_exists = (
|
490
|
+
op.schema,
|
491
|
+
op.table_name,
|
492
|
+
op.column_name,
|
493
|
+
op.if_exists,
|
494
|
+
)
|
480
495
|
|
481
496
|
if autogen_context._has_batch:
|
482
497
|
template = "%(prefix)sdrop_column(%(cname)r)"
|
@@ -484,6 +499,8 @@ def _drop_column(autogen_context: AutogenContext, op: ops.DropColumnOp) -> str:
|
|
484
499
|
template = "%(prefix)sdrop_column(%(tname)r, %(cname)r"
|
485
500
|
if schema:
|
486
501
|
template += ", schema=%(schema)r"
|
502
|
+
if if_exists is not None:
|
503
|
+
template += ", if_exists=%(if_exists)r"
|
487
504
|
template += ")"
|
488
505
|
|
489
506
|
text = template % {
|
@@ -491,6 +508,7 @@ def _drop_column(autogen_context: AutogenContext, op: ops.DropColumnOp) -> str:
|
|
491
508
|
"tname": _ident(tname),
|
492
509
|
"cname": _ident(column_name),
|
493
510
|
"schema": _ident(schema),
|
511
|
+
"if_exists": if_exists,
|
494
512
|
}
|
495
513
|
return text
|
496
514
|
|
@@ -505,6 +523,7 @@ def _alter_column(
|
|
505
523
|
type_ = op.modify_type
|
506
524
|
nullable = op.modify_nullable
|
507
525
|
comment = op.modify_comment
|
526
|
+
newname = op.modify_name
|
508
527
|
autoincrement = op.kw.get("autoincrement", None)
|
509
528
|
existing_type = op.existing_type
|
510
529
|
existing_nullable = op.existing_nullable
|
@@ -533,6 +552,8 @@ def _alter_column(
|
|
533
552
|
rendered = _render_server_default(server_default, autogen_context)
|
534
553
|
text += ",\n%sserver_default=%s" % (indent, rendered)
|
535
554
|
|
555
|
+
if newname is not None:
|
556
|
+
text += ",\n%snew_column_name=%r" % (indent, newname)
|
536
557
|
if type_ is not None:
|
537
558
|
text += ",\n%stype_=%s" % (indent, _repr_type(type_, autogen_context))
|
538
559
|
if nullable is not None:
|
@@ -1119,7 +1140,10 @@ def _execute_sql(autogen_context: AutogenContext, op: ops.ExecuteSQLOp) -> str:
|
|
1119
1140
|
"Autogenerate rendering of SQL Expression language constructs "
|
1120
1141
|
"not supported here; please use a plain SQL string"
|
1121
1142
|
)
|
1122
|
-
return "
|
1143
|
+
return "{prefix}execute({sqltext!r})".format(
|
1144
|
+
prefix=_alembic_autogenerate_prefix(autogen_context),
|
1145
|
+
sqltext=op.sqltext,
|
1146
|
+
)
|
1123
1147
|
|
1124
1148
|
|
1125
1149
|
renderers = default_renderers.branch()
|
alembic/command.py
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import os
|
6
|
+
import pathlib
|
6
7
|
from typing import List
|
7
8
|
from typing import Optional
|
8
9
|
from typing import TYPE_CHECKING
|
@@ -12,6 +13,7 @@ from . import autogenerate as autogen
|
|
12
13
|
from . import util
|
13
14
|
from .runtime.environment import EnvironmentContext
|
14
15
|
from .script import ScriptDirectory
|
16
|
+
from .util import compat
|
15
17
|
|
16
18
|
if TYPE_CHECKING:
|
17
19
|
from alembic.config import Config
|
@@ -28,12 +30,10 @@ def list_templates(config: Config) -> None:
|
|
28
30
|
"""
|
29
31
|
|
30
32
|
config.print_stdout("Available templates:\n")
|
31
|
-
for tempname in
|
32
|
-
with open(
|
33
|
-
os.path.join(config.get_template_directory(), tempname, "README")
|
34
|
-
) as readme:
|
33
|
+
for tempname in config._get_template_path().iterdir():
|
34
|
+
with (tempname / "README").open() as readme:
|
35
35
|
synopsis = next(readme).rstrip()
|
36
|
-
config.print_stdout("%s - %s", tempname, synopsis)
|
36
|
+
config.print_stdout("%s - %s", tempname.name, synopsis)
|
37
37
|
|
38
38
|
config.print_stdout("\nTemplates are used via the 'init' command, e.g.:")
|
39
39
|
config.print_stdout("\n alembic init --template generic ./scripts")
|
@@ -59,65 +59,136 @@ def init(
|
|
59
59
|
|
60
60
|
"""
|
61
61
|
|
62
|
-
|
62
|
+
directory_path = pathlib.Path(directory)
|
63
|
+
if directory_path.exists() and list(directory_path.iterdir()):
|
63
64
|
raise util.CommandError(
|
64
|
-
"Directory %s already exists and is not empty" %
|
65
|
+
"Directory %s already exists and is not empty" % directory_path
|
65
66
|
)
|
66
67
|
|
67
|
-
|
68
|
-
if not os.access(template_dir, os.F_OK):
|
69
|
-
raise util.CommandError("No such template %r" % template)
|
68
|
+
template_path = config._get_template_path() / template
|
70
69
|
|
71
|
-
if not
|
70
|
+
if not template_path.exists():
|
71
|
+
raise util.CommandError("No such template {template_path}")
|
72
|
+
|
73
|
+
# left as os.access() to suit unit test mocking
|
74
|
+
if not os.access(directory_path, os.F_OK):
|
72
75
|
with util.status(
|
73
|
-
f"Creating directory {
|
76
|
+
f"Creating directory {directory_path.absolute()}",
|
74
77
|
**config.messaging_opts,
|
75
78
|
):
|
76
|
-
os.makedirs(
|
79
|
+
os.makedirs(directory_path)
|
77
80
|
|
78
|
-
versions =
|
81
|
+
versions = directory_path / "versions"
|
79
82
|
with util.status(
|
80
|
-
f"Creating directory {
|
83
|
+
f"Creating directory {versions.absolute()}",
|
81
84
|
**config.messaging_opts,
|
82
85
|
):
|
83
86
|
os.makedirs(versions)
|
84
87
|
|
85
|
-
|
88
|
+
if not directory_path.is_absolute():
|
89
|
+
# for non-absolute path, state config file in .ini / pyproject
|
90
|
+
# as relative to the %(here)s token, which is where the config
|
91
|
+
# file itself would be
|
92
|
+
|
93
|
+
if config._config_file_path is not None:
|
94
|
+
rel_dir = compat.path_relative_to(
|
95
|
+
directory_path.absolute(),
|
96
|
+
config._config_file_path.absolute().parent,
|
97
|
+
walk_up=True,
|
98
|
+
)
|
99
|
+
ini_script_location_directory = ("%(here)s" / rel_dir).as_posix()
|
100
|
+
if config._toml_file_path is not None:
|
101
|
+
rel_dir = compat.path_relative_to(
|
102
|
+
directory_path.absolute(),
|
103
|
+
config._toml_file_path.absolute().parent,
|
104
|
+
walk_up=True,
|
105
|
+
)
|
106
|
+
toml_script_location_directory = ("%(here)s" / rel_dir).as_posix()
|
107
|
+
|
108
|
+
else:
|
109
|
+
ini_script_location_directory = directory_path.as_posix()
|
110
|
+
toml_script_location_directory = directory_path.as_posix()
|
111
|
+
|
112
|
+
script = ScriptDirectory(directory_path)
|
86
113
|
|
87
|
-
|
88
|
-
|
89
|
-
|
114
|
+
has_toml = False
|
115
|
+
|
116
|
+
config_file: pathlib.Path | None = None
|
117
|
+
|
118
|
+
for file_path in template_path.iterdir():
|
119
|
+
file_ = file_path.name
|
90
120
|
if file_ == "alembic.ini.mako":
|
91
121
|
assert config.config_file_name is not None
|
92
|
-
config_file =
|
93
|
-
if
|
122
|
+
config_file = pathlib.Path(config.config_file_name).absolute()
|
123
|
+
if config_file.exists():
|
94
124
|
util.msg(
|
95
|
-
f"File {config_file
|
125
|
+
f"File {config_file} already exists, skipping",
|
96
126
|
**config.messaging_opts,
|
97
127
|
)
|
98
128
|
else:
|
99
129
|
script._generate_template(
|
100
|
-
file_path,
|
130
|
+
file_path,
|
131
|
+
config_file,
|
132
|
+
script_location=ini_script_location_directory,
|
133
|
+
)
|
134
|
+
elif file_ == "pyproject.toml.mako":
|
135
|
+
has_toml = True
|
136
|
+
assert config._toml_file_path is not None
|
137
|
+
toml_path = config._toml_file_path.absolute()
|
138
|
+
|
139
|
+
if toml_path.exists():
|
140
|
+
# left as open() to suit unit test mocking
|
141
|
+
with open(toml_path, "rb") as f:
|
142
|
+
toml_data = compat.tomllib.load(f)
|
143
|
+
if "tool" in toml_data and "alembic" in toml_data["tool"]:
|
144
|
+
|
145
|
+
util.msg(
|
146
|
+
f"File {toml_path} already exists "
|
147
|
+
"and already has a [tool.alembic] section, "
|
148
|
+
"skipping",
|
149
|
+
)
|
150
|
+
continue
|
151
|
+
script._append_template(
|
152
|
+
file_path,
|
153
|
+
toml_path,
|
154
|
+
script_location=toml_script_location_directory,
|
101
155
|
)
|
102
|
-
|
103
|
-
|
156
|
+
else:
|
157
|
+
script._generate_template(
|
158
|
+
file_path,
|
159
|
+
toml_path,
|
160
|
+
script_location=toml_script_location_directory,
|
161
|
+
)
|
162
|
+
|
163
|
+
elif file_path.is_file():
|
164
|
+
output_file = directory_path / file_
|
104
165
|
script._copy_file(file_path, output_file)
|
105
166
|
|
106
167
|
if package:
|
107
168
|
for path in [
|
108
|
-
|
109
|
-
|
169
|
+
directory_path.absolute() / "__init__.py",
|
170
|
+
versions.absolute() / "__init__.py",
|
110
171
|
]:
|
111
|
-
with util.status(f"Adding {path!
|
172
|
+
with util.status(f"Adding {path!s}", **config.messaging_opts):
|
173
|
+
# left as open() to suit unit test mocking
|
112
174
|
with open(path, "w"):
|
113
175
|
pass
|
114
176
|
|
115
177
|
assert config_file is not None
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
178
|
+
|
179
|
+
if has_toml:
|
180
|
+
util.msg(
|
181
|
+
f"Please edit configuration settings in {toml_path} and "
|
182
|
+
"configuration/connection/logging "
|
183
|
+
f"settings in {config_file} before proceeding.",
|
184
|
+
**config.messaging_opts,
|
185
|
+
)
|
186
|
+
else:
|
187
|
+
util.msg(
|
188
|
+
"Please edit configuration/connection/logging "
|
189
|
+
f"settings in {config_file} before proceeding.",
|
190
|
+
**config.messaging_opts,
|
191
|
+
)
|
121
192
|
|
122
193
|
|
123
194
|
def revision(
|
@@ -128,7 +199,7 @@ def revision(
|
|
128
199
|
head: str = "head",
|
129
200
|
splice: bool = False,
|
130
201
|
branch_label: Optional[_RevIdType] = None,
|
131
|
-
version_path:
|
202
|
+
version_path: Union[str, os.PathLike[str], None] = None,
|
132
203
|
rev_id: Optional[str] = None,
|
133
204
|
depends_on: Optional[str] = None,
|
134
205
|
process_revision_directives: Optional[ProcessRevisionDirectiveFn] = None,
|
@@ -198,7 +269,9 @@ def revision(
|
|
198
269
|
process_revision_directives=process_revision_directives,
|
199
270
|
)
|
200
271
|
|
201
|
-
environment = util.asbool(
|
272
|
+
environment = util.asbool(
|
273
|
+
config.get_alembic_option("revision_environment")
|
274
|
+
)
|
202
275
|
|
203
276
|
if autogenerate:
|
204
277
|
environment = True
|
@@ -338,7 +411,9 @@ def merge(
|
|
338
411
|
# e.g. multiple databases
|
339
412
|
}
|
340
413
|
|
341
|
-
environment = util.asbool(
|
414
|
+
environment = util.asbool(
|
415
|
+
config.get_alembic_option("revision_environment")
|
416
|
+
)
|
342
417
|
|
343
418
|
if environment:
|
344
419
|
|
@@ -511,7 +586,7 @@ def history(
|
|
511
586
|
base = head = None
|
512
587
|
|
513
588
|
environment = (
|
514
|
-
util.asbool(config.
|
589
|
+
util.asbool(config.get_alembic_option("revision_environment"))
|
515
590
|
or indicate_current
|
516
591
|
)
|
517
592
|
|