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.
Files changed (41) hide show
  1. alembic/__init__.py +1 -1
  2. alembic/autogenerate/compare.py +60 -7
  3. alembic/autogenerate/render.py +28 -4
  4. alembic/command.py +112 -37
  5. alembic/config.py +574 -222
  6. alembic/ddl/base.py +36 -8
  7. alembic/ddl/impl.py +24 -7
  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 +25 -6
  13. alembic/operations/base.py +21 -4
  14. alembic/operations/ops.py +53 -10
  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.1.dist-info → alembic-1.16.0.dist-info}/METADATA +5 -4
  37. {alembic-1.15.1.dist-info → alembic-1.16.0.dist-info}/RECORD +41 -36
  38. {alembic-1.15.1.dist-info → alembic-1.16.0.dist-info}/WHEEL +1 -1
  39. {alembic-1.15.1.dist-info → alembic-1.16.0.dist-info}/entry_points.txt +0 -0
  40. {alembic-1.15.1.dist-info → alembic-1.16.0.dist-info/licenses}/LICENSE +0 -0
  41. {alembic-1.15.1.dist-info → alembic-1.16.0.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,7 @@ from sqlalchemy import schema as sa_schema
8
8
  from . import ops
9
9
  from .base import Operations
10
10
  from ..util.sqla_compat import _copy
11
+ from ..util.sqla_compat import sqla_2
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from sqlalchemy.sql.schema import Table
@@ -92,7 +93,11 @@ def drop_column(
92
93
  ) -> None:
93
94
  column = operation.to_column(operations.migration_context)
94
95
  operations.impl.drop_column(
95
- operation.table_name, column, schema=operation.schema, **operation.kw
96
+ operation.table_name,
97
+ column,
98
+ schema=operation.schema,
99
+ if_exists=operation.if_exists,
100
+ **operation.kw,
96
101
  )
97
102
 
98
103
 
@@ -167,7 +172,13 @@ def add_column(operations: "Operations", operation: "ops.AddColumnOp") -> None:
167
172
  column = _copy(column)
168
173
 
169
174
  t = operations.schema_obj.table(table_name, column, schema=schema)
170
- operations.impl.add_column(table_name, column, schema=schema, **kw)
175
+ operations.impl.add_column(
176
+ table_name,
177
+ column,
178
+ schema=schema,
179
+ if_not_exists=operation.if_not_exists,
180
+ **kw,
181
+ )
171
182
 
172
183
  for constraint in t.constraints:
173
184
  if not isinstance(constraint, sa_schema.PrimaryKeyConstraint):
@@ -197,13 +208,19 @@ def create_constraint(
197
208
  def drop_constraint(
198
209
  operations: "Operations", operation: "ops.DropConstraintOp"
199
210
  ) -> None:
211
+ kw = {}
212
+ if operation.if_exists is not None:
213
+ if not sqla_2:
214
+ raise NotImplementedError("SQLAlchemy 2.0 required")
215
+ kw["if_exists"] = operation.if_exists
200
216
  operations.impl.drop_constraint(
201
217
  operations.schema_obj.generic_constraint(
202
218
  operation.constraint_name,
203
219
  operation.table_name,
204
220
  operation.constraint_type,
205
221
  schema=operation.schema,
206
- )
222
+ ),
223
+ **kw,
207
224
  )
208
225
 
209
226
 
alembic/script/base.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from contextlib import contextmanager
4
4
  import datetime
5
5
  import os
6
+ from pathlib import Path
6
7
  import re
7
8
  import shutil
8
9
  import sys
@@ -11,7 +12,6 @@ from typing import Any
11
12
  from typing import cast
12
13
  from typing import Iterator
13
14
  from typing import List
14
- from typing import Mapping
15
15
  from typing import Optional
16
16
  from typing import Sequence
17
17
  from typing import Set
@@ -25,6 +25,7 @@ from .. import util
25
25
  from ..runtime import migration
26
26
  from ..util import compat
27
27
  from ..util import not_none
28
+ from ..util.pyfiles import _preserving_path_as_str
28
29
 
29
30
  if TYPE_CHECKING:
30
31
  from .revision import _GetRevArg
@@ -32,6 +33,7 @@ if TYPE_CHECKING:
32
33
  from .revision import Revision
33
34
  from ..config import Config
34
35
  from ..config import MessagingOptions
36
+ from ..config import PostWriteHookConfig
35
37
  from ..runtime.migration import RevisionStep
36
38
  from ..runtime.migration import StampStep
37
39
 
@@ -50,9 +52,6 @@ _only_source_rev_file = re.compile(r"(?!\.\#|__init__)(.*\.py)$")
50
52
  _legacy_rev = re.compile(r"([a-f0-9]+)\.py$")
51
53
  _slug_re = re.compile(r"\w+")
52
54
  _default_file_template = "%(rev)s_%(slug)s"
53
- _split_on_space_comma = re.compile(r", *|(?: +)")
54
-
55
- _split_on_space_comma_colon = re.compile(r", *|(?: +)|\:")
56
55
 
57
56
 
58
57
  class ScriptDirectory:
@@ -77,40 +76,55 @@ class ScriptDirectory:
77
76
 
78
77
  def __init__(
79
78
  self,
80
- dir: str, # noqa
79
+ dir: Union[str, os.PathLike[str]], # noqa: A002
81
80
  file_template: str = _default_file_template,
82
81
  truncate_slug_length: Optional[int] = 40,
83
- version_locations: Optional[List[str]] = None,
82
+ version_locations: Optional[
83
+ Sequence[Union[str, os.PathLike[str]]]
84
+ ] = None,
84
85
  sourceless: bool = False,
85
86
  output_encoding: str = "utf-8",
86
87
  timezone: Optional[str] = None,
87
- hook_config: Optional[Mapping[str, str]] = None,
88
+ hooks: list[PostWriteHookConfig] = [],
88
89
  recursive_version_locations: bool = False,
89
90
  messaging_opts: MessagingOptions = cast(
90
91
  "MessagingOptions", util.EMPTY_DICT
91
92
  ),
92
93
  ) -> None:
93
- self.dir = dir
94
+ self.dir = _preserving_path_as_str(dir)
95
+ self.version_locations = [
96
+ _preserving_path_as_str(p) for p in version_locations or ()
97
+ ]
94
98
  self.file_template = file_template
95
- self.version_locations = version_locations
96
99
  self.truncate_slug_length = truncate_slug_length or 40
97
100
  self.sourceless = sourceless
98
101
  self.output_encoding = output_encoding
99
102
  self.revision_map = revision.RevisionMap(self._load_revisions)
100
103
  self.timezone = timezone
101
- self.hook_config = hook_config
104
+ self.hooks = hooks
102
105
  self.recursive_version_locations = recursive_version_locations
103
106
  self.messaging_opts = messaging_opts
104
107
 
105
108
  if not os.access(dir, os.F_OK):
106
109
  raise util.CommandError(
107
- "Path doesn't exist: %r. Please use "
110
+ f"Path doesn't exist: {dir}. Please use "
108
111
  "the 'init' command to create a new "
109
- "scripts folder." % os.path.abspath(dir)
112
+ "scripts folder."
110
113
  )
111
114
 
112
115
  @property
113
116
  def versions(self) -> str:
117
+ """return a single version location based on the sole path passed
118
+ within version_locations.
119
+
120
+ If multiple version locations are configured, an error is raised.
121
+
122
+
123
+ """
124
+ return str(self._singular_version_location)
125
+
126
+ @util.memoized_property
127
+ def _singular_version_location(self) -> Path:
114
128
  loc = self._version_locations
115
129
  if len(loc) > 1:
116
130
  raise util.CommandError("Multiple version_locations present")
@@ -118,40 +132,31 @@ class ScriptDirectory:
118
132
  return loc[0]
119
133
 
120
134
  @util.memoized_property
121
- def _version_locations(self) -> Sequence[str]:
135
+ def _version_locations(self) -> Sequence[Path]:
122
136
  if self.version_locations:
123
137
  return [
124
- os.path.abspath(util.coerce_resource_to_filename(location))
138
+ util.coerce_resource_to_filename(location).absolute()
125
139
  for location in self.version_locations
126
140
  ]
127
141
  else:
128
- return (os.path.abspath(os.path.join(self.dir, "versions")),)
142
+ return [Path(self.dir, "versions").absolute()]
129
143
 
130
144
  def _load_revisions(self) -> Iterator[Script]:
131
- if self.version_locations:
132
- paths = [
133
- vers
134
- for vers in self._version_locations
135
- if os.path.exists(vers)
136
- ]
137
- else:
138
- paths = [self.versions]
145
+ paths = [vers for vers in self._version_locations if vers.exists()]
139
146
 
140
147
  dupes = set()
141
148
  for vers in paths:
142
149
  for file_path in Script._list_py_dir(self, vers):
143
- real_path = os.path.realpath(file_path)
150
+ real_path = file_path.resolve()
144
151
  if real_path in dupes:
145
152
  util.warn(
146
- "File %s loaded twice! ignoring. Please ensure "
147
- "version_locations is unique." % real_path
153
+ f"File {real_path} loaded twice! ignoring. "
154
+ "Please ensure version_locations is unique."
148
155
  )
149
156
  continue
150
157
  dupes.add(real_path)
151
158
 
152
- filename = os.path.basename(real_path)
153
- dir_name = os.path.dirname(real_path)
154
- script = Script._from_filename(self, dir_name, filename)
159
+ script = Script._from_path(self, real_path)
155
160
  if script is None:
156
161
  continue
157
162
  yield script
@@ -165,78 +170,38 @@ class ScriptDirectory:
165
170
  present.
166
171
 
167
172
  """
168
- script_location = config.get_main_option("script_location")
173
+ script_location = config.get_alembic_option("script_location")
169
174
  if script_location is None:
170
175
  raise util.CommandError(
171
- "No 'script_location' key " "found in configuration."
176
+ "No 'script_location' key found in configuration."
172
177
  )
173
178
  truncate_slug_length: Optional[int]
174
- tsl = config.get_main_option("truncate_slug_length")
179
+ tsl = config.get_alembic_option("truncate_slug_length")
175
180
  if tsl is not None:
176
181
  truncate_slug_length = int(tsl)
177
182
  else:
178
183
  truncate_slug_length = None
179
184
 
180
- version_locations_str = config.get_main_option("version_locations")
181
- version_locations: Optional[List[str]]
182
- if version_locations_str:
183
- version_path_separator = config.get_main_option(
184
- "version_path_separator"
185
- )
186
-
187
- split_on_path = {
188
- None: None,
189
- "space": " ",
190
- "newline": "\n",
191
- "os": os.pathsep,
192
- ":": ":",
193
- ";": ";",
194
- }
195
-
196
- try:
197
- split_char: Optional[str] = split_on_path[
198
- version_path_separator
199
- ]
200
- except KeyError as ke:
201
- raise ValueError(
202
- "'%s' is not a valid value for "
203
- "version_path_separator; "
204
- "expected 'space', 'newline', 'os', ':', ';'"
205
- % version_path_separator
206
- ) from ke
207
- else:
208
- if split_char is None:
209
- # legacy behaviour for backwards compatibility
210
- version_locations = _split_on_space_comma.split(
211
- version_locations_str
212
- )
213
- else:
214
- version_locations = [
215
- x.strip()
216
- for x in version_locations_str.split(split_char)
217
- if x
218
- ]
219
- else:
220
- version_locations = None
221
-
222
- prepend_sys_path = config.get_main_option("prepend_sys_path")
185
+ prepend_sys_path = config.get_prepend_sys_paths_list()
223
186
  if prepend_sys_path:
224
- sys.path[:0] = list(
225
- _split_on_space_comma_colon.split(prepend_sys_path)
226
- )
187
+ sys.path[:0] = prepend_sys_path
227
188
 
228
- rvl = config.get_main_option("recursive_version_locations") == "true"
189
+ rvl = (
190
+ config.get_alembic_option("recursive_version_locations") == "true"
191
+ )
229
192
  return ScriptDirectory(
230
193
  util.coerce_resource_to_filename(script_location),
231
- file_template=config.get_main_option(
194
+ file_template=config.get_alembic_option(
232
195
  "file_template", _default_file_template
233
196
  ),
234
197
  truncate_slug_length=truncate_slug_length,
235
- sourceless=config.get_main_option("sourceless") == "true",
236
- output_encoding=config.get_main_option("output_encoding", "utf-8"),
237
- version_locations=version_locations,
238
- timezone=config.get_main_option("timezone"),
239
- hook_config=config.get_section("post_write_hooks", {}),
198
+ sourceless=config.get_alembic_option("sourceless") == "true",
199
+ output_encoding=config.get_alembic_option(
200
+ "output_encoding", "utf-8"
201
+ ),
202
+ version_locations=config.get_version_locations_list(),
203
+ timezone=config.get_alembic_option("timezone"),
204
+ hooks=config.get_hooks_list(),
240
205
  recursive_version_locations=rvl,
241
206
  messaging_opts=config.messaging_opts,
242
207
  )
@@ -587,23 +552,32 @@ class ScriptDirectory:
587
552
 
588
553
  @property
589
554
  def env_py_location(self) -> str:
590
- return os.path.abspath(os.path.join(self.dir, "env.py"))
555
+ return str(Path(self.dir, "env.py"))
556
+
557
+ def _append_template(self, src: Path, dest: Path, **kw: Any) -> None:
558
+ with util.status(
559
+ f"Appending to existing {dest.absolute()}",
560
+ **self.messaging_opts,
561
+ ):
562
+ util.template_to_file(
563
+ src, dest, self.output_encoding, append=True, **kw
564
+ )
591
565
 
592
- def _generate_template(self, src: str, dest: str, **kw: Any) -> None:
566
+ def _generate_template(self, src: Path, dest: Path, **kw: Any) -> None:
593
567
  with util.status(
594
- f"Generating {os.path.abspath(dest)}", **self.messaging_opts
568
+ f"Generating {dest.absolute()}", **self.messaging_opts
595
569
  ):
596
570
  util.template_to_file(src, dest, self.output_encoding, **kw)
597
571
 
598
- def _copy_file(self, src: str, dest: str) -> None:
572
+ def _copy_file(self, src: Path, dest: Path) -> None:
599
573
  with util.status(
600
- f"Generating {os.path.abspath(dest)}", **self.messaging_opts
574
+ f"Generating {dest.absolute()}", **self.messaging_opts
601
575
  ):
602
576
  shutil.copy(src, dest)
603
577
 
604
- def _ensure_directory(self, path: str) -> None:
605
- path = os.path.abspath(path)
606
- if not os.path.exists(path):
578
+ def _ensure_directory(self, path: Path) -> None:
579
+ path = path.absolute()
580
+ if not path.exists():
607
581
  with util.status(
608
582
  f"Creating directory {path}", **self.messaging_opts
609
583
  ):
@@ -628,11 +602,10 @@ class ScriptDirectory:
628
602
  raise util.CommandError(
629
603
  "Can't locate timezone: %s" % self.timezone
630
604
  ) from None
631
- create_date = (
632
- datetime.datetime.utcnow()
633
- .replace(tzinfo=datetime.timezone.utc)
634
- .astimezone(tzinfo)
635
- )
605
+
606
+ create_date = datetime.datetime.now(
607
+ tz=datetime.timezone.utc
608
+ ).astimezone(tzinfo)
636
609
  else:
637
610
  create_date = datetime.datetime.now()
638
611
  return create_date
@@ -644,7 +617,8 @@ class ScriptDirectory:
644
617
  head: Optional[_RevIdType] = None,
645
618
  splice: Optional[bool] = False,
646
619
  branch_labels: Optional[_RevIdType] = None,
647
- version_path: Optional[str] = None,
620
+ version_path: Union[str, os.PathLike[str], None] = None,
621
+ file_template: Optional[str] = None,
648
622
  depends_on: Optional[_RevIdType] = None,
649
623
  **kw: Any,
650
624
  ) -> Optional[Script]:
@@ -697,7 +671,7 @@ class ScriptDirectory:
697
671
  for head_ in heads:
698
672
  if head_ is not None:
699
673
  assert isinstance(head_, Script)
700
- version_path = os.path.dirname(head_.path)
674
+ version_path = head_._script_path.parent
701
675
  break
702
676
  else:
703
677
  raise util.CommandError(
@@ -705,16 +679,19 @@ class ScriptDirectory:
705
679
  "please specify --version-path"
706
680
  )
707
681
  else:
708
- version_path = self.versions
682
+ version_path = self._singular_version_location
683
+ else:
684
+ version_path = Path(version_path)
709
685
 
710
- norm_path = os.path.normpath(os.path.abspath(version_path))
686
+ assert isinstance(version_path, Path)
687
+ norm_path = version_path.absolute()
711
688
  for vers_path in self._version_locations:
712
- if os.path.normpath(vers_path) == norm_path:
689
+ if vers_path.absolute() == norm_path:
713
690
  break
714
691
  else:
715
692
  raise util.CommandError(
716
- "Path %s is not represented in current "
717
- "version locations" % version_path
693
+ f"Path {version_path} is not represented in current "
694
+ "version locations"
718
695
  )
719
696
 
720
697
  if self.version_locations:
@@ -749,7 +726,7 @@ class ScriptDirectory:
749
726
  resolved_depends_on = None
750
727
 
751
728
  self._generate_template(
752
- os.path.join(self.dir, "script.py.mako"),
729
+ Path(self.dir, "script.py.mako"),
753
730
  path,
754
731
  up_revision=str(revid),
755
732
  down_revision=revision.tuple_rev_as_scalar(
@@ -763,7 +740,7 @@ class ScriptDirectory:
763
740
  **kw,
764
741
  )
765
742
 
766
- post_write_hooks = self.hook_config
743
+ post_write_hooks = self.hooks
767
744
  if post_write_hooks:
768
745
  write_hooks._run_hooks(path, post_write_hooks)
769
746
 
@@ -786,11 +763,11 @@ class ScriptDirectory:
786
763
 
787
764
  def _rev_path(
788
765
  self,
789
- path: str,
766
+ path: Union[str, os.PathLike[str]],
790
767
  rev_id: str,
791
768
  message: Optional[str],
792
769
  create_date: datetime.datetime,
793
- ) -> str:
770
+ ) -> Path:
794
771
  epoch = int(create_date.timestamp())
795
772
  slug = "_".join(_slug_re.findall(message or "")).lower()
796
773
  if len(slug) > self.truncate_slug_length:
@@ -809,7 +786,7 @@ class ScriptDirectory:
809
786
  "second": create_date.second,
810
787
  }
811
788
  )
812
- return os.path.join(path, filename)
789
+ return Path(path) / filename
813
790
 
814
791
 
815
792
  class Script(revision.Revision):
@@ -820,9 +797,14 @@ class Script(revision.Revision):
820
797
 
821
798
  """
822
799
 
823
- def __init__(self, module: ModuleType, rev_id: str, path: str):
800
+ def __init__(
801
+ self,
802
+ module: ModuleType,
803
+ rev_id: str,
804
+ path: Union[str, os.PathLike[str]],
805
+ ):
824
806
  self.module = module
825
- self.path = path
807
+ self.path = _preserving_path_as_str(path)
826
808
  super().__init__(
827
809
  rev_id,
828
810
  module.down_revision,
@@ -840,6 +822,10 @@ class Script(revision.Revision):
840
822
  path: str
841
823
  """Filesystem path of the script."""
842
824
 
825
+ @property
826
+ def _script_path(self) -> Path:
827
+ return Path(self.path)
828
+
843
829
  _db_current_indicator: Optional[bool] = None
844
830
  """Utility variable which when set will cause string output to indicate
845
831
  this is a "current" version in some database"""
@@ -972,36 +958,33 @@ class Script(revision.Revision):
972
958
  return util.format_as_comma(self._versioned_down_revisions)
973
959
 
974
960
  @classmethod
975
- def _from_path(
976
- cls, scriptdir: ScriptDirectory, path: str
977
- ) -> Optional[Script]:
978
- dir_, filename = os.path.split(path)
979
- return cls._from_filename(scriptdir, dir_, filename)
980
-
981
- @classmethod
982
- def _list_py_dir(cls, scriptdir: ScriptDirectory, path: str) -> List[str]:
961
+ def _list_py_dir(
962
+ cls, scriptdir: ScriptDirectory, path: Path
963
+ ) -> List[Path]:
983
964
  paths = []
984
- for root, dirs, files in os.walk(path, topdown=True):
985
- if root.endswith("__pycache__"):
965
+ for root, dirs, files in compat.path_walk(path, top_down=True):
966
+ if root.name.endswith("__pycache__"):
986
967
  # a special case - we may include these files
987
968
  # if a `sourceless` option is specified
988
969
  continue
989
970
 
990
971
  for filename in sorted(files):
991
- paths.append(os.path.join(root, filename))
972
+ paths.append(root / filename)
992
973
 
993
974
  if scriptdir.sourceless:
994
975
  # look for __pycache__
995
- py_cache_path = os.path.join(root, "__pycache__")
996
- if os.path.exists(py_cache_path):
976
+ py_cache_path = root / "__pycache__"
977
+ if py_cache_path.exists():
997
978
  # add all files from __pycache__ whose filename is not
998
979
  # already in the names we got from the version directory.
999
980
  # add as relative paths including __pycache__ token
1000
- names = {filename.split(".")[0] for filename in files}
981
+ names = {
982
+ Path(filename).name.split(".")[0] for filename in files
983
+ }
1001
984
  paths.extend(
1002
- os.path.join(py_cache_path, pyc)
1003
- for pyc in os.listdir(py_cache_path)
1004
- if pyc.split(".")[0] not in names
985
+ py_cache_path / pyc
986
+ for pyc in py_cache_path.iterdir()
987
+ if pyc.name.split(".")[0] not in names
1005
988
  )
1006
989
 
1007
990
  if not scriptdir.recursive_version_locations:
@@ -1016,9 +999,13 @@ class Script(revision.Revision):
1016
999
  return paths
1017
1000
 
1018
1001
  @classmethod
1019
- def _from_filename(
1020
- cls, scriptdir: ScriptDirectory, dir_: str, filename: str
1002
+ def _from_path(
1003
+ cls, scriptdir: ScriptDirectory, path: Union[str, os.PathLike[str]]
1021
1004
  ) -> Optional[Script]:
1005
+
1006
+ path = Path(path)
1007
+ dir_, filename = path.parent, path.name
1008
+
1022
1009
  if scriptdir.sourceless:
1023
1010
  py_match = _sourceless_rev_file.match(filename)
1024
1011
  else:
@@ -1036,8 +1023,8 @@ class Script(revision.Revision):
1036
1023
  is_c = is_o = False
1037
1024
 
1038
1025
  if is_o or is_c:
1039
- py_exists = os.path.exists(os.path.join(dir_, py_filename))
1040
- pyc_exists = os.path.exists(os.path.join(dir_, py_filename + "c"))
1026
+ py_exists = (dir_ / py_filename).exists()
1027
+ pyc_exists = (dir_ / (py_filename + "c")).exists()
1041
1028
 
1042
1029
  # prefer .py over .pyc because we'd like to get the
1043
1030
  # source encoding; prefer .pyc over .pyo because we'd like to
@@ -1053,14 +1040,14 @@ class Script(revision.Revision):
1053
1040
  m = _legacy_rev.match(filename)
1054
1041
  if not m:
1055
1042
  raise util.CommandError(
1056
- "Could not determine revision id from filename %s. "
1043
+ "Could not determine revision id from "
1044
+ f"filename {filename}. "
1057
1045
  "Be sure the 'revision' variable is "
1058
1046
  "declared inside the script (please see 'Upgrading "
1059
1047
  "from Alembic 0.1 to 0.2' in the documentation)."
1060
- % filename
1061
1048
  )
1062
1049
  else:
1063
1050
  revision = m.group(1)
1064
1051
  else:
1065
1052
  revision = module.revision
1066
- return Script(module, revision, os.path.join(dir_, filename))
1053
+ return Script(module, revision, dir_ / filename)
@@ -1708,7 +1708,7 @@ def tuple_rev_as_scalar(rev: None) -> None: ...
1708
1708
 
1709
1709
  @overload
1710
1710
  def tuple_rev_as_scalar(
1711
- rev: Union[Tuple[_T, ...], List[_T]]
1711
+ rev: Union[Tuple[_T, ...], List[_T]],
1712
1712
  ) -> Union[_T, Tuple[_T, ...], List[_T]]: ...
1713
1713
 
1714
1714
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import os
6
7
  import shlex
7
8
  import subprocess
8
9
  import sys
@@ -10,13 +11,16 @@ from typing import Any
10
11
  from typing import Callable
11
12
  from typing import Dict
12
13
  from typing import List
13
- from typing import Mapping
14
14
  from typing import Optional
15
+ from typing import TYPE_CHECKING
15
16
  from typing import Union
16
17
 
17
18
  from .. import util
18
19
  from ..util import compat
20
+ from ..util.pyfiles import _preserving_path_as_str
19
21
 
22
+ if TYPE_CHECKING:
23
+ from ..config import PostWriteHookConfig
20
24
 
21
25
  REVISION_SCRIPT_TOKEN = "REVISION_SCRIPT_FILENAME"
22
26
 
@@ -43,16 +47,19 @@ def register(name: str) -> Callable:
43
47
 
44
48
 
45
49
  def _invoke(
46
- name: str, revision: str, options: Mapping[str, Union[str, int]]
50
+ name: str,
51
+ revision_path: Union[str, os.PathLike[str]],
52
+ options: PostWriteHookConfig,
47
53
  ) -> Any:
48
54
  """Invokes the formatter registered for the given name.
49
55
 
50
56
  :param name: The name of a formatter in the registry
51
- :param revision: A :class:`.MigrationRevision` instance
57
+ :param revision: string path to the revision file
52
58
  :param options: A dict containing kwargs passed to the
53
59
  specified formatter.
54
60
  :raises: :class:`alembic.util.CommandError`
55
61
  """
62
+ revision_path = _preserving_path_as_str(revision_path)
56
63
  try:
57
64
  hook = _registry[name]
58
65
  except KeyError as ke:
@@ -60,36 +67,28 @@ def _invoke(
60
67
  f"No formatter with name '{name}' registered"
61
68
  ) from ke
62
69
  else:
63
- return hook(revision, options)
70
+ return hook(revision_path, options)
64
71
 
65
72
 
66
- def _run_hooks(path: str, hook_config: Mapping[str, str]) -> None:
73
+ def _run_hooks(
74
+ path: Union[str, os.PathLike[str]], hooks: list[PostWriteHookConfig]
75
+ ) -> None:
67
76
  """Invoke hooks for a generated revision."""
68
77
 
69
- from .base import _split_on_space_comma
70
-
71
- names = _split_on_space_comma.split(hook_config.get("hooks", ""))
72
-
73
- for name in names:
74
- if not name:
75
- continue
76
- opts = {
77
- key[len(name) + 1 :]: hook_config[key]
78
- for key in hook_config
79
- if key.startswith(name + ".")
80
- }
81
- opts["_hook_name"] = name
78
+ for hook in hooks:
79
+ name = hook["_hook_name"]
82
80
  try:
83
- type_ = opts["type"]
81
+ type_ = hook["type"]
84
82
  except KeyError as ke:
85
83
  raise util.CommandError(
86
- f"Key {name}.type is required for post write hook {name!r}"
84
+ f"Key '{name}.type' (or 'type' in toml) is required "
85
+ f"for post write hook {name!r}"
87
86
  ) from ke
88
87
  else:
89
88
  with util.status(
90
89
  f"Running post write hook {name!r}", newline=True
91
90
  ):
92
- _invoke(type_, path, opts)
91
+ _invoke(type_, path, hook)
93
92
 
94
93
 
95
94
  def _parse_cmdline_options(cmdline_options_str: str, path: str) -> List[str]: