From dfba9c719f2328e8e0cd6277c362c4d780e14ef3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 14 Nov 2011 23:34:17 -0500 Subject: [PATCH] - add argparse to install requires - more docs - get env.py to work in --sql mode even without the DBAPI installed, using just the URL - add get_section_option() --- alembic/config.py | 44 ++++++++++++++++++-------------- alembic/context.py | 42 ++++++++++++++++++++++-------- alembic/templates/generic/env.py | 18 ++++++++----- alembic/templates/multidb/env.py | 26 ++++++++++++++----- alembic/templates/pylons/env.py | 3 ++- docs/build/api.rst | 27 ++++++++++++-------- docs/build/tutorial.rst | 6 ++--- setup.py | 6 ++++- 8 files changed, 113 insertions(+), 59 deletions(-) diff --git a/alembic/config.py b/alembic/config.py index 26750a98..c5389f80 100644 --- a/alembic/config.py +++ b/alembic/config.py @@ -6,17 +6,17 @@ import os class Config(object): """Represent an Alembic configuration. - + You can get at one of these by specifying the name of an .ini file:: - + from alembic.config import Config alembic_cfg = Config("/path/to/yourapp/alembic.ini") - + With a :class:`.Config` object, you can then run Alembic commands programmatically using the directives in :mod:`alembic.command`. - + """ def __init__(self, file_, ini_section='alembic'): self.config_file_name = file_ @@ -30,13 +30,13 @@ class Config(object): from. Defaults to ``alembic``, that is the ``[alembic]`` section of the .ini file. This value is modified using the ``-n/--name`` option to the Alembic runnier. - + """ @util.memoized_property def file_config(self): """Return the underlying :class:`ConfigParser` object. - + Direct access to the .ini file is available here, though the :meth:`.Config.get_section` and :meth:`.Config.get_main_option` @@ -51,36 +51,42 @@ class Config(object): def get_template_directory(self): """Return the directory where Alembic setup templates are found. - + This method is used by the alembic ``init`` and ``list_templates`` commands. - + """ return os.path.join(package_dir, 'templates') def get_section(self, name): """Return all the configuration options from a given .ini file section as a dictionary. - + """ return dict(self.file_config.items(name)) + def get_section_option(self, section, name, default=None): + """Return an option from the given section of the .ini file. + + """ + if not self.file_config.has_section(section): + util.err("No config file %r found, or file has no " + "'[%s]' section" % + (self.config_file_name, section)) + if self.file_config.has_option(section, name): + return self.file_config.get(section, name) + else: + return default + def get_main_option(self, name, default=None): """Return an option from the 'main' section of the .ini file. - + This defaults to being a key from the ``[alembic]`` section, unless the ``-n/--name`` flag were used to indicate a different section. - + """ - if not self.file_config.has_section(self.config_ini_section): - util.err("No config file %r found, or file has no " - "'[%s]' section" % - (self.config_file_name, self.config_ini_section)) - if self.file_config.has_option(self.config_ini_section, name): - return self.file_config.get(self.config_ini_section, name) - else: - return default + return self.get_section_option(self.config_ini_section, name, default) def main(argv): """The console runner function for Alembic.""" diff --git a/alembic/context.py b/alembic/context.py index 752bd2dd..adcb9e06 100644 --- a/alembic/context.py +++ b/alembic/context.py @@ -19,6 +19,10 @@ class Context(object): Mediates the relationship between an ``env.py`` environment script, a :class:`.ScriptDirectory` instance, and a :class:`.DDLImpl` instance. + + The :class:`.Context` is available via the :func:`.get_context` function, + though usually one would call the various module level functions + described here. """ def __init__(self, dialect, script, connection, fn, @@ -88,6 +92,8 @@ class Context(object): if self.as_sql and not current_rev: _version.create(self.connection) log.info("Running %s %s -> %s", change.__name__, prev_rev, rev) + if self.as_sql: + self.impl.static_output("-- Running %s %s -> %s" %(change.__name__, prev_rev, rev)) change(**kw) if not self.impl.transactional_ddl: self._update_current_rev(prev_rev, rev) @@ -151,18 +157,23 @@ def requires_connection(): """Return True if the current migrations environment should have an active database connection. + Currently, this is ``True`` or ``False`` depending + on the the ``--sql`` flag passed. + """ return not _context_opts.get('as_sql', False) def get_head_revision(): - """Return the value of the 'head' revision.""" + """Return the hex identifier of the 'head' revision.""" return _script._as_rev_number("head") def get_starting_revision_argument(): """Return the 'starting revision' argument, - if the revision was passed as start:end. + if the revision was passed using ``start:end``. - This is only usable in "offline" mode. + This is only meaningful in "offline" mode. + Returns ``None`` if no value is available + or was configured. """ if _context is not None: @@ -175,15 +186,28 @@ def get_starting_revision_argument(): def get_revision_argument(): """Get the 'destination' revision argument. - This will be the target rev number. 'head' - is translated into the actual version number - as is 'base' which is translated to None. + This is typically the argument passed to the + ``upgrade`` or ``downgrade`` command, but can + be overridden via the ``destination_rev`` argument + passed to :func:`.configure`. + + If + it was specified as ``head``, the actual + version number is returned; if specified + as ``base``, ``None`` is returned. """ return _script._as_rev_number(_context_opts['destination_rev']) def get_tag_argument(): - """Return the value passed for the ``--tag`` argument, if any.""" + """Return the value passed for the ``--tag`` argument, if any. + + The ``--tag`` argument is not used directly by Alembic, + but is available for custom ``env.py`` configurations that + wish to use it; particularly for offline generation scripts + that wish to generate tagged filenames. + + """ return _context_opts.get('tag', None) def configure( @@ -292,9 +316,7 @@ def execute(sql): get_context().execute(sql) def get_context(): - """Return the current :class:`.DefaultContext` object. - - This object is the entrypoint to dialect specific behavior. + """Return the current :class:`.Context` object. Generally, env.py scripts should access the module-level functions in :mod:`alebmic.context` to get at this object's functionality. diff --git a/alembic/templates/generic/env.py b/alembic/templates/generic/env.py index ac64bee7..8f5018f1 100644 --- a/alembic/templates/generic/env.py +++ b/alembic/templates/generic/env.py @@ -10,12 +10,6 @@ config = context.config # This line sets up loggers basically. fileConfig(config.config_file_name) -# Produce a SQLAlchemy engine using the key/values -# within the "alembic" section of the documentation, -# other otherwise what config_ini_section points to. -engine = engine_from_config( - config.get_section(config.config_ini_section), prefix='sqlalchemy.') - # other values from the config, defined by the needs of env.py, # can be acquired: @@ -23,12 +17,22 @@ engine = engine_from_config( # ... etc. # if we're running in --sql mode, do everything connectionless. +# We only need a URL, not an Engine, thereby not even requiring +# the DBAPI be installed, though an actual Engine would of course be fine as well. if not context.requires_connection(): - context.configure(dialect_name=engine.name) + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url) context.run_migrations() # otherwise we need to make a connection. else: + + # Produce a SQLAlchemy engine using the key/values + # within the "alembic" section of the documentation, + # other otherwise what config_ini_section points to. + engine = engine_from_config( + config.get_section(config.config_ini_section), prefix='sqlalchemy.') + connection = engine.connect() context.configure(connection=connection, dialect_name=engine.name) diff --git a/alembic/templates/multidb/env.py b/alembic/templates/multidb/env.py index 3acb27cf..e561e9be 100644 --- a/alembic/templates/multidb/env.py +++ b/alembic/templates/multidb/env.py @@ -1,6 +1,6 @@ USE_TWOPHASE = False -from alembic import options, context +from alembic import context from sqlalchemy import engine_from_config import re import sys @@ -8,25 +8,37 @@ import sys import logging logging.fileConfig(options.config_file) +# gather section names referring to different +# databases. db_names = options.get_main_option('databases') + +# set aside if we need engines or just URLs to do this. +need_engine = context.requires_connection() + +# load up SQLAlchemy engines or URLs. engines = {} for name in re.split(r',\s*', db_names): engines[name] = rec = {} - rec['engine'] = engine = \ - engine_from_config(options.get_section(name), + if need_engine: + rec['engine'] = engine_from_config(context.config.get_section(name), prefix='sqlalchemy.') + else: + rec['url'] = context.config.get_section_option(name, "sqlalchemy.url") - -if not context.requires_connection(): +# for the --sql use case, run migrations for each URL into +# individual files. +if not need_engine: for name, rec in engines.items(): - # Write output to individual per-engine files. file_ = "%s.sql" % name sys.stderr.write("Writing output to %s\n" % file_) context.configure( - dialect_name=rec['engine'].name, + url=rec['url'], output_buffer=file(file_, 'w') ) context.run_migrations(engine=name) + +# for the direct-to-DB use case, start a transaction on all +# engines, then run all migrations, then commit all transactions. else: for name, rec in engines.items(): engine = rec['engine'] diff --git a/alembic/templates/pylons/env.py b/alembic/templates/pylons/env.py index 799ba025..2a3d9972 100644 --- a/alembic/templates/pylons/env.py +++ b/alembic/templates/pylons/env.py @@ -4,7 +4,7 @@ Place 'pylons_config_file' into alembic.ini, and the application will be loaded from there. """ -from alembic import config, context +from alembic import context from paste.deploy import loadapp import logging @@ -23,6 +23,7 @@ except: # customize this section for non-standard engine configurations. meta = __import__("%s.model.meta" % config['pylons.package']).model.meta + if not context.requires_connection(): context.configure( dialect_name=meta.engine.name) diff --git a/docs/build/api.rst b/docs/build/api.rst index 411600a1..cff5826c 100644 --- a/docs/build/api.rst +++ b/docs/build/api.rst @@ -8,37 +8,42 @@ a migration environment's ``env.py`` file. env.py Directives ================= +The :mod:`alembic.context` module contains API features that are generally used within +``env.py`` files. + .. autofunction:: sqlalchemy.engine.engine_from_config -.. autofunction:: alembic.context.configure -.. autofunction:: alembic.context.get_context -.. autofunction:: alembic.context.execute -.. autofunction:: alembic.context.requires_connection -.. autofunction:: alembic.context.run_migrations -Internals -========= +.. currentmodule:: alembic.context -.. currentmodule:: alembic.command +.. automodule:: alembic.context + :members: Commands --------- +========= Alembic commands are all represented by functions in the :mod:`alembic.command` package. They all accept the same style of usage, being sent the :class:`~.alembic.config.Config` object as the first argument. +.. currentmodule:: alembic.command .. automodule:: alembic.command :members: :undoc-members: -Misc ----- +Configuration +============== + +.. currentmodule:: alembic.config + .. automodule:: alembic.config :members: :undoc-members: +Internals +========= + .. automodule:: alembic.script :members: :undoc-members: diff --git a/docs/build/tutorial.rst b/docs/build/tutorial.rst index 652ed7b4..2b2eb639 100644 --- a/docs/build/tutorial.rst +++ b/docs/build/tutorial.rst @@ -457,11 +457,11 @@ Autogenerate can *not* detect: yet implemented. -Generating SQL Scripts -====================== +Generating SQL Scripts (a.k.a. "Offline Mode") +============================================== A major capability of Alembic is to generate migrations as SQL scripts, instead of running -them against the database - this is also referred to as "offline" mode. +them against the database - this is also referred to as *offline mode*. This is a critical feature when working in large organizations where access to DDL is restricted, and SQL scripts must be handed off to DBAs. Alembic makes this easy via the ``--sql`` option passed to any ``upgrade`` or ``downgrade`` command. We diff --git a/setup.py b/setup.py index cdc6483c..e6e2beb4 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,11 @@ setup(name='alembic', zip_safe=False, install_requires=[ 'SQLAlchemy>=0.6.0', - 'Mako' + 'Mako', + # TODO: should this not be here if the env. is + # Python 2.7/3.2 ? not sure how this is supposed + # to be handled + 'argparse' ], entry_points=""" """, -- 2.47.2