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