alembic 1.15.2__py3-none-any.whl → 1.16.1__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.
Files changed (41) hide show
  1. alembic/__init__.py +1 -1
  2. alembic/autogenerate/compare.py +60 -7
  3. alembic/autogenerate/render.py +25 -4
  4. alembic/command.py +112 -37
  5. alembic/config.py +574 -222
  6. alembic/ddl/base.py +31 -7
  7. alembic/ddl/impl.py +23 -5
  8. alembic/ddl/mssql.py +3 -1
  9. alembic/ddl/mysql.py +8 -4
  10. alembic/ddl/postgresql.py +6 -2
  11. alembic/ddl/sqlite.py +1 -1
  12. alembic/op.pyi +24 -5
  13. alembic/operations/base.py +18 -3
  14. alembic/operations/ops.py +49 -8
  15. alembic/operations/toimpl.py +20 -3
  16. alembic/script/base.py +123 -136
  17. alembic/script/revision.py +1 -1
  18. alembic/script/write_hooks.py +20 -21
  19. alembic/templates/async/alembic.ini.mako +40 -16
  20. alembic/templates/generic/alembic.ini.mako +39 -17
  21. alembic/templates/multidb/alembic.ini.mako +42 -17
  22. alembic/templates/pyproject/README +1 -0
  23. alembic/templates/pyproject/alembic.ini.mako +44 -0
  24. alembic/templates/pyproject/env.py +78 -0
  25. alembic/templates/pyproject/pyproject.toml.mako +76 -0
  26. alembic/templates/pyproject/script.py.mako +28 -0
  27. alembic/testing/__init__.py +2 -0
  28. alembic/testing/assertions.py +4 -0
  29. alembic/testing/env.py +56 -1
  30. alembic/testing/fixtures.py +28 -1
  31. alembic/testing/suite/_autogen_fixtures.py +113 -0
  32. alembic/util/__init__.py +1 -0
  33. alembic/util/compat.py +56 -0
  34. alembic/util/messaging.py +4 -0
  35. alembic/util/pyfiles.py +56 -19
  36. {alembic-1.15.2.dist-info → alembic-1.16.1.dist-info}/METADATA +3 -3
  37. {alembic-1.15.2.dist-info → alembic-1.16.1.dist-info}/RECORD +41 -36
  38. {alembic-1.15.2.dist-info → alembic-1.16.1.dist-info}/WHEEL +1 -1
  39. {alembic-1.15.2.dist-info → alembic-1.16.1.dist-info}/entry_points.txt +0 -0
  40. {alembic-1.15.2.dist-info → alembic-1.16.1.dist-info}/licenses/LICENSE +0 -0
  41. {alembic-1.15.2.dist-info → alembic-1.16.1.dist-info}/top_level.txt +0 -0
alembic/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  from . import context
2
2
  from . import op
3
3
 
4
- __version__ = "1.15.2"
4
+ __version__ = "1.16.1"
@@ -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( # type:ignore[assignment] # noqa
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( # type:ignore[assignment]
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(tname, schema=schema)
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) # type: ignore[arg-type]
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
@@ -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 = op.schema, op.table_name, op.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 = op.schema, op.table_name, op.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
 
@@ -1122,7 +1140,10 @@ def _execute_sql(autogen_context: AutogenContext, op: ops.ExecuteSQLOp) -> str:
1122
1140
  "Autogenerate rendering of SQL Expression language constructs "
1123
1141
  "not supported here; please use a plain SQL string"
1124
1142
  )
1125
- return "op.execute(%r)" % op.sqltext
1143
+ return "{prefix}execute({sqltext!r})".format(
1144
+ prefix=_alembic_autogenerate_prefix(autogen_context),
1145
+ sqltext=op.sqltext,
1146
+ )
1126
1147
 
1127
1148
 
1128
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 os.listdir(config.get_template_directory()):
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
- if os.access(directory, os.F_OK) and os.listdir(directory):
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" % directory
65
+ "Directory %s already exists and is not empty" % directory_path
65
66
  )
66
67
 
67
- template_dir = os.path.join(config.get_template_directory(), template)
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 os.access(directory, os.F_OK):
70
+ if not template_path.exists():
71
+ raise util.CommandError(f"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 {os.path.abspath(directory)!r}",
76
+ f"Creating directory {directory_path.absolute()}",
74
77
  **config.messaging_opts,
75
78
  ):
76
- os.makedirs(directory)
79
+ os.makedirs(directory_path)
77
80
 
78
- versions = os.path.join(directory, "versions")
81
+ versions = directory_path / "versions"
79
82
  with util.status(
80
- f"Creating directory {os.path.abspath(versions)!r}",
83
+ f"Creating directory {versions.absolute()}",
81
84
  **config.messaging_opts,
82
85
  ):
83
86
  os.makedirs(versions)
84
87
 
85
- script = ScriptDirectory(directory)
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
- config_file: str | None = None
88
- for file_ in os.listdir(template_dir):
89
- file_path = os.path.join(template_dir, file_)
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 = os.path.abspath(config.config_file_name)
93
- if os.access(config_file, os.F_OK):
122
+ config_file = pathlib.Path(config.config_file_name).absolute()
123
+ if config_file.exists():
94
124
  util.msg(
95
- f"File {config_file!r} already exists, skipping",
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, config_file, script_location=directory
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
- elif os.path.isfile(file_path):
103
- output_file = os.path.join(directory, file_)
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
- os.path.join(os.path.abspath(directory), "__init__.py"),
109
- os.path.join(os.path.abspath(versions), "__init__.py"),
169
+ directory_path.absolute() / "__init__.py",
170
+ versions.absolute() / "__init__.py",
110
171
  ]:
111
- with util.status(f"Adding {path!r}", **config.messaging_opts):
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
- util.msg(
117
- "Please edit configuration/connection/logging "
118
- f"settings in {config_file!r} before proceeding.",
119
- **config.messaging_opts,
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: Optional[str] = None,
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(config.get_main_option("revision_environment"))
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(config.get_main_option("revision_environment"))
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.get_main_option("revision_environment"))
589
+ util.asbool(config.get_alembic_option("revision_environment"))
515
590
  or indicate_current
516
591
  )
517
592