1import os 2 3from . import autogenerate as autogen 4from . import util 5from .runtime.environment import EnvironmentContext 6from .script import ScriptDirectory 7 8 9def list_templates(config): 10 """List available templates. 11 12 :param config: a :class:`.Config` object. 13 14 """ 15 16 config.print_stdout("Available templates:\n") 17 for tempname in os.listdir(config.get_template_directory()): 18 with open( 19 os.path.join(config.get_template_directory(), tempname, "README") 20 ) as readme: 21 synopsis = next(readme) 22 config.print_stdout("%s - %s", tempname, synopsis) 23 24 config.print_stdout("\nTemplates are used via the 'init' command, e.g.:") 25 config.print_stdout("\n alembic init --template generic ./scripts") 26 27 28def init(config, directory, template="generic", package=False): 29 """Initialize a new scripts directory. 30 31 :param config: a :class:`.Config` object. 32 33 :param directory: string path of the target directory 34 35 :param template: string name of the migration environment template to 36 use. 37 38 :param package: when True, write ``__init__.py`` files into the 39 environment location as well as the versions/ location. 40 41 .. versionadded:: 1.2 42 43 44 """ 45 46 if os.access(directory, os.F_OK) and os.listdir(directory): 47 raise util.CommandError( 48 "Directory %s already exists and is not empty" % directory 49 ) 50 51 template_dir = os.path.join(config.get_template_directory(), template) 52 if not os.access(template_dir, os.F_OK): 53 raise util.CommandError("No such template %r" % template) 54 55 if not os.access(directory, os.F_OK): 56 util.status( 57 "Creating directory %s" % os.path.abspath(directory), 58 os.makedirs, 59 directory, 60 ) 61 62 versions = os.path.join(directory, "versions") 63 util.status( 64 "Creating directory %s" % os.path.abspath(versions), 65 os.makedirs, 66 versions, 67 ) 68 69 script = ScriptDirectory(directory) 70 71 for file_ in os.listdir(template_dir): 72 file_path = os.path.join(template_dir, file_) 73 if file_ == "alembic.ini.mako": 74 config_file = os.path.abspath(config.config_file_name) 75 if os.access(config_file, os.F_OK): 76 util.msg("File %s already exists, skipping" % config_file) 77 else: 78 script._generate_template( 79 file_path, config_file, script_location=directory 80 ) 81 elif os.path.isfile(file_path): 82 output_file = os.path.join(directory, file_) 83 script._copy_file(file_path, output_file) 84 85 if package: 86 for path in [ 87 os.path.join(os.path.abspath(directory), "__init__.py"), 88 os.path.join(os.path.abspath(versions), "__init__.py"), 89 ]: 90 file_ = util.status("Adding %s" % path, open, path, "w") 91 file_.close() 92 93 util.msg( 94 "Please edit configuration/connection/logging " 95 "settings in %r before proceeding." % config_file 96 ) 97 98 99def revision( 100 config, 101 message=None, 102 autogenerate=False, 103 sql=False, 104 head="head", 105 splice=False, 106 branch_label=None, 107 version_path=None, 108 rev_id=None, 109 depends_on=None, 110 process_revision_directives=None, 111): 112 """Create a new revision file. 113 114 :param config: a :class:`.Config` object. 115 116 :param message: string message to apply to the revision; this is the 117 ``-m`` option to ``alembic revision``. 118 119 :param autogenerate: whether or not to autogenerate the script from 120 the database; this is the ``--autogenerate`` option to 121 ``alembic revision``. 122 123 :param sql: whether to dump the script out as a SQL string; when specified, 124 the script is dumped to stdout. This is the ``--sql`` option to 125 ``alembic revision``. 126 127 :param head: head revision to build the new revision upon as a parent; 128 this is the ``--head`` option to ``alembic revision``. 129 130 :param splice: whether or not the new revision should be made into a 131 new head of its own; is required when the given ``head`` is not itself 132 a head. This is the ``--splice`` option to ``alembic revision``. 133 134 :param branch_label: string label to apply to the branch; this is the 135 ``--branch-label`` option to ``alembic revision``. 136 137 :param version_path: string symbol identifying a specific version path 138 from the configuration; this is the ``--version-path`` option to 139 ``alembic revision``. 140 141 :param rev_id: optional revision identifier to use instead of having 142 one generated; this is the ``--rev-id`` option to ``alembic revision``. 143 144 :param depends_on: optional list of "depends on" identifiers; this is the 145 ``--depends-on`` option to ``alembic revision``. 146 147 :param process_revision_directives: this is a callable that takes the 148 same form as the callable described at 149 :paramref:`.EnvironmentContext.configure.process_revision_directives`; 150 will be applied to the structure generated by the revision process 151 where it can be altered programmatically. Note that unlike all 152 the other parameters, this option is only available via programmatic 153 use of :func:`.command.revision` 154 155 .. versionadded:: 0.9.0 156 157 """ 158 159 script_directory = ScriptDirectory.from_config(config) 160 161 command_args = dict( 162 message=message, 163 autogenerate=autogenerate, 164 sql=sql, 165 head=head, 166 splice=splice, 167 branch_label=branch_label, 168 version_path=version_path, 169 rev_id=rev_id, 170 depends_on=depends_on, 171 ) 172 revision_context = autogen.RevisionContext( 173 config, 174 script_directory, 175 command_args, 176 process_revision_directives=process_revision_directives, 177 ) 178 179 environment = util.asbool(config.get_main_option("revision_environment")) 180 181 if autogenerate: 182 environment = True 183 184 if sql: 185 raise util.CommandError( 186 "Using --sql with --autogenerate does not make any sense" 187 ) 188 189 def retrieve_migrations(rev, context): 190 revision_context.run_autogenerate(rev, context) 191 return [] 192 193 elif environment: 194 195 def retrieve_migrations(rev, context): 196 revision_context.run_no_autogenerate(rev, context) 197 return [] 198 199 elif sql: 200 raise util.CommandError( 201 "Using --sql with the revision command when " 202 "revision_environment is not configured does not make any sense" 203 ) 204 205 if environment: 206 with EnvironmentContext( 207 config, 208 script_directory, 209 fn=retrieve_migrations, 210 as_sql=sql, 211 template_args=revision_context.template_args, 212 revision_context=revision_context, 213 ): 214 script_directory.run_env() 215 216 # the revision_context now has MigrationScript structure(s) present. 217 # these could theoretically be further processed / rewritten *here*, 218 # in addition to the hooks present within each run_migrations() call, 219 # or at the end of env.py run_migrations_online(). 220 221 scripts = [script for script in revision_context.generate_scripts()] 222 if len(scripts) == 1: 223 return scripts[0] 224 else: 225 return scripts 226 227 228def merge(config, revisions, message=None, branch_label=None, rev_id=None): 229 """Merge two revisions together. Creates a new migration file. 230 231 .. versionadded:: 0.7.0 232 233 :param config: a :class:`.Config` instance 234 235 :param message: string message to apply to the revision 236 237 :param branch_label: string label name to apply to the new revision 238 239 :param rev_id: hardcoded revision identifier instead of generating a new 240 one. 241 242 .. seealso:: 243 244 :ref:`branches` 245 246 """ 247 248 script = ScriptDirectory.from_config(config) 249 template_args = { 250 "config": config # Let templates use config for 251 # e.g. multiple databases 252 } 253 return script.generate_revision( 254 rev_id or util.rev_id(), 255 message, 256 refresh=True, 257 head=revisions, 258 branch_labels=branch_label, 259 **template_args 260 ) 261 262 263def upgrade(config, revision, sql=False, tag=None): 264 """Upgrade to a later version. 265 266 :param config: a :class:`.Config` instance. 267 268 :param revision: string revision target or range for --sql mode 269 270 :param sql: if True, use ``--sql`` mode 271 272 :param tag: an arbitrary "tag" that can be intercepted by custom 273 ``env.py`` scripts via the :meth:`.EnvironmentContext.get_tag_argument` 274 method. 275 276 """ 277 278 script = ScriptDirectory.from_config(config) 279 280 starting_rev = None 281 if ":" in revision: 282 if not sql: 283 raise util.CommandError("Range revision not allowed") 284 starting_rev, revision = revision.split(":", 2) 285 286 def upgrade(rev, context): 287 return script._upgrade_revs(revision, rev) 288 289 with EnvironmentContext( 290 config, 291 script, 292 fn=upgrade, 293 as_sql=sql, 294 starting_rev=starting_rev, 295 destination_rev=revision, 296 tag=tag, 297 ): 298 script.run_env() 299 300 301def downgrade(config, revision, sql=False, tag=None): 302 """Revert to a previous version. 303 304 :param config: a :class:`.Config` instance. 305 306 :param revision: string revision target or range for --sql mode 307 308 :param sql: if True, use ``--sql`` mode 309 310 :param tag: an arbitrary "tag" that can be intercepted by custom 311 ``env.py`` scripts via the :meth:`.EnvironmentContext.get_tag_argument` 312 method. 313 314 """ 315 316 script = ScriptDirectory.from_config(config) 317 starting_rev = None 318 if ":" in revision: 319 if not sql: 320 raise util.CommandError("Range revision not allowed") 321 starting_rev, revision = revision.split(":", 2) 322 elif sql: 323 raise util.CommandError( 324 "downgrade with --sql requires <fromrev>:<torev>" 325 ) 326 327 def downgrade(rev, context): 328 return script._downgrade_revs(revision, rev) 329 330 with EnvironmentContext( 331 config, 332 script, 333 fn=downgrade, 334 as_sql=sql, 335 starting_rev=starting_rev, 336 destination_rev=revision, 337 tag=tag, 338 ): 339 script.run_env() 340 341 342def show(config, rev): 343 """Show the revision(s) denoted by the given symbol. 344 345 :param config: a :class:`.Config` instance. 346 347 :param revision: string revision target 348 349 """ 350 351 script = ScriptDirectory.from_config(config) 352 353 if rev == "current": 354 355 def show_current(rev, context): 356 for sc in script.get_revisions(rev): 357 config.print_stdout(sc.log_entry) 358 return [] 359 360 with EnvironmentContext(config, script, fn=show_current): 361 script.run_env() 362 else: 363 for sc in script.get_revisions(rev): 364 config.print_stdout(sc.log_entry) 365 366 367def history(config, rev_range=None, verbose=False, indicate_current=False): 368 """List changeset scripts in chronological order. 369 370 :param config: a :class:`.Config` instance. 371 372 :param rev_range: string revision range 373 374 :param verbose: output in verbose mode. 375 376 :param indicate_current: indicate current revision. 377 378 ..versionadded:: 0.9.9 379 380 """ 381 382 script = ScriptDirectory.from_config(config) 383 if rev_range is not None: 384 if ":" not in rev_range: 385 raise util.CommandError( 386 "History range requires [start]:[end], " "[start]:, or :[end]" 387 ) 388 base, head = rev_range.strip().split(":") 389 else: 390 base = head = None 391 392 environment = ( 393 util.asbool(config.get_main_option("revision_environment")) 394 or indicate_current 395 ) 396 397 def _display_history(config, script, base, head, currents=()): 398 for sc in script.walk_revisions( 399 base=base or "base", head=head or "heads" 400 ): 401 402 if indicate_current: 403 sc._db_current_indicator = sc.revision in currents 404 405 config.print_stdout( 406 sc.cmd_format( 407 verbose=verbose, 408 include_branches=True, 409 include_doc=True, 410 include_parents=True, 411 ) 412 ) 413 414 def _display_history_w_current(config, script, base, head): 415 def _display_current_history(rev, context): 416 if head == "current": 417 _display_history(config, script, base, rev, rev) 418 elif base == "current": 419 _display_history(config, script, rev, head, rev) 420 else: 421 _display_history(config, script, base, head, rev) 422 return [] 423 424 with EnvironmentContext(config, script, fn=_display_current_history): 425 script.run_env() 426 427 if base == "current" or head == "current" or environment: 428 _display_history_w_current(config, script, base, head) 429 else: 430 _display_history(config, script, base, head) 431 432 433def heads(config, verbose=False, resolve_dependencies=False): 434 """Show current available heads in the script directory. 435 436 :param config: a :class:`.Config` instance. 437 438 :param verbose: output in verbose mode. 439 440 :param resolve_dependencies: treat dependency version as down revisions. 441 442 """ 443 444 script = ScriptDirectory.from_config(config) 445 if resolve_dependencies: 446 heads = script.get_revisions("heads") 447 else: 448 heads = script.get_revisions(script.get_heads()) 449 450 for rev in heads: 451 config.print_stdout( 452 rev.cmd_format( 453 verbose, include_branches=True, tree_indicators=False 454 ) 455 ) 456 457 458def branches(config, verbose=False): 459 """Show current branch points. 460 461 :param config: a :class:`.Config` instance. 462 463 :param verbose: output in verbose mode. 464 465 """ 466 script = ScriptDirectory.from_config(config) 467 for sc in script.walk_revisions(): 468 if sc.is_branch_point: 469 config.print_stdout( 470 "%s\n%s\n", 471 sc.cmd_format(verbose, include_branches=True), 472 "\n".join( 473 "%s -> %s" 474 % ( 475 " " * len(str(sc.revision)), 476 rev_obj.cmd_format( 477 False, include_branches=True, include_doc=verbose 478 ), 479 ) 480 for rev_obj in ( 481 script.get_revision(rev) for rev in sc.nextrev 482 ) 483 ), 484 ) 485 486 487def current(config, verbose=False, head_only=False): 488 """Display the current revision for a database. 489 490 :param config: a :class:`.Config` instance. 491 492 :param verbose: output in verbose mode. 493 494 :param head_only: deprecated; use ``verbose`` for additional output. 495 496 """ 497 498 script = ScriptDirectory.from_config(config) 499 500 if head_only: 501 util.warn("--head-only is deprecated", stacklevel=3) 502 503 def display_version(rev, context): 504 if verbose: 505 config.print_stdout( 506 "Current revision(s) for %s:", 507 util.obfuscate_url_pw(context.connection.engine.url), 508 ) 509 for rev in script.get_all_current(rev): 510 config.print_stdout(rev.cmd_format(verbose)) 511 512 return [] 513 514 with EnvironmentContext( 515 config, script, fn=display_version, dont_mutate=True 516 ): 517 script.run_env() 518 519 520def stamp(config, revision, sql=False, tag=None, purge=False): 521 """'stamp' the revision table with the given revision; don't 522 run any migrations. 523 524 :param config: a :class:`.Config` instance. 525 526 :param revision: target revision or list of revisions. May be a list 527 to indicate stamping of multiple branch heads. 528 529 .. note:: this parameter is called "revisions" in the command line 530 interface. 531 532 .. versionchanged:: 1.2 The revision may be a single revision or 533 list of revisions when stamping multiple branch heads. 534 535 :param sql: use ``--sql`` mode 536 537 :param tag: an arbitrary "tag" that can be intercepted by custom 538 ``env.py`` scripts via the :class:`.EnvironmentContext.get_tag_argument` 539 method. 540 541 :param purge: delete all entries in the version table before stamping. 542 543 .. versionadded:: 1.2 544 545 """ 546 547 script = ScriptDirectory.from_config(config) 548 549 if sql: 550 destination_revs = [] 551 starting_rev = None 552 for _revision in util.to_list(revision): 553 if ":" in _revision: 554 srev, _revision = _revision.split(":", 2) 555 556 if starting_rev != srev: 557 if starting_rev is None: 558 starting_rev = srev 559 else: 560 raise util.CommandError( 561 "Stamp operation with --sql only supports a " 562 "single starting revision at a time" 563 ) 564 destination_revs.append(_revision) 565 else: 566 destination_revs = util.to_list(revision) 567 568 def do_stamp(rev, context): 569 return script._stamp_revs(util.to_tuple(destination_revs), rev) 570 571 with EnvironmentContext( 572 config, 573 script, 574 fn=do_stamp, 575 as_sql=sql, 576 starting_rev=starting_rev if sql else None, 577 destination_rev=util.to_tuple(destination_revs), 578 tag=tag, 579 purge=purge, 580 ): 581 script.run_env() 582 583 584def edit(config, rev): 585 """Edit revision script(s) using $EDITOR. 586 587 :param config: a :class:`.Config` instance. 588 589 :param rev: target revision. 590 591 """ 592 593 script = ScriptDirectory.from_config(config) 594 595 if rev == "current": 596 597 def edit_current(rev, context): 598 if not rev: 599 raise util.CommandError("No current revisions") 600 for sc in script.get_revisions(rev): 601 util.edit(sc.path) 602 return [] 603 604 with EnvironmentContext(config, script, fn=edit_current): 605 script.run_env() 606 else: 607 revs = script.get_revisions(rev) 608 if not revs: 609 raise util.CommandError( 610 "No revision files indicated by symbol '%s'" % rev 611 ) 612 for sc in revs: 613 util.edit(sc.path) 614