1from argparse import ArgumentParser
2import inspect
3import os
4import sys
5
6from . import __version__
7from . import command
8from . import util
9from .util import compat
10from .util.compat import SafeConfigParser
11
12
13class Config(object):
14
15    r"""Represent an Alembic configuration.
16
17    Within an ``env.py`` script, this is available
18    via the :attr:`.EnvironmentContext.config` attribute,
19    which in turn is available at ``alembic.context``::
20
21        from alembic import context
22
23        some_param = context.config.get_main_option("my option")
24
25    When invoking Alembic programatically, a new
26    :class:`.Config` can be created by passing
27    the name of an .ini file to the constructor::
28
29        from alembic.config import Config
30        alembic_cfg = Config("/path/to/yourapp/alembic.ini")
31
32    With a :class:`.Config` object, you can then
33    run Alembic commands programmatically using the directives
34    in :mod:`alembic.command`.
35
36    The :class:`.Config` object can also be constructed without
37    a filename.   Values can be set programmatically, and
38    new sections will be created as needed::
39
40        from alembic.config import Config
41        alembic_cfg = Config()
42        alembic_cfg.set_main_option("script_location", "myapp:migrations")
43        alembic_cfg.set_main_option("sqlalchemy.url", "postgresql://foo/bar")
44        alembic_cfg.set_section_option("mysection", "foo", "bar")
45
46    .. warning::
47
48       When using programmatic configuration, make sure the
49       ``env.py`` file in use is compatible with the target configuration;
50       including that the call to Python ``logging.fileConfig()`` is
51       omitted if the programmatic configuration doesn't actually include
52       logging directives.
53
54    For passing non-string values to environments, such as connections and
55    engines, use the :attr:`.Config.attributes` dictionary::
56
57        with engine.begin() as connection:
58            alembic_cfg.attributes['connection'] = connection
59            command.upgrade(alembic_cfg, "head")
60
61    :param file\_: name of the .ini file to open.
62    :param ini_section: name of the main Alembic section within the
63     .ini file
64    :param output_buffer: optional file-like input buffer which
65     will be passed to the :class:`.MigrationContext` - used to redirect
66     the output of "offline generation" when using Alembic programmatically.
67    :param stdout: buffer where the "print" output of commands will be sent.
68     Defaults to ``sys.stdout``.
69
70     .. versionadded:: 0.4
71
72    :param config_args: A dictionary of keys and values that will be used
73     for substitution in the alembic config file.  The dictionary as given
74     is **copied** to a new one, stored locally as the attribute
75     ``.config_args``. When the :attr:`.Config.file_config` attribute is
76     first invoked, the replacement variable ``here`` will be added to this
77     dictionary before the dictionary is passed to ``SafeConfigParser()``
78     to parse the .ini file.
79
80     .. versionadded:: 0.7.0
81
82    :param attributes: optional dictionary of arbitrary Python keys/values,
83     which will be populated into the :attr:`.Config.attributes` dictionary.
84
85     .. versionadded:: 0.7.5
86
87     .. seealso::
88
89        :ref:`connection_sharing`
90
91    """
92
93    def __init__(
94        self,
95        file_=None,
96        ini_section="alembic",
97        output_buffer=None,
98        stdout=sys.stdout,
99        cmd_opts=None,
100        config_args=util.immutabledict(),
101        attributes=None,
102    ):
103        """Construct a new :class:`.Config`
104
105        """
106        self.config_file_name = file_
107        self.config_ini_section = ini_section
108        self.output_buffer = output_buffer
109        self.stdout = stdout
110        self.cmd_opts = cmd_opts
111        self.config_args = dict(config_args)
112        if attributes:
113            self.attributes.update(attributes)
114
115    cmd_opts = None
116    """The command-line options passed to the ``alembic`` script.
117
118    Within an ``env.py`` script this can be accessed via the
119    :attr:`.EnvironmentContext.config` attribute.
120
121    .. versionadded:: 0.6.0
122
123    .. seealso::
124
125        :meth:`.EnvironmentContext.get_x_argument`
126
127    """
128
129    config_file_name = None
130    """Filesystem path to the .ini file in use."""
131
132    config_ini_section = None
133    """Name of the config file section to read basic configuration
134    from.  Defaults to ``alembic``, that is the ``[alembic]`` section
135    of the .ini file.  This value is modified using the ``-n/--name``
136    option to the Alembic runnier.
137
138    """
139
140    @util.memoized_property
141    def attributes(self):
142        """A Python dictionary for storage of additional state.
143
144
145        This is a utility dictionary which can include not just strings but
146        engines, connections, schema objects, or anything else.
147        Use this to pass objects into an env.py script, such as passing
148        a :class:`sqlalchemy.engine.base.Connection` when calling
149        commands from :mod:`alembic.command` programmatically.
150
151        .. versionadded:: 0.7.5
152
153        .. seealso::
154
155            :ref:`connection_sharing`
156
157            :paramref:`.Config.attributes`
158
159        """
160        return {}
161
162    def print_stdout(self, text, *arg):
163        """Render a message to standard out.
164
165        When :meth:`.Config.print_stdout` is called with additional args
166        those arguments will formatted against the provided text,
167        otherwise we simply output the provided text verbatim.
168
169        e.g.::
170
171            >>> config.print_stdout('Some text %s', 'arg')
172            Some Text arg
173
174        """
175
176        if arg:
177            output = compat.text_type(text) % arg
178        else:
179            output = compat.text_type(text)
180
181        util.write_outstream(self.stdout, output, "\n")
182
183    @util.memoized_property
184    def file_config(self):
185        """Return the underlying ``ConfigParser`` object.
186
187        Direct access to the .ini file is available here,
188        though the :meth:`.Config.get_section` and
189        :meth:`.Config.get_main_option`
190        methods provide a possibly simpler interface.
191
192        """
193
194        if self.config_file_name:
195            here = os.path.abspath(os.path.dirname(self.config_file_name))
196        else:
197            here = ""
198        self.config_args["here"] = here
199        file_config = SafeConfigParser(self.config_args)
200        if self.config_file_name:
201            file_config.read([self.config_file_name])
202        else:
203            file_config.add_section(self.config_ini_section)
204        return file_config
205
206    def get_template_directory(self):
207        """Return the directory where Alembic setup templates are found.
208
209        This method is used by the alembic ``init`` and ``list_templates``
210        commands.
211
212        """
213        import alembic
214
215        package_dir = os.path.abspath(os.path.dirname(alembic.__file__))
216        return os.path.join(package_dir, "templates")
217
218    def get_section(self, name, default=None):
219        """Return all the configuration options from a given .ini file section
220        as a dictionary.
221
222        """
223        if not self.file_config.has_section(name):
224            return default
225
226        return dict(self.file_config.items(name))
227
228    def set_main_option(self, name, value):
229        """Set an option programmatically within the 'main' section.
230
231        This overrides whatever was in the .ini file.
232
233        :param name: name of the value
234
235        :param value: the value.  Note that this value is passed to
236         ``ConfigParser.set``, which supports variable interpolation using
237         pyformat (e.g. ``%(some_value)s``).   A raw percent sign not part of
238         an interpolation symbol must therefore be escaped, e.g. ``%%``.
239         The given value may refer to another value already in the file
240         using the interpolation format.
241
242        """
243        self.set_section_option(self.config_ini_section, name, value)
244
245    def remove_main_option(self, name):
246        self.file_config.remove_option(self.config_ini_section, name)
247
248    def set_section_option(self, section, name, value):
249        """Set an option programmatically within the given section.
250
251        The section is created if it doesn't exist already.
252        The value here will override whatever was in the .ini
253        file.
254
255        :param section: name of the section
256
257        :param name: name of the value
258
259        :param value: the value.  Note that this value is passed to
260         ``ConfigParser.set``, which supports variable interpolation using
261         pyformat (e.g. ``%(some_value)s``).   A raw percent sign not part of
262         an interpolation symbol must therefore be escaped, e.g. ``%%``.
263         The given value may refer to another value already in the file
264         using the interpolation format.
265
266        """
267
268        if not self.file_config.has_section(section):
269            self.file_config.add_section(section)
270        self.file_config.set(section, name, value)
271
272    def get_section_option(self, section, name, default=None):
273        """Return an option from the given section of the .ini file.
274
275        """
276        if not self.file_config.has_section(section):
277            raise util.CommandError(
278                "No config file %r found, or file has no "
279                "'[%s]' section" % (self.config_file_name, section)
280            )
281        if self.file_config.has_option(section, name):
282            return self.file_config.get(section, name)
283        else:
284            return default
285
286    def get_main_option(self, name, default=None):
287        """Return an option from the 'main' section of the .ini file.
288
289        This defaults to being a key from the ``[alembic]``
290        section, unless the ``-n/--name`` flag were used to
291        indicate a different section.
292
293        """
294        return self.get_section_option(self.config_ini_section, name, default)
295
296
297class CommandLine(object):
298    def __init__(self, prog=None):
299        self._generate_args(prog)
300
301    def _generate_args(self, prog):
302        def add_options(fn, parser, positional, kwargs):
303            kwargs_opts = {
304                "template": (
305                    "-t",
306                    "--template",
307                    dict(
308                        default="generic",
309                        type=str,
310                        help="Setup template for use with 'init'",
311                    ),
312                ),
313                "message": (
314                    "-m",
315                    "--message",
316                    dict(
317                        type=str, help="Message string to use with 'revision'"
318                    ),
319                ),
320                "sql": (
321                    "--sql",
322                    dict(
323                        action="store_true",
324                        help="Don't emit SQL to database - dump to "
325                        "standard output/file instead. See docs on "
326                        "offline mode.",
327                    ),
328                ),
329                "tag": (
330                    "--tag",
331                    dict(
332                        type=str,
333                        help="Arbitrary 'tag' name - can be used by "
334                        "custom env.py scripts.",
335                    ),
336                ),
337                "head": (
338                    "--head",
339                    dict(
340                        type=str,
341                        help="Specify head revision or <branchname>@head "
342                        "to base new revision on.",
343                    ),
344                ),
345                "splice": (
346                    "--splice",
347                    dict(
348                        action="store_true",
349                        help="Allow a non-head revision as the "
350                        "'head' to splice onto",
351                    ),
352                ),
353                "depends_on": (
354                    "--depends-on",
355                    dict(
356                        action="append",
357                        help="Specify one or more revision identifiers "
358                        "which this revision should depend on.",
359                    ),
360                ),
361                "rev_id": (
362                    "--rev-id",
363                    dict(
364                        type=str,
365                        help="Specify a hardcoded revision id instead of "
366                        "generating one",
367                    ),
368                ),
369                "version_path": (
370                    "--version-path",
371                    dict(
372                        type=str,
373                        help="Specify specific path from config for "
374                        "version file",
375                    ),
376                ),
377                "branch_label": (
378                    "--branch-label",
379                    dict(
380                        type=str,
381                        help="Specify a branch label to apply to the "
382                        "new revision",
383                    ),
384                ),
385                "verbose": (
386                    "-v",
387                    "--verbose",
388                    dict(action="store_true", help="Use more verbose output"),
389                ),
390                "resolve_dependencies": (
391                    "--resolve-dependencies",
392                    dict(
393                        action="store_true",
394                        help="Treat dependency versions as down revisions",
395                    ),
396                ),
397                "autogenerate": (
398                    "--autogenerate",
399                    dict(
400                        action="store_true",
401                        help="Populate revision script with candidate "
402                        "migration operations, based on comparison "
403                        "of database to model.",
404                    ),
405                ),
406                "head_only": (
407                    "--head-only",
408                    dict(
409                        action="store_true",
410                        help="Deprecated.  Use --verbose for "
411                        "additional output",
412                    ),
413                ),
414                "rev_range": (
415                    "-r",
416                    "--rev-range",
417                    dict(
418                        action="store",
419                        help="Specify a revision range; "
420                        "format is [start]:[end]",
421                    ),
422                ),
423                "indicate_current": (
424                    "-i",
425                    "--indicate-current",
426                    dict(
427                        action="store_true",
428                        help="Indicate the current revision",
429                    ),
430                ),
431                "purge": (
432                    "--purge",
433                    dict(
434                        action="store_true",
435                        help="Unconditionally erase the version table "
436                        "before stamping",
437                    ),
438                ),
439                "package": (
440                    "--package",
441                    dict(
442                        action="store_true",
443                        help="Write empty __init__.py files to the "
444                        "environment and version locations",
445                    ),
446                ),
447            }
448            positional_help = {
449                "directory": "location of scripts directory",
450                "revision": "revision identifier",
451                "revisions": "one or more revisions, or 'heads' for all heads",
452            }
453            for arg in kwargs:
454                if arg in kwargs_opts:
455                    args = kwargs_opts[arg]
456                    args, kw = args[0:-1], args[-1]
457                    parser.add_argument(*args, **kw)
458
459            for arg in positional:
460                if (
461                    arg == "revisions"
462                    or fn in positional_translations
463                    and positional_translations[fn][arg] == "revisions"
464                ):
465                    subparser.add_argument(
466                        "revisions",
467                        nargs="+",
468                        help=positional_help.get("revisions"),
469                    )
470                else:
471                    subparser.add_argument(arg, help=positional_help.get(arg))
472
473        parser = ArgumentParser(prog=prog)
474
475        parser.add_argument(
476            "--version", action="version", version="%%(prog)s %s" % __version__
477        )
478        parser.add_argument(
479            "-c",
480            "--config",
481            type=str,
482            default=os.environ.get("ALEMBIC_CONFIG", "alembic.ini"),
483            help="Alternate config file; defaults to value of "
484            'ALEMBIC_CONFIG environment variable, or "alembic.ini"',
485        )
486        parser.add_argument(
487            "-n",
488            "--name",
489            type=str,
490            default="alembic",
491            help="Name of section in .ini file to " "use for Alembic config",
492        )
493        parser.add_argument(
494            "-x",
495            action="append",
496            help="Additional arguments consumed by "
497            "custom env.py scripts, e.g. -x "
498            "setting1=somesetting -x setting2=somesetting",
499        )
500        parser.add_argument(
501            "--raiseerr",
502            action="store_true",
503            help="Raise a full stack trace on error",
504        )
505        subparsers = parser.add_subparsers()
506
507        positional_translations = {command.stamp: {"revision": "revisions"}}
508
509        for fn in [getattr(command, n) for n in dir(command)]:
510            if (
511                inspect.isfunction(fn)
512                and fn.__name__[0] != "_"
513                and fn.__module__ == "alembic.command"
514            ):
515
516                spec = compat.inspect_getargspec(fn)
517                if spec[3]:
518                    positional = spec[0][1 : -len(spec[3])]
519                    kwarg = spec[0][-len(spec[3]) :]
520                else:
521                    positional = spec[0][1:]
522                    kwarg = []
523
524                if fn in positional_translations:
525                    positional = [
526                        positional_translations[fn].get(name, name)
527                        for name in positional
528                    ]
529
530                # parse first line(s) of helptext without a line break
531                help_ = fn.__doc__
532                if help_:
533                    help_text = []
534                    for line in help_.split("\n"):
535                        if not line.strip():
536                            break
537                        else:
538                            help_text.append(line.strip())
539                else:
540                    help_text = ""
541                subparser = subparsers.add_parser(
542                    fn.__name__, help=" ".join(help_text)
543                )
544                add_options(fn, subparser, positional, kwarg)
545                subparser.set_defaults(cmd=(fn, positional, kwarg))
546        self.parser = parser
547
548    def run_cmd(self, config, options):
549        fn, positional, kwarg = options.cmd
550
551        try:
552            fn(
553                config,
554                *[getattr(options, k, None) for k in positional],
555                **dict((k, getattr(options, k, None)) for k in kwarg)
556            )
557        except util.CommandError as e:
558            if options.raiseerr:
559                raise
560            else:
561                util.err(str(e))
562
563    def main(self, argv=None):
564        options = self.parser.parse_args(argv)
565        if not hasattr(options, "cmd"):
566            # see http://bugs.python.org/issue9253, argparse
567            # behavior changed incompatibly in py3.3
568            self.parser.error("too few arguments")
569        else:
570            cfg = Config(
571                file_=options.config,
572                ini_section=options.name,
573                cmd_opts=options,
574            )
575            self.run_cmd(cfg, options)
576
577
578def main(argv=None, prog=None, **kwargs):
579    """The console runner function for Alembic."""
580
581    CommandLine(prog=prog).main(argv=argv)
582
583
584if __name__ == "__main__":
585    main()
586