alembic 1.12.0__py3-none-any.whl → 1.13.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.
Files changed (36) hide show
  1. alembic/__init__.py +1 -3
  2. alembic/autogenerate/api.py +14 -6
  3. alembic/autogenerate/compare.py +129 -195
  4. alembic/autogenerate/render.py +42 -32
  5. alembic/autogenerate/rewriter.py +19 -19
  6. alembic/command.py +11 -9
  7. alembic/config.py +1 -1
  8. alembic/context.pyi +12 -5
  9. alembic/ddl/_autogen.py +323 -0
  10. alembic/ddl/impl.py +167 -41
  11. alembic/ddl/mssql.py +1 -4
  12. alembic/ddl/mysql.py +4 -3
  13. alembic/ddl/postgresql.py +157 -70
  14. alembic/op.pyi +9 -11
  15. alembic/operations/__init__.py +2 -0
  16. alembic/operations/base.py +10 -11
  17. alembic/operations/ops.py +14 -14
  18. alembic/operations/toimpl.py +5 -5
  19. alembic/runtime/environment.py +7 -5
  20. alembic/runtime/migration.py +4 -4
  21. alembic/script/base.py +25 -17
  22. alembic/script/revision.py +30 -25
  23. alembic/templates/async/alembic.ini.mako +3 -3
  24. alembic/templates/generic/alembic.ini.mako +3 -3
  25. alembic/templates/multidb/alembic.ini.mako +3 -3
  26. alembic/testing/requirements.py +12 -4
  27. alembic/testing/schemacompare.py +9 -0
  28. alembic/testing/suite/test_autogen_identity.py +23 -38
  29. alembic/util/compat.py +0 -1
  30. alembic/util/sqla_compat.py +58 -30
  31. {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/METADATA +8 -6
  32. {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/RECORD +36 -35
  33. {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/WHEEL +1 -1
  34. {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/LICENSE +0 -0
  35. {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/entry_points.txt +0 -0
  36. {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/top_level.txt +0 -0
@@ -40,7 +40,7 @@ if TYPE_CHECKING:
40
40
  from sqlalchemy.engine.base import Connection
41
41
  from sqlalchemy.engine.base import Transaction
42
42
  from sqlalchemy.engine.mock import MockConnection
43
- from sqlalchemy.sql.elements import ClauseElement
43
+ from sqlalchemy.sql import Executable
44
44
 
45
45
  from .environment import EnvironmentContext
46
46
  from ..config import Config
@@ -651,7 +651,7 @@ class MigrationContext:
651
651
 
652
652
  def execute(
653
653
  self,
654
- sql: Union[ClauseElement, str],
654
+ sql: Union[Executable, str],
655
655
  execution_options: Optional[dict] = None,
656
656
  ) -> None:
657
657
  """Execute a SQL construct or string statement.
@@ -1157,7 +1157,7 @@ class RevisionStep(MigrationStep):
1157
1157
  self.to_revisions[0],
1158
1158
  )
1159
1159
 
1160
- def _unmerge_to_revisions(self, heads: Collection[str]) -> Tuple[str, ...]:
1160
+ def _unmerge_to_revisions(self, heads: Set[str]) -> Tuple[str, ...]:
1161
1161
  other_heads = set(heads).difference([self.revision.revision])
1162
1162
  if other_heads:
1163
1163
  ancestors = {
@@ -1171,7 +1171,7 @@ class RevisionStep(MigrationStep):
1171
1171
  return self.to_revisions
1172
1172
 
1173
1173
  def unmerge_branch_idents(
1174
- self, heads: Collection[str]
1174
+ self, heads: Set[str]
1175
1175
  ) -> Tuple[str, str, Tuple[str, ...]]:
1176
1176
  to_revisions = self._unmerge_to_revisions(heads)
1177
1177
 
alembic/script/base.py CHANGED
@@ -23,9 +23,11 @@ from . import revision
23
23
  from . import write_hooks
24
24
  from .. import util
25
25
  from ..runtime import migration
26
+ from ..util import compat
26
27
  from ..util import not_none
27
28
 
28
29
  if TYPE_CHECKING:
30
+ from .revision import _GetRevArg
29
31
  from .revision import _RevIdType
30
32
  from .revision import Revision
31
33
  from ..config import Config
@@ -34,9 +36,14 @@ if TYPE_CHECKING:
34
36
  from ..runtime.migration import StampStep
35
37
 
36
38
  try:
37
- from dateutil import tz
39
+ if compat.py39:
40
+ from zoneinfo import ZoneInfo
41
+ from zoneinfo import ZoneInfoNotFoundError
42
+ else:
43
+ from backports.zoneinfo import ZoneInfo # type: ignore[import-not-found,no-redef] # noqa: E501
44
+ from backports.zoneinfo import ZoneInfoNotFoundError # type: ignore[import-not-found,no-redef] # noqa: E501
38
45
  except ImportError:
39
- tz = None # type: ignore[assignment]
46
+ ZoneInfo = None # type: ignore[assignment, misc]
40
47
 
41
48
  _sourceless_rev_file = re.compile(r"(?!\.\#|__init__)(.*\.py)(c|o)?$")
42
49
  _only_source_rev_file = re.compile(r"(?!\.\#|__init__)(.*\.py)$")
@@ -296,7 +303,7 @@ class ScriptDirectory:
296
303
  ):
297
304
  yield cast(Script, rev)
298
305
 
299
- def get_revisions(self, id_: _RevIdType) -> Tuple[Optional[Script], ...]:
306
+ def get_revisions(self, id_: _GetRevArg) -> Tuple[Optional[Script], ...]:
300
307
  """Return the :class:`.Script` instance with the given rev identifier,
301
308
  symbolic name, or sequence of identifiers.
302
309
 
@@ -603,23 +610,26 @@ class ScriptDirectory:
603
610
 
604
611
  def _generate_create_date(self) -> datetime.datetime:
605
612
  if self.timezone is not None:
606
- if tz is None:
613
+ if ZoneInfo is None:
607
614
  raise util.CommandError(
608
- "The library 'python-dateutil' is required "
609
- "for timezone support"
615
+ "Python >= 3.9 is required for timezone support or"
616
+ "the 'backports.zoneinfo' package must be installed."
610
617
  )
611
618
  # First, assume correct capitalization
612
- tzinfo = tz.gettz(self.timezone)
613
- if tzinfo is None:
614
- # Fall back to uppercase
615
- tzinfo = tz.gettz(self.timezone.upper())
619
+ try:
620
+ tzinfo = ZoneInfo(self.timezone)
621
+ except ZoneInfoNotFoundError:
622
+ tzinfo = None
616
623
  if tzinfo is None:
617
- raise util.CommandError(
618
- "Can't locate timezone: %s" % self.timezone
619
- )
624
+ try:
625
+ tzinfo = ZoneInfo(self.timezone.upper())
626
+ except ZoneInfoNotFoundError:
627
+ raise util.CommandError(
628
+ "Can't locate timezone: %s" % self.timezone
629
+ ) from None
620
630
  create_date = (
621
631
  datetime.datetime.utcnow()
622
- .replace(tzinfo=tz.tzutc())
632
+ .replace(tzinfo=datetime.timezone.utc)
623
633
  .astimezone(tzinfo)
624
634
  )
625
635
  else:
@@ -630,8 +640,7 @@ class ScriptDirectory:
630
640
  self,
631
641
  revid: str,
632
642
  message: Optional[str],
633
- head: Optional[str] = None,
634
- refresh: bool = False,
643
+ head: Optional[_RevIdType] = None,
635
644
  splice: Optional[bool] = False,
636
645
  branch_labels: Optional[_RevIdType] = None,
637
646
  version_path: Optional[str] = None,
@@ -653,7 +662,6 @@ class ScriptDirectory:
653
662
  :param splice: if True, allow the "head" version to not be an
654
663
  actual head; otherwise, the selected head must be a head
655
664
  (e.g. endpoint) revision.
656
- :param refresh: deprecated.
657
665
 
658
666
  """
659
667
  if head is None:
@@ -29,13 +29,19 @@ from ..util import not_none
29
29
  if TYPE_CHECKING:
30
30
  from typing import Literal
31
31
 
32
- _RevIdType = Union[str, Sequence[str]]
32
+ _RevIdType = Union[str, List[str], Tuple[str, ...]]
33
+ _GetRevArg = Union[
34
+ str,
35
+ Iterable[Optional[str]],
36
+ Iterable[str],
37
+ ]
33
38
  _RevisionIdentifierType = Union[str, Tuple[str, ...], None]
34
39
  _RevisionOrStr = Union["Revision", str]
35
40
  _RevisionOrBase = Union["Revision", "Literal['base']"]
36
41
  _InterimRevisionMapType = Dict[str, "Revision"]
37
42
  _RevisionMapType = Dict[Union[None, str, Tuple[()]], Optional["Revision"]]
38
- _T = TypeVar("_T", bound=Union[str, "Revision"])
43
+ _T = TypeVar("_T")
44
+ _TR = TypeVar("_TR", bound=Optional[_RevisionOrStr])
39
45
 
40
46
  _relative_destination = re.compile(r"(?:(.+?)@)?(\w+)?((?:\+|-)\d+)")
41
47
  _revision_illegal_chars = ["@", "-", "+"]
@@ -501,7 +507,7 @@ class RevisionMap:
501
507
  return self.filter_for_lineage(self.bases, identifier)
502
508
 
503
509
  def get_revisions(
504
- self, id_: Union[str, Collection[Optional[str]], None]
510
+ self, id_: Optional[_GetRevArg]
505
511
  ) -> Tuple[Optional[_RevisionOrBase], ...]:
506
512
  """Return the :class:`.Revision` instances with the given rev id
507
513
  or identifiers.
@@ -523,9 +529,7 @@ class RevisionMap:
523
529
  if isinstance(id_, (list, tuple, set, frozenset)):
524
530
  return sum([self.get_revisions(id_elem) for id_elem in id_], ())
525
531
  else:
526
- resolved_id, branch_label = self._resolve_revision_number(
527
- id_ # type:ignore [arg-type]
528
- )
532
+ resolved_id, branch_label = self._resolve_revision_number(id_)
529
533
  if len(resolved_id) == 1:
530
534
  try:
531
535
  rint = int(resolved_id[0])
@@ -590,7 +594,7 @@ class RevisionMap:
590
594
 
591
595
  def _revision_for_ident(
592
596
  self,
593
- resolved_id: Union[str, Tuple[()]],
597
+ resolved_id: Union[str, Tuple[()], None],
594
598
  check_branch: Optional[str] = None,
595
599
  ) -> Optional[Revision]:
596
600
  branch_rev: Optional[Revision]
@@ -669,10 +673,10 @@ class RevisionMap:
669
673
 
670
674
  def filter_for_lineage(
671
675
  self,
672
- targets: Iterable[_T],
676
+ targets: Iterable[_TR],
673
677
  check_against: Optional[str],
674
678
  include_dependencies: bool = False,
675
- ) -> Tuple[_T, ...]:
679
+ ) -> Tuple[_TR, ...]:
676
680
  id_, branch_label = self._resolve_revision_number(check_against)
677
681
 
678
682
  shares = []
@@ -691,7 +695,7 @@ class RevisionMap:
691
695
 
692
696
  def _shares_lineage(
693
697
  self,
694
- target: _RevisionOrStr,
698
+ target: Optional[_RevisionOrStr],
695
699
  test_against_revs: Sequence[_RevisionOrStr],
696
700
  include_dependencies: bool = False,
697
701
  ) -> bool:
@@ -728,7 +732,7 @@ class RevisionMap:
728
732
  )
729
733
 
730
734
  def _resolve_revision_number(
731
- self, id_: Optional[str]
735
+ self, id_: Optional[_GetRevArg]
732
736
  ) -> Tuple[Tuple[str, ...], Optional[str]]:
733
737
  branch_label: Optional[str]
734
738
  if isinstance(id_, str) and "@" in id_:
@@ -1080,13 +1084,13 @@ class RevisionMap:
1080
1084
  ) -> Tuple[Optional[str], Optional[_RevisionOrBase]]:
1081
1085
  """
1082
1086
  Parse downgrade command syntax :target to retrieve the target revision
1083
- and branch label (if any) given the :current_revisons stamp of the
1087
+ and branch label (if any) given the :current_revisions stamp of the
1084
1088
  database.
1085
1089
 
1086
1090
  Returns a tuple (branch_label, target_revision) where branch_label
1087
1091
  is a string from the command specifying the branch to consider (or
1088
1092
  None if no branch given), and target_revision is a Revision object
1089
- which the command refers to. target_revsions is None if the command
1093
+ which the command refers to. target_revisions is None if the command
1090
1094
  refers to 'base'. The target may be specified in absolute form, or
1091
1095
  relative to :current_revisions.
1092
1096
  """
@@ -1129,7 +1133,7 @@ class RevisionMap:
1129
1133
  if not symbol_list:
1130
1134
  # check the case where there are multiple branches
1131
1135
  # but there is currently a single heads, since all
1132
- # other branch heads are dependant of the current
1136
+ # other branch heads are dependent of the current
1133
1137
  # single heads.
1134
1138
  all_current = cast(
1135
1139
  Set[Revision], self._get_all_current(cr_tuple)
@@ -1196,7 +1200,7 @@ class RevisionMap:
1196
1200
  ) -> Tuple[Optional[_RevisionOrBase], ...]:
1197
1201
  """
1198
1202
  Parse upgrade command syntax :target to retrieve the target revision
1199
- and given the :current_revisons stamp of the database.
1203
+ and given the :current_revisions stamp of the database.
1200
1204
 
1201
1205
  Returns a tuple of Revision objects which should be iterated/upgraded
1202
1206
  to. The target may be specified in absolute form, or relative to
@@ -1211,7 +1215,7 @@ class RevisionMap:
1211
1215
  # No relative destination, target is absolute.
1212
1216
  return self.get_revisions(target)
1213
1217
 
1214
- current_revisions_tup: Union[str, Collection[Optional[str]], None]
1218
+ current_revisions_tup: Union[str, Tuple[Optional[str], ...], None]
1215
1219
  current_revisions_tup = util.to_tuple(current_revisions)
1216
1220
 
1217
1221
  branch_label, symbol, relative_str = match.groups()
@@ -1224,7 +1228,8 @@ class RevisionMap:
1224
1228
  start_revs = current_revisions_tup
1225
1229
  if branch_label:
1226
1230
  start_revs = self.filter_for_lineage(
1227
- self.get_revisions(current_revisions_tup), branch_label
1231
+ self.get_revisions(current_revisions_tup), # type: ignore[arg-type] # noqa: E501
1232
+ branch_label,
1228
1233
  )
1229
1234
  if not start_revs:
1230
1235
  # The requested branch is not a head, so we need to
@@ -1577,8 +1582,8 @@ class Revision:
1577
1582
 
1578
1583
  self.verify_rev_id(revision)
1579
1584
  self.revision = revision
1580
- self.down_revision = tuple_rev_as_scalar(down_revision)
1581
- self.dependencies = tuple_rev_as_scalar(dependencies)
1585
+ self.down_revision = tuple_rev_as_scalar(util.to_tuple(down_revision))
1586
+ self.dependencies = tuple_rev_as_scalar(util.to_tuple(dependencies))
1582
1587
  self._orig_branch_labels = util.to_tuple(branch_labels, default=())
1583
1588
  self.branch_labels = set(self._orig_branch_labels)
1584
1589
 
@@ -1676,20 +1681,20 @@ class Revision:
1676
1681
 
1677
1682
 
1678
1683
  @overload
1679
- def tuple_rev_as_scalar(
1680
- rev: Optional[Sequence[str]],
1681
- ) -> Optional[Union[str, Sequence[str]]]:
1684
+ def tuple_rev_as_scalar(rev: None) -> None:
1682
1685
  ...
1683
1686
 
1684
1687
 
1685
1688
  @overload
1686
1689
  def tuple_rev_as_scalar(
1687
- rev: Optional[Sequence[Optional[str]]],
1688
- ) -> Optional[Union[Optional[str], Sequence[Optional[str]]]]:
1690
+ rev: Union[Tuple[_T, ...], List[_T]]
1691
+ ) -> Union[_T, Tuple[_T, ...], List[_T]]:
1689
1692
  ...
1690
1693
 
1691
1694
 
1692
- def tuple_rev_as_scalar(rev):
1695
+ def tuple_rev_as_scalar(
1696
+ rev: Optional[Sequence[_T]],
1697
+ ) -> Union[_T, Sequence[_T], None]:
1693
1698
  if not rev:
1694
1699
  return None
1695
1700
  elif len(rev) == 1:
@@ -14,9 +14,9 @@ prepend_sys_path = .
14
14
 
15
15
  # timezone to use when rendering the date within the migration file
16
16
  # as well as the filename.
17
- # If specified, requires the python-dateutil library that can be
18
- # installed by adding `alembic[tz]` to the pip requirements
19
- # string value is passed to dateutil.tz.gettz()
17
+ # If specified, requires the python>=3.9 or backports.zoneinfo library.
18
+ # Any required deps can installed by adding `alembic[tz]` to the pip requirements
19
+ # string value is passed to ZoneInfo()
20
20
  # leave blank for localtime
21
21
  # timezone =
22
22
 
@@ -16,9 +16,9 @@ prepend_sys_path = .
16
16
 
17
17
  # timezone to use when rendering the date within the migration file
18
18
  # as well as the filename.
19
- # If specified, requires the python-dateutil library that can be
20
- # installed by adding `alembic[tz]` to the pip requirements
21
- # string value is passed to dateutil.tz.gettz()
19
+ # If specified, requires the python>=3.9 or backports.zoneinfo library.
20
+ # Any required deps can installed by adding `alembic[tz]` to the pip requirements
21
+ # string value is passed to ZoneInfo()
22
22
  # leave blank for localtime
23
23
  # timezone =
24
24
 
@@ -16,9 +16,9 @@ prepend_sys_path = .
16
16
 
17
17
  # timezone to use when rendering the date within the migration file
18
18
  # as well as the filename.
19
- # If specified, requires the python-dateutil library that can be
20
- # installed by adding `alembic[tz]` to the pip requirements
21
- # string value is passed to dateutil.tz.gettz()
19
+ # If specified, requires the python>=3.9 or backports.zoneinfo library.
20
+ # Any required deps can installed by adding `alembic[tz]` to the pip requirements
21
+ # string value is passed to ZoneInfo()
22
22
  # leave blank for localtime
23
23
  # timezone =
24
24
 
@@ -95,6 +95,18 @@ class SuiteRequirements(Requirements):
95
95
  "SQLAlchemy 2.x test",
96
96
  )
97
97
 
98
+ @property
99
+ def asyncio(self):
100
+ def go(config):
101
+ try:
102
+ import greenlet # noqa: F401
103
+ except ImportError:
104
+ return False
105
+ else:
106
+ return True
107
+
108
+ return self.sqlalchemy_14 + exclusions.only_if(go)
109
+
98
110
  @property
99
111
  def comments(self):
100
112
  return exclusions.only_if(
@@ -196,7 +208,3 @@ class SuiteRequirements(Requirements):
196
208
  return exclusions.only_if(
197
209
  exclusions.BooleanPredicate(sqla_compat.has_identity)
198
210
  )
199
-
200
- @property
201
- def supports_identity_on_null(self):
202
- return exclusions.closed()
@@ -1,6 +1,7 @@
1
1
  from itertools import zip_longest
2
2
 
3
3
  from sqlalchemy import schema
4
+ from sqlalchemy.sql.elements import ClauseList
4
5
 
5
6
 
6
7
  class CompareTable:
@@ -60,6 +61,14 @@ class CompareIndex:
60
61
  def __ne__(self, other):
61
62
  return not self.__eq__(other)
62
63
 
64
+ def __repr__(self):
65
+ expr = ClauseList(*self.index.expressions)
66
+ try:
67
+ expr_str = expr.compile().string
68
+ except Exception:
69
+ expr_str = str(expr)
70
+ return f"<CompareIndex {self.index.name}({expr_str})>"
71
+
63
72
 
64
73
  class CompareCheckConstraint:
65
74
  def __init__(self, constraint):
@@ -4,6 +4,7 @@ from sqlalchemy import Integer
4
4
  from sqlalchemy import MetaData
5
5
  from sqlalchemy import Table
6
6
 
7
+ from alembic.util import sqla_compat
7
8
  from ._autogen_fixtures import AutogenFixtureTest
8
9
  from ... import testing
9
10
  from ...testing import config
@@ -78,16 +79,33 @@ class AutogenerateIdentityTest(AutogenFixtureTest, TestBase):
78
79
  m2 = MetaData()
79
80
 
80
81
  for m in (m1, m2):
81
- Table(
82
- "user",
83
- m,
84
- Column("id", Integer, sa.Identity(start=2)),
85
- )
82
+ id_ = sa.Identity(start=2)
83
+ Table("user", m, Column("id", Integer, id_))
86
84
 
87
85
  diffs = self._fixture(m1, m2)
88
86
 
89
87
  eq_(diffs, [])
90
88
 
89
+ def test_dialect_kwargs_changes(self):
90
+ m1 = MetaData()
91
+ m2 = MetaData()
92
+
93
+ if sqla_compat.identity_has_dialect_kwargs:
94
+ args = {"oracle_on_null": True, "oracle_order": True}
95
+ else:
96
+ args = {"on_null": True, "order": True}
97
+
98
+ Table("user", m1, Column("id", Integer, sa.Identity(start=2)))
99
+ id_ = sa.Identity(start=2, **args)
100
+ Table("user", m2, Column("id", Integer, id_))
101
+
102
+ diffs = self._fixture(m1, m2)
103
+ if config.db.name == "oracle":
104
+ is_true(len(diffs), 1)
105
+ eq_(diffs[0][0][0], "modify_default")
106
+ else:
107
+ eq_(diffs, [])
108
+
91
109
  @testing.combinations(
92
110
  (None, dict(start=2)),
93
111
  (dict(start=2), None),
@@ -206,36 +224,3 @@ class AutogenerateIdentityTest(AutogenFixtureTest, TestBase):
206
224
  removed = diffs[5]
207
225
 
208
226
  is_true(isinstance(removed, sa.Identity))
209
-
210
- def test_identity_on_null(self):
211
- m1 = MetaData()
212
- m2 = MetaData()
213
-
214
- Table(
215
- "user",
216
- m1,
217
- Column("id", Integer, sa.Identity(start=2, on_null=True)),
218
- Column("other", sa.Text),
219
- )
220
-
221
- Table(
222
- "user",
223
- m2,
224
- Column("id", Integer, sa.Identity(start=2, on_null=False)),
225
- Column("other", sa.Text),
226
- )
227
-
228
- diffs = self._fixture(m1, m2)
229
- if not config.requirements.supports_identity_on_null.enabled:
230
- eq_(diffs, [])
231
- else:
232
- eq_(len(diffs[0]), 1)
233
- diffs = diffs[0][0]
234
- eq_(diffs[0], "modify_default")
235
- eq_(diffs[2], "user")
236
- eq_(diffs[3], "id")
237
- old = diffs[5]
238
- new = diffs[6]
239
-
240
- is_true(isinstance(old, sa.Identity))
241
- is_true(isinstance(new, sa.Identity))
alembic/util/compat.py CHANGED
@@ -16,7 +16,6 @@ is_posix = os.name == "posix"
16
16
  py311 = sys.version_info >= (3, 11)
17
17
  py310 = sys.version_info >= (3, 10)
18
18
  py39 = sys.version_info >= (3, 9)
19
- py38 = sys.version_info >= (3, 8)
20
19
 
21
20
 
22
21
  # produce a wrapper that allows encoded text to stream
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import contextlib
4
4
  import re
5
5
  from typing import Any
6
+ from typing import Dict
6
7
  from typing import Iterable
7
8
  from typing import Iterator
8
9
  from typing import Mapping
@@ -22,6 +23,7 @@ from sqlalchemy.schema import CheckConstraint
22
23
  from sqlalchemy.schema import Column
23
24
  from sqlalchemy.schema import ForeignKeyConstraint
24
25
  from sqlalchemy.sql import visitors
26
+ from sqlalchemy.sql.base import DialectKWArgs
25
27
  from sqlalchemy.sql.elements import BindParameter
26
28
  from sqlalchemy.sql.elements import ColumnClause
27
29
  from sqlalchemy.sql.elements import quoted_name
@@ -80,9 +82,10 @@ class _Unsupported:
80
82
  try:
81
83
  from sqlalchemy import Computed
82
84
  except ImportError:
85
+ if not TYPE_CHECKING:
83
86
 
84
- class Computed(_Unsupported): # type: ignore
85
- pass
87
+ class Computed(_Unsupported):
88
+ pass
86
89
 
87
90
  has_computed = False
88
91
  has_computed_reflection = False
@@ -93,26 +96,54 @@ else:
93
96
  try:
94
97
  from sqlalchemy import Identity
95
98
  except ImportError:
99
+ if not TYPE_CHECKING:
96
100
 
97
- class Identity(_Unsupported): # type: ignore
98
- pass
101
+ class Identity(_Unsupported):
102
+ pass
99
103
 
100
104
  has_identity = False
101
105
  else:
102
- # attributes common to Identity and Sequence
103
- _identity_options_attrs = (
104
- "start",
105
- "increment",
106
- "minvalue",
107
- "maxvalue",
108
- "nominvalue",
109
- "nomaxvalue",
110
- "cycle",
111
- "cache",
112
- "order",
113
- )
114
- # attributes of Identity
115
- _identity_attrs = _identity_options_attrs + ("on_null",)
106
+ identity_has_dialect_kwargs = issubclass(Identity, DialectKWArgs)
107
+
108
+ def _get_identity_options_dict(
109
+ identity: Union[Identity, schema.Sequence, None],
110
+ dialect_kwargs: bool = False,
111
+ ) -> Dict[str, Any]:
112
+ if identity is None:
113
+ return {}
114
+ elif identity_has_dialect_kwargs:
115
+ as_dict = identity._as_dict() # type: ignore
116
+ if dialect_kwargs:
117
+ assert isinstance(identity, DialectKWArgs)
118
+ as_dict.update(identity.dialect_kwargs)
119
+ else:
120
+ as_dict = {}
121
+ if isinstance(identity, Identity):
122
+ # always=None means something different than always=False
123
+ as_dict["always"] = identity.always
124
+ if identity.on_null is not None:
125
+ as_dict["on_null"] = identity.on_null
126
+ # attributes common to Identity and Sequence
127
+ attrs = (
128
+ "start",
129
+ "increment",
130
+ "minvalue",
131
+ "maxvalue",
132
+ "nominvalue",
133
+ "nomaxvalue",
134
+ "cycle",
135
+ "cache",
136
+ "order",
137
+ )
138
+ as_dict.update(
139
+ {
140
+ key: getattr(identity, key, None)
141
+ for key in attrs
142
+ if getattr(identity, key, None) is not None
143
+ }
144
+ )
145
+ return as_dict
146
+
116
147
  has_identity = True
117
148
 
118
149
  if sqla_2:
@@ -493,14 +524,6 @@ def _render_literal_bindparam(
493
524
  return compiler.render_literal_bindparam(element, **kw)
494
525
 
495
526
 
496
- def _get_index_expressions(idx):
497
- return list(idx.expressions)
498
-
499
-
500
- def _get_index_column_names(idx):
501
- return [getattr(exp, "name", None) for exp in _get_index_expressions(idx)]
502
-
503
-
504
527
  def _column_kwargs(col: Column) -> Mapping:
505
528
  if sqla_13:
506
529
  return col.kwargs
@@ -599,10 +622,15 @@ else:
599
622
 
600
623
 
601
624
  def is_expression_index(index: Index) -> bool:
602
- expr: Any
603
625
  for expr in index.expressions:
604
- while isinstance(expr, UnaryExpression):
605
- expr = expr.element
606
- if not isinstance(expr, ColumnClause) or expr.is_literal:
626
+ if is_expression(expr):
607
627
  return True
608
628
  return False
629
+
630
+
631
+ def is_expression(expr: Any) -> bool:
632
+ while isinstance(expr, UnaryExpression):
633
+ expr = expr.element
634
+ if not isinstance(expr, ColumnClause) or expr.is_literal:
635
+ return True
636
+ return False
@@ -1,13 +1,14 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: alembic
3
- Version: 1.12.0
3
+ Version: 1.13.0
4
4
  Summary: A database migration tool for SQLAlchemy.
5
5
  Home-page: https://alembic.sqlalchemy.org
6
6
  Author: Mike Bayer
7
7
  Author-email: mike_mp@zzzcomputing.com
8
8
  License: MIT
9
- Project-URL: Source, https://github.com/sqlalchemy/alembic/
10
9
  Project-URL: Documentation, https://alembic.sqlalchemy.org/en/latest/
10
+ Project-URL: Changelog, https://alembic.sqlalchemy.org/en/latest/changelog.html
11
+ Project-URL: Source, https://github.com/sqlalchemy/alembic/
11
12
  Project-URL: Issue Tracker, https://github.com/sqlalchemy/alembic/issues/
12
13
  Classifier: Development Status :: 5 - Production/Stable
13
14
  Classifier: Intended Audience :: Developers
@@ -16,14 +17,15 @@ Classifier: License :: OSI Approved :: MIT License
16
17
  Classifier: Operating System :: OS Independent
17
18
  Classifier: Programming Language :: Python
18
19
  Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.7
20
20
  Classifier: Programming Language :: Python :: 3.8
21
21
  Classifier: Programming Language :: Python :: 3.9
22
22
  Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
23
25
  Classifier: Programming Language :: Python :: Implementation :: CPython
24
26
  Classifier: Programming Language :: Python :: Implementation :: PyPy
25
27
  Classifier: Topic :: Database :: Front-Ends
26
- Requires-Python: >=3.7
28
+ Requires-Python: >=3.8
27
29
  Description-Content-Type: text/x-rst
28
30
  License-File: LICENSE
29
31
  Requires-Dist: SQLAlchemy >=1.3.0
@@ -32,7 +34,7 @@ Requires-Dist: typing-extensions >=4
32
34
  Requires-Dist: importlib-metadata ; python_version < "3.9"
33
35
  Requires-Dist: importlib-resources ; python_version < "3.9"
34
36
  Provides-Extra: tz
35
- Requires-Dist: python-dateutil ; extra == 'tz'
37
+ Requires-Dist: backports.zoneinfo ; (python_version < "3.9") and extra == 'tz'
36
38
 
37
39
  Alembic is a database migrations tool written by the author
38
40
  of `SQLAlchemy <http://www.sqlalchemy.org>`_. A migrations tool
@@ -101,7 +103,7 @@ The goals of Alembic are:
101
103
  * Provide a library of ALTER constructs that can be used by any SQLAlchemy
102
104
  application. The DDL constructs build upon SQLAlchemy's own DDLElement base
103
105
  and can be used standalone by any application or script.
104
- * At long last, bring SQLite and its inablity to ALTER things into the fold,
106
+ * At long last, bring SQLite and its inability to ALTER things into the fold,
105
107
  but in such a way that SQLite's very special workflow needs are accommodated
106
108
  in an explicit way that makes the most of a bad situation, through the
107
109
  concept of a "batch" migration, where multiple changes to a table can