alembic 1.15.2__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 +25 -4
- alembic/command.py +112 -37
- alembic/config.py +574 -222
- alembic/ddl/base.py +31 -7
- alembic/ddl/impl.py +23 -5
- 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 +24 -5
- alembic/operations/base.py +18 -3
- alembic/operations/ops.py +49 -8
- 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.2.dist-info → alembic-1.16.0.dist-info}/METADATA +3 -3
- {alembic-1.15.2.dist-info → alembic-1.16.0.dist-info}/RECORD +41 -36
- {alembic-1.15.2.dist-info → alembic-1.16.0.dist-info}/WHEEL +1 -1
- {alembic-1.15.2.dist-info → alembic-1.16.0.dist-info}/entry_points.txt +0 -0
- {alembic-1.15.2.dist-info → alembic-1.16.0.dist-info}/licenses/LICENSE +0 -0
- {alembic-1.15.2.dist-info → alembic-1.16.0.dist-info}/top_level.txt +0 -0
alembic/config.py
CHANGED
@@ -5,6 +5,8 @@ from argparse import Namespace
|
|
5
5
|
from configparser import ConfigParser
|
6
6
|
import inspect
|
7
7
|
import os
|
8
|
+
from pathlib import Path
|
9
|
+
import re
|
8
10
|
import sys
|
9
11
|
from typing import Any
|
10
12
|
from typing import cast
|
@@ -12,6 +14,7 @@ from typing import Dict
|
|
12
14
|
from typing import Mapping
|
13
15
|
from typing import Optional
|
14
16
|
from typing import overload
|
17
|
+
from typing import Protocol
|
15
18
|
from typing import Sequence
|
16
19
|
from typing import TextIO
|
17
20
|
from typing import Union
|
@@ -22,6 +25,7 @@ from . import __version__
|
|
22
25
|
from . import command
|
23
26
|
from . import util
|
24
27
|
from .util import compat
|
28
|
+
from .util.pyfiles import _preserving_path_as_str
|
25
29
|
|
26
30
|
|
27
31
|
class Config:
|
@@ -81,12 +85,13 @@ class Config:
|
|
81
85
|
Defaults to ``sys.stdout``.
|
82
86
|
|
83
87
|
:param config_args: A dictionary of keys and values that will be used
|
84
|
-
for substitution in the alembic config file
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
to
|
88
|
+
for substitution in the alembic config file, as well as the pyproject.toml
|
89
|
+
file, depending on which / both are used. The dictionary as given is
|
90
|
+
**copied** to two new, independent dictionaries, stored locally under the
|
91
|
+
attributes ``.config_args`` and ``.toml_args``. Both of these
|
92
|
+
dictionaries will also be populated with the replacement variable
|
93
|
+
``%(here)s``, which refers to the location of the .ini and/or .toml file
|
94
|
+
as appropriate.
|
90
95
|
|
91
96
|
:param attributes: optional dictionary of arbitrary Python keys/values,
|
92
97
|
which will be populated into the :attr:`.Config.attributes` dictionary.
|
@@ -100,6 +105,7 @@ class Config:
|
|
100
105
|
def __init__(
|
101
106
|
self,
|
102
107
|
file_: Union[str, os.PathLike[str], None] = None,
|
108
|
+
toml_file: Union[str, os.PathLike[str], None] = None,
|
103
109
|
ini_section: str = "alembic",
|
104
110
|
output_buffer: Optional[TextIO] = None,
|
105
111
|
stdout: TextIO = sys.stdout,
|
@@ -108,12 +114,18 @@ class Config:
|
|
108
114
|
attributes: Optional[Dict[str, Any]] = None,
|
109
115
|
) -> None:
|
110
116
|
"""Construct a new :class:`.Config`"""
|
111
|
-
self.config_file_name =
|
117
|
+
self.config_file_name = (
|
118
|
+
_preserving_path_as_str(file_) if file_ else None
|
119
|
+
)
|
120
|
+
self.toml_file_name = (
|
121
|
+
_preserving_path_as_str(toml_file) if toml_file else None
|
122
|
+
)
|
112
123
|
self.config_ini_section = ini_section
|
113
124
|
self.output_buffer = output_buffer
|
114
125
|
self.stdout = stdout
|
115
126
|
self.cmd_opts = cmd_opts
|
116
127
|
self.config_args = dict(config_args)
|
128
|
+
self.toml_args = dict(config_args)
|
117
129
|
if attributes:
|
118
130
|
self.attributes.update(attributes)
|
119
131
|
|
@@ -129,9 +141,28 @@ class Config:
|
|
129
141
|
|
130
142
|
"""
|
131
143
|
|
132
|
-
config_file_name:
|
144
|
+
config_file_name: Optional[str] = None
|
133
145
|
"""Filesystem path to the .ini file in use."""
|
134
146
|
|
147
|
+
toml_file_name: Optional[str] = None
|
148
|
+
"""Filesystem path to the pyproject.toml file in use.
|
149
|
+
|
150
|
+
.. versionadded:: 1.16.0
|
151
|
+
|
152
|
+
"""
|
153
|
+
|
154
|
+
@property
|
155
|
+
def _config_file_path(self) -> Optional[Path]:
|
156
|
+
if self.config_file_name is None:
|
157
|
+
return None
|
158
|
+
return Path(self.config_file_name)
|
159
|
+
|
160
|
+
@property
|
161
|
+
def _toml_file_path(self) -> Optional[Path]:
|
162
|
+
if self.toml_file_name is None:
|
163
|
+
return None
|
164
|
+
return Path(self.toml_file_name)
|
165
|
+
|
135
166
|
config_ini_section: str = None # type:ignore[assignment]
|
136
167
|
"""Name of the config file section to read basic configuration
|
137
168
|
from. Defaults to ``alembic``, that is the ``[alembic]`` section
|
@@ -187,36 +218,67 @@ class Config:
|
|
187
218
|
def file_config(self) -> ConfigParser:
|
188
219
|
"""Return the underlying ``ConfigParser`` object.
|
189
220
|
|
190
|
-
|
221
|
+
Dir*-ect access to the .ini file is available here,
|
191
222
|
though the :meth:`.Config.get_section` and
|
192
223
|
:meth:`.Config.get_main_option`
|
193
224
|
methods provide a possibly simpler interface.
|
194
225
|
|
195
226
|
"""
|
196
227
|
|
197
|
-
if self.
|
198
|
-
here =
|
228
|
+
if self._config_file_path:
|
229
|
+
here = self._config_file_path.absolute().parent
|
199
230
|
else:
|
200
|
-
here =
|
201
|
-
self.config_args["here"] = here
|
231
|
+
here = Path()
|
232
|
+
self.config_args["here"] = here.as_posix()
|
202
233
|
file_config = ConfigParser(self.config_args)
|
203
|
-
if self.
|
204
|
-
compat.read_config_parser(file_config, [self.
|
234
|
+
if self._config_file_path:
|
235
|
+
compat.read_config_parser(file_config, [self._config_file_path])
|
205
236
|
else:
|
206
237
|
file_config.add_section(self.config_ini_section)
|
207
238
|
return file_config
|
208
239
|
|
240
|
+
@util.memoized_property
|
241
|
+
def toml_alembic_config(self) -> Mapping[str, Any]:
|
242
|
+
"""Return a dictionary of the [tool.alembic] section from
|
243
|
+
pyproject.toml"""
|
244
|
+
|
245
|
+
if self._toml_file_path and self._toml_file_path.exists():
|
246
|
+
|
247
|
+
here = self._toml_file_path.absolute().parent
|
248
|
+
self.toml_args["here"] = here.as_posix()
|
249
|
+
|
250
|
+
with open(self._toml_file_path, "rb") as f:
|
251
|
+
toml_data = compat.tomllib.load(f)
|
252
|
+
data = toml_data.get("tool", {}).get("alembic", {})
|
253
|
+
if not isinstance(data, dict):
|
254
|
+
raise util.CommandError("Incorrect TOML format")
|
255
|
+
return data
|
256
|
+
|
257
|
+
else:
|
258
|
+
return {}
|
259
|
+
|
209
260
|
def get_template_directory(self) -> str:
|
210
261
|
"""Return the directory where Alembic setup templates are found.
|
211
262
|
|
212
263
|
This method is used by the alembic ``init`` and ``list_templates``
|
213
264
|
commands.
|
214
265
|
|
266
|
+
"""
|
267
|
+
return self._get_template_path().as_posix()
|
268
|
+
|
269
|
+
def _get_template_path(self) -> Path:
|
270
|
+
"""Return the directory where Alembic setup templates are found.
|
271
|
+
|
272
|
+
This method is used by the alembic ``init`` and ``list_templates``
|
273
|
+
commands.
|
274
|
+
|
275
|
+
.. versionadded:: 1.16.0
|
276
|
+
|
215
277
|
"""
|
216
278
|
import alembic
|
217
279
|
|
218
|
-
package_dir =
|
219
|
-
return
|
280
|
+
package_dir = Path(alembic.__file__).absolute().parent
|
281
|
+
return package_dir / "templates"
|
220
282
|
|
221
283
|
@overload
|
222
284
|
def get_section(
|
@@ -278,6 +340,12 @@ class Config:
|
|
278
340
|
The value here will override whatever was in the .ini
|
279
341
|
file.
|
280
342
|
|
343
|
+
Does **NOT** consume from the pyproject.toml file.
|
344
|
+
|
345
|
+
.. seealso::
|
346
|
+
|
347
|
+
:meth:`.Config.get_alembic_option` - includes pyproject support
|
348
|
+
|
281
349
|
:param section: name of the section
|
282
350
|
|
283
351
|
:param name: name of the value
|
@@ -326,9 +394,85 @@ class Config:
|
|
326
394
|
section, unless the ``-n/--name`` flag were used to
|
327
395
|
indicate a different section.
|
328
396
|
|
397
|
+
Does **NOT** consume from the pyproject.toml file.
|
398
|
+
|
399
|
+
.. seealso::
|
400
|
+
|
401
|
+
:meth:`.Config.get_alembic_option` - includes pyproject support
|
402
|
+
|
329
403
|
"""
|
330
404
|
return self.get_section_option(self.config_ini_section, name, default)
|
331
405
|
|
406
|
+
@overload
|
407
|
+
def get_alembic_option(self, name: str, default: str) -> str: ...
|
408
|
+
|
409
|
+
@overload
|
410
|
+
def get_alembic_option(
|
411
|
+
self, name: str, default: Optional[str] = None
|
412
|
+
) -> Optional[str]: ...
|
413
|
+
|
414
|
+
def get_alembic_option(
|
415
|
+
self, name: str, default: Optional[str] = None
|
416
|
+
) -> Union[None, str, list[str], dict[str, str], list[dict[str, str]]]:
|
417
|
+
"""Return an option from the "[alembic]" or "[tool.alembic]" section
|
418
|
+
of the configparser-parsed .ini file (e.g. ``alembic.ini``) or
|
419
|
+
toml-parsed ``pyproject.toml`` file.
|
420
|
+
|
421
|
+
The value returned is expected to be None, string, list of strings,
|
422
|
+
or dictionary of strings. Within each type of string value, the
|
423
|
+
``%(here)s`` token is substituted out with the absolute path of the
|
424
|
+
``pyproject.toml`` file, as are other tokens which are extracted from
|
425
|
+
the :paramref:`.Config.config_args` dictionary.
|
426
|
+
|
427
|
+
Searches always prioritize the configparser namespace first, before
|
428
|
+
searching in the toml namespace.
|
429
|
+
|
430
|
+
If Alembic was run using the ``-n/--name`` flag to indicate an
|
431
|
+
alternate main section name, this is taken into account **only** for
|
432
|
+
the configparser-parsed .ini file. The section name in toml is always
|
433
|
+
``[tool.alembic]``.
|
434
|
+
|
435
|
+
|
436
|
+
.. versionadded:: 1.16.0
|
437
|
+
|
438
|
+
"""
|
439
|
+
|
440
|
+
if self.file_config.has_option(self.config_ini_section, name):
|
441
|
+
return self.file_config.get(self.config_ini_section, name)
|
442
|
+
else:
|
443
|
+
return self._get_toml_config_value(name, default=default)
|
444
|
+
|
445
|
+
def _get_toml_config_value(
|
446
|
+
self, name: str, default: Optional[Any] = None
|
447
|
+
) -> Union[None, str, list[str], dict[str, str], list[dict[str, str]]]:
|
448
|
+
USE_DEFAULT = object()
|
449
|
+
value: Union[None, str, list[str], dict[str, str]] = (
|
450
|
+
self.toml_alembic_config.get(name, USE_DEFAULT)
|
451
|
+
)
|
452
|
+
if value is USE_DEFAULT:
|
453
|
+
return default
|
454
|
+
if value is not None:
|
455
|
+
if isinstance(value, str):
|
456
|
+
value = value % (self.toml_args)
|
457
|
+
elif isinstance(value, list):
|
458
|
+
if value and isinstance(value[0], dict):
|
459
|
+
value = [
|
460
|
+
{k: v % (self.toml_args) for k, v in dv.items()}
|
461
|
+
for dv in value
|
462
|
+
]
|
463
|
+
else:
|
464
|
+
value = cast(
|
465
|
+
"list[str]", [v % (self.toml_args) for v in value]
|
466
|
+
)
|
467
|
+
elif isinstance(value, dict):
|
468
|
+
value = cast(
|
469
|
+
"dict[str, str]",
|
470
|
+
{k: v % (self.toml_args) for k, v in value.items()},
|
471
|
+
)
|
472
|
+
else:
|
473
|
+
raise util.CommandError("unsupported TOML value type")
|
474
|
+
return value
|
475
|
+
|
332
476
|
@util.memoized_property
|
333
477
|
def messaging_opts(self) -> MessagingOptions:
|
334
478
|
"""The messaging options."""
|
@@ -339,181 +483,313 @@ class Config:
|
|
339
483
|
),
|
340
484
|
)
|
341
485
|
|
486
|
+
def _get_file_separator_char(self, *names: str) -> Optional[str]:
|
487
|
+
for name in names:
|
488
|
+
separator = self.get_main_option(name)
|
489
|
+
if separator is not None:
|
490
|
+
break
|
491
|
+
else:
|
492
|
+
return None
|
493
|
+
|
494
|
+
split_on_path = {
|
495
|
+
"space": " ",
|
496
|
+
"newline": "\n",
|
497
|
+
"os": os.pathsep,
|
498
|
+
":": ":",
|
499
|
+
";": ";",
|
500
|
+
}
|
501
|
+
|
502
|
+
try:
|
503
|
+
sep = split_on_path[separator]
|
504
|
+
except KeyError as ke:
|
505
|
+
raise ValueError(
|
506
|
+
"'%s' is not a valid value for %s; "
|
507
|
+
"expected 'space', 'newline', 'os', ':', ';'"
|
508
|
+
% (separator, name)
|
509
|
+
) from ke
|
510
|
+
else:
|
511
|
+
if name == "version_path_separator":
|
512
|
+
util.warn_deprecated(
|
513
|
+
"The version_path_separator configuration parameter "
|
514
|
+
"is deprecated; please use path_separator"
|
515
|
+
)
|
516
|
+
return sep
|
517
|
+
|
518
|
+
def get_version_locations_list(self) -> Optional[list[str]]:
|
519
|
+
|
520
|
+
version_locations_str = self.file_config.get(
|
521
|
+
self.config_ini_section, "version_locations", fallback=None
|
522
|
+
)
|
523
|
+
|
524
|
+
if version_locations_str:
|
525
|
+
split_char = self._get_file_separator_char(
|
526
|
+
"path_separator", "version_path_separator"
|
527
|
+
)
|
528
|
+
|
529
|
+
if split_char is None:
|
530
|
+
|
531
|
+
# legacy behaviour for backwards compatibility
|
532
|
+
util.warn_deprecated(
|
533
|
+
"No path_separator found in configuration; "
|
534
|
+
"falling back to legacy splitting on spaces/commas "
|
535
|
+
"for version_locations. Consider adding "
|
536
|
+
"path_separator=os to Alembic config."
|
537
|
+
)
|
538
|
+
|
539
|
+
_split_on_space_comma = re.compile(r", *|(?: +)")
|
540
|
+
return _split_on_space_comma.split(version_locations_str)
|
541
|
+
else:
|
542
|
+
return [
|
543
|
+
x.strip()
|
544
|
+
for x in version_locations_str.split(split_char)
|
545
|
+
if x
|
546
|
+
]
|
547
|
+
else:
|
548
|
+
return cast(
|
549
|
+
"list[str]",
|
550
|
+
self._get_toml_config_value("version_locations", None),
|
551
|
+
)
|
552
|
+
|
553
|
+
def get_prepend_sys_paths_list(self) -> Optional[list[str]]:
|
554
|
+
prepend_sys_path_str = self.file_config.get(
|
555
|
+
self.config_ini_section, "prepend_sys_path", fallback=None
|
556
|
+
)
|
557
|
+
|
558
|
+
if prepend_sys_path_str:
|
559
|
+
split_char = self._get_file_separator_char("path_separator")
|
560
|
+
|
561
|
+
if split_char is None:
|
562
|
+
|
563
|
+
# legacy behaviour for backwards compatibility
|
564
|
+
util.warn_deprecated(
|
565
|
+
"No path_separator found in configuration; "
|
566
|
+
"falling back to legacy splitting on spaces, commas, "
|
567
|
+
"and colons for prepend_sys_path. Consider adding "
|
568
|
+
"path_separator=os to Alembic config."
|
569
|
+
)
|
570
|
+
|
571
|
+
_split_on_space_comma_colon = re.compile(r", *|(?: +)|\:")
|
572
|
+
return _split_on_space_comma_colon.split(prepend_sys_path_str)
|
573
|
+
else:
|
574
|
+
return [
|
575
|
+
x.strip()
|
576
|
+
for x in prepend_sys_path_str.split(split_char)
|
577
|
+
if x
|
578
|
+
]
|
579
|
+
else:
|
580
|
+
return cast(
|
581
|
+
"list[str]",
|
582
|
+
self._get_toml_config_value("prepend_sys_path", None),
|
583
|
+
)
|
584
|
+
|
585
|
+
def get_hooks_list(self) -> list[PostWriteHookConfig]:
|
586
|
+
|
587
|
+
hooks: list[PostWriteHookConfig] = []
|
588
|
+
|
589
|
+
if not self.file_config.has_section("post_write_hooks"):
|
590
|
+
toml_hook_config = cast(
|
591
|
+
"list[dict[str, str]]",
|
592
|
+
self._get_toml_config_value("post_write_hooks", []),
|
593
|
+
)
|
594
|
+
for cfg in toml_hook_config:
|
595
|
+
opts = dict(cfg)
|
596
|
+
opts["_hook_name"] = opts.pop("name")
|
597
|
+
hooks.append(opts)
|
598
|
+
|
599
|
+
else:
|
600
|
+
_split_on_space_comma = re.compile(r", *|(?: +)")
|
601
|
+
ini_hook_config = self.get_section("post_write_hooks", {})
|
602
|
+
names = _split_on_space_comma.split(
|
603
|
+
ini_hook_config.get("hooks", "")
|
604
|
+
)
|
605
|
+
|
606
|
+
for name in names:
|
607
|
+
if not name:
|
608
|
+
continue
|
609
|
+
opts = {
|
610
|
+
key[len(name) + 1 :]: ini_hook_config[key]
|
611
|
+
for key in ini_hook_config
|
612
|
+
if key.startswith(name + ".")
|
613
|
+
}
|
614
|
+
|
615
|
+
opts["_hook_name"] = name
|
616
|
+
hooks.append(opts)
|
617
|
+
|
618
|
+
return hooks
|
619
|
+
|
620
|
+
|
621
|
+
PostWriteHookConfig = Mapping[str, str]
|
622
|
+
|
342
623
|
|
343
624
|
class MessagingOptions(TypedDict, total=False):
|
344
625
|
quiet: bool
|
345
626
|
|
346
627
|
|
628
|
+
class CommandFunction(Protocol):
|
629
|
+
"""A function that may be registered in the CLI as an alembic command.
|
630
|
+
It must be a named function and it must accept a :class:`.Config` object
|
631
|
+
as the first argument.
|
632
|
+
|
633
|
+
.. versionadded:: 1.15.3
|
634
|
+
|
635
|
+
"""
|
636
|
+
|
637
|
+
__name__: str
|
638
|
+
|
639
|
+
def __call__(self, config: Config, *args: Any, **kwargs: Any) -> Any: ...
|
640
|
+
|
641
|
+
|
347
642
|
class CommandLine:
|
643
|
+
"""Provides the command line interface to Alembic."""
|
644
|
+
|
348
645
|
def __init__(self, prog: Optional[str] = None) -> None:
|
349
646
|
self._generate_args(prog)
|
350
647
|
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
"
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
"
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
"
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
"
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
"revision": "revision identifier",
|
495
|
-
"revisions": "one or more revisions, or 'heads' for all heads",
|
496
|
-
}
|
497
|
-
for arg in kwargs:
|
498
|
-
if arg in kwargs_opts:
|
499
|
-
args = kwargs_opts[arg]
|
500
|
-
args, kw = args[0:-1], args[-1]
|
501
|
-
parser.add_argument(*args, **kw)
|
502
|
-
|
503
|
-
for arg in positional:
|
504
|
-
if (
|
505
|
-
arg == "revisions"
|
506
|
-
or fn in positional_translations
|
507
|
-
and positional_translations[fn][arg] == "revisions"
|
508
|
-
):
|
509
|
-
subparser.add_argument(
|
510
|
-
"revisions",
|
511
|
-
nargs="+",
|
512
|
-
help=positional_help.get("revisions"),
|
513
|
-
)
|
514
|
-
else:
|
515
|
-
subparser.add_argument(arg, help=positional_help.get(arg))
|
648
|
+
_KWARGS_OPTS = {
|
649
|
+
"template": (
|
650
|
+
"-t",
|
651
|
+
"--template",
|
652
|
+
dict(
|
653
|
+
default="generic",
|
654
|
+
type=str,
|
655
|
+
help="Setup template for use with 'init'",
|
656
|
+
),
|
657
|
+
),
|
658
|
+
"message": (
|
659
|
+
"-m",
|
660
|
+
"--message",
|
661
|
+
dict(type=str, help="Message string to use with 'revision'"),
|
662
|
+
),
|
663
|
+
"sql": (
|
664
|
+
"--sql",
|
665
|
+
dict(
|
666
|
+
action="store_true",
|
667
|
+
help="Don't emit SQL to database - dump to "
|
668
|
+
"standard output/file instead. See docs on "
|
669
|
+
"offline mode.",
|
670
|
+
),
|
671
|
+
),
|
672
|
+
"tag": (
|
673
|
+
"--tag",
|
674
|
+
dict(
|
675
|
+
type=str,
|
676
|
+
help="Arbitrary 'tag' name - can be used by "
|
677
|
+
"custom env.py scripts.",
|
678
|
+
),
|
679
|
+
),
|
680
|
+
"head": (
|
681
|
+
"--head",
|
682
|
+
dict(
|
683
|
+
type=str,
|
684
|
+
help="Specify head revision or <branchname>@head "
|
685
|
+
"to base new revision on.",
|
686
|
+
),
|
687
|
+
),
|
688
|
+
"splice": (
|
689
|
+
"--splice",
|
690
|
+
dict(
|
691
|
+
action="store_true",
|
692
|
+
help="Allow a non-head revision as the 'head' to splice onto",
|
693
|
+
),
|
694
|
+
),
|
695
|
+
"depends_on": (
|
696
|
+
"--depends-on",
|
697
|
+
dict(
|
698
|
+
action="append",
|
699
|
+
help="Specify one or more revision identifiers "
|
700
|
+
"which this revision should depend on.",
|
701
|
+
),
|
702
|
+
),
|
703
|
+
"rev_id": (
|
704
|
+
"--rev-id",
|
705
|
+
dict(
|
706
|
+
type=str,
|
707
|
+
help="Specify a hardcoded revision id instead of "
|
708
|
+
"generating one",
|
709
|
+
),
|
710
|
+
),
|
711
|
+
"version_path": (
|
712
|
+
"--version-path",
|
713
|
+
dict(
|
714
|
+
type=str,
|
715
|
+
help="Specify specific path from config for version file",
|
716
|
+
),
|
717
|
+
),
|
718
|
+
"branch_label": (
|
719
|
+
"--branch-label",
|
720
|
+
dict(
|
721
|
+
type=str,
|
722
|
+
help="Specify a branch label to apply to the new revision",
|
723
|
+
),
|
724
|
+
),
|
725
|
+
"verbose": (
|
726
|
+
"-v",
|
727
|
+
"--verbose",
|
728
|
+
dict(action="store_true", help="Use more verbose output"),
|
729
|
+
),
|
730
|
+
"resolve_dependencies": (
|
731
|
+
"--resolve-dependencies",
|
732
|
+
dict(
|
733
|
+
action="store_true",
|
734
|
+
help="Treat dependency versions as down revisions",
|
735
|
+
),
|
736
|
+
),
|
737
|
+
"autogenerate": (
|
738
|
+
"--autogenerate",
|
739
|
+
dict(
|
740
|
+
action="store_true",
|
741
|
+
help="Populate revision script with candidate "
|
742
|
+
"migration operations, based on comparison "
|
743
|
+
"of database to model.",
|
744
|
+
),
|
745
|
+
),
|
746
|
+
"rev_range": (
|
747
|
+
"-r",
|
748
|
+
"--rev-range",
|
749
|
+
dict(
|
750
|
+
action="store",
|
751
|
+
help="Specify a revision range; format is [start]:[end]",
|
752
|
+
),
|
753
|
+
),
|
754
|
+
"indicate_current": (
|
755
|
+
"-i",
|
756
|
+
"--indicate-current",
|
757
|
+
dict(
|
758
|
+
action="store_true",
|
759
|
+
help="Indicate the current revision",
|
760
|
+
),
|
761
|
+
),
|
762
|
+
"purge": (
|
763
|
+
"--purge",
|
764
|
+
dict(
|
765
|
+
action="store_true",
|
766
|
+
help="Unconditionally erase the version table before stamping",
|
767
|
+
),
|
768
|
+
),
|
769
|
+
"package": (
|
770
|
+
"--package",
|
771
|
+
dict(
|
772
|
+
action="store_true",
|
773
|
+
help="Write empty __init__.py files to the "
|
774
|
+
"environment and version locations",
|
775
|
+
),
|
776
|
+
),
|
777
|
+
}
|
778
|
+
_POSITIONAL_OPTS = {
|
779
|
+
"directory": dict(help="location of scripts directory"),
|
780
|
+
"revision": dict(
|
781
|
+
help="revision identifier",
|
782
|
+
),
|
783
|
+
"revisions": dict(
|
784
|
+
nargs="+",
|
785
|
+
help="one or more revisions, or 'heads' for all heads",
|
786
|
+
),
|
787
|
+
}
|
788
|
+
_POSITIONAL_TRANSLATIONS: dict[Any, dict[str, str]] = {
|
789
|
+
command.stamp: {"revision": "revisions"}
|
790
|
+
}
|
516
791
|
|
792
|
+
def _generate_args(self, prog: Optional[str]) -> None:
|
517
793
|
parser = ArgumentParser(prog=prog)
|
518
794
|
|
519
795
|
parser.add_argument(
|
@@ -522,17 +798,19 @@ class CommandLine:
|
|
522
798
|
parser.add_argument(
|
523
799
|
"-c",
|
524
800
|
"--config",
|
525
|
-
|
526
|
-
default=os.environ.get("ALEMBIC_CONFIG", "alembic.ini"),
|
801
|
+
action="append",
|
527
802
|
help="Alternate config file; defaults to value of "
|
528
|
-
'ALEMBIC_CONFIG environment variable, or "alembic.ini"'
|
803
|
+
'ALEMBIC_CONFIG environment variable, or "alembic.ini". '
|
804
|
+
"May also refer to pyproject.toml file. May be specified twice "
|
805
|
+
"to reference both files separately",
|
529
806
|
)
|
530
807
|
parser.add_argument(
|
531
808
|
"-n",
|
532
809
|
"--name",
|
533
810
|
type=str,
|
534
811
|
default="alembic",
|
535
|
-
help="Name of section in .ini file to
|
812
|
+
help="Name of section in .ini file to use for Alembic config "
|
813
|
+
"(only applies to configparser config, not toml)",
|
536
814
|
)
|
537
815
|
parser.add_argument(
|
538
816
|
"-x",
|
@@ -552,50 +830,81 @@ class CommandLine:
|
|
552
830
|
action="store_true",
|
553
831
|
help="Do not log to std output.",
|
554
832
|
)
|
555
|
-
subparsers = parser.add_subparsers()
|
556
833
|
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
for fn in [getattr(command, n) for n in dir(command)]:
|
834
|
+
self.subparsers = parser.add_subparsers()
|
835
|
+
alembic_commands = (
|
836
|
+
cast(CommandFunction, fn)
|
837
|
+
for fn in (getattr(command, name) for name in dir(command))
|
562
838
|
if (
|
563
839
|
inspect.isfunction(fn)
|
564
840
|
and fn.__name__[0] != "_"
|
565
841
|
and fn.__module__ == "alembic.command"
|
566
|
-
)
|
567
|
-
|
568
|
-
if spec[3] is not None:
|
569
|
-
positional = spec[0][1 : -len(spec[3])]
|
570
|
-
kwarg = spec[0][-len(spec[3]) :]
|
571
|
-
else:
|
572
|
-
positional = spec[0][1:]
|
573
|
-
kwarg = []
|
842
|
+
)
|
843
|
+
)
|
574
844
|
|
575
|
-
|
576
|
-
|
577
|
-
positional_translations[fn].get(name, name)
|
578
|
-
for name in positional
|
579
|
-
]
|
845
|
+
for fn in alembic_commands:
|
846
|
+
self.register_command(fn)
|
580
847
|
|
581
|
-
# parse first line(s) of helptext without a line break
|
582
|
-
help_ = fn.__doc__
|
583
|
-
if help_:
|
584
|
-
help_text = []
|
585
|
-
for line in help_.split("\n"):
|
586
|
-
if not line.strip():
|
587
|
-
break
|
588
|
-
else:
|
589
|
-
help_text.append(line.strip())
|
590
|
-
else:
|
591
|
-
help_text = []
|
592
|
-
subparser = subparsers.add_parser(
|
593
|
-
fn.__name__, help=" ".join(help_text)
|
594
|
-
)
|
595
|
-
add_options(fn, subparser, positional, kwarg)
|
596
|
-
subparser.set_defaults(cmd=(fn, positional, kwarg))
|
597
848
|
self.parser = parser
|
598
849
|
|
850
|
+
def register_command(self, fn: CommandFunction) -> None:
|
851
|
+
"""Registers a function as a CLI subcommand. The subcommand name
|
852
|
+
matches the function name, the arguments are extracted from the
|
853
|
+
signature and the help text is read from the docstring.
|
854
|
+
|
855
|
+
.. versionadded:: 1.15.3
|
856
|
+
|
857
|
+
.. seealso::
|
858
|
+
|
859
|
+
:ref:`custom_commandline`
|
860
|
+
"""
|
861
|
+
|
862
|
+
positional, kwarg, help_text = self._inspect_function(fn)
|
863
|
+
|
864
|
+
subparser = self.subparsers.add_parser(fn.__name__, help=help_text)
|
865
|
+
subparser.set_defaults(cmd=(fn, positional, kwarg))
|
866
|
+
|
867
|
+
for arg in kwarg:
|
868
|
+
if arg in self._KWARGS_OPTS:
|
869
|
+
kwarg_opt = self._KWARGS_OPTS[arg]
|
870
|
+
args, opts = kwarg_opt[0:-1], kwarg_opt[-1]
|
871
|
+
subparser.add_argument(*args, **opts) # type:ignore
|
872
|
+
|
873
|
+
for arg in positional:
|
874
|
+
opts = self._POSITIONAL_OPTS.get(arg, {})
|
875
|
+
subparser.add_argument(arg, **opts) # type:ignore
|
876
|
+
|
877
|
+
def _inspect_function(self, fn: CommandFunction) -> tuple[Any, Any, str]:
|
878
|
+
spec = compat.inspect_getfullargspec(fn)
|
879
|
+
if spec[3] is not None:
|
880
|
+
positional = spec[0][1 : -len(spec[3])]
|
881
|
+
kwarg = spec[0][-len(spec[3]) :]
|
882
|
+
else:
|
883
|
+
positional = spec[0][1:]
|
884
|
+
kwarg = []
|
885
|
+
|
886
|
+
if fn in self._POSITIONAL_TRANSLATIONS:
|
887
|
+
positional = [
|
888
|
+
self._POSITIONAL_TRANSLATIONS[fn].get(name, name)
|
889
|
+
for name in positional
|
890
|
+
]
|
891
|
+
|
892
|
+
# parse first line(s) of helptext without a line break
|
893
|
+
help_ = fn.__doc__
|
894
|
+
if help_:
|
895
|
+
help_lines = []
|
896
|
+
for line in help_.split("\n"):
|
897
|
+
if not line.strip():
|
898
|
+
break
|
899
|
+
else:
|
900
|
+
help_lines.append(line.strip())
|
901
|
+
else:
|
902
|
+
help_lines = []
|
903
|
+
|
904
|
+
help_text = " ".join(help_lines)
|
905
|
+
|
906
|
+
return positional, kwarg, help_text
|
907
|
+
|
599
908
|
def run_cmd(self, config: Config, options: Namespace) -> None:
|
600
909
|
fn, positional, kwarg = options.cmd
|
601
910
|
|
@@ -611,15 +920,58 @@ class CommandLine:
|
|
611
920
|
else:
|
612
921
|
util.err(str(e), **config.messaging_opts)
|
613
922
|
|
923
|
+
def _inis_from_config(self, options: Namespace) -> tuple[str, str]:
|
924
|
+
names = options.config
|
925
|
+
|
926
|
+
alembic_config_env = os.environ.get("ALEMBIC_CONFIG")
|
927
|
+
if (
|
928
|
+
alembic_config_env
|
929
|
+
and os.path.basename(alembic_config_env) == "pyproject.toml"
|
930
|
+
):
|
931
|
+
default_pyproject_toml = alembic_config_env
|
932
|
+
default_alembic_config = "alembic.ini"
|
933
|
+
elif alembic_config_env:
|
934
|
+
default_pyproject_toml = "pyproject.toml"
|
935
|
+
default_alembic_config = alembic_config_env
|
936
|
+
else:
|
937
|
+
default_alembic_config = "alembic.ini"
|
938
|
+
default_pyproject_toml = "pyproject.toml"
|
939
|
+
|
940
|
+
if not names:
|
941
|
+
return default_pyproject_toml, default_alembic_config
|
942
|
+
|
943
|
+
toml = ini = None
|
944
|
+
|
945
|
+
for name in names:
|
946
|
+
if os.path.basename(name) == "pyproject.toml":
|
947
|
+
if toml is not None:
|
948
|
+
raise util.CommandError(
|
949
|
+
"pyproject.toml indicated more than once"
|
950
|
+
)
|
951
|
+
toml = name
|
952
|
+
else:
|
953
|
+
if ini is not None:
|
954
|
+
raise util.CommandError(
|
955
|
+
"only one ini file may be indicated"
|
956
|
+
)
|
957
|
+
ini = name
|
958
|
+
|
959
|
+
return toml if toml else default_pyproject_toml, (
|
960
|
+
ini if ini else default_alembic_config
|
961
|
+
)
|
962
|
+
|
614
963
|
def main(self, argv: Optional[Sequence[str]] = None) -> None:
|
964
|
+
"""Executes the command line with the provided arguments."""
|
615
965
|
options = self.parser.parse_args(argv)
|
616
966
|
if not hasattr(options, "cmd"):
|
617
967
|
# see http://bugs.python.org/issue9253, argparse
|
618
968
|
# behavior changed incompatibly in py3.3
|
619
969
|
self.parser.error("too few arguments")
|
620
970
|
else:
|
971
|
+
toml, ini = self._inis_from_config(options)
|
621
972
|
cfg = Config(
|
622
|
-
file_=
|
973
|
+
file_=ini,
|
974
|
+
toml_file=toml,
|
623
975
|
ini_section=options.name,
|
624
976
|
cmd_opts=options,
|
625
977
|
)
|