summaryrefslogtreecommitdiff
path: root/alembic/script
diff options
context:
space:
mode:
authorMike Waites <mikey.waites@gmail.com>2018-12-30 16:09:27 +0000
committerMike Bayer <mike_mp@zzzcomputing.com>2019-09-19 13:16:14 -0400
commit51b307247de12f77b2e482b7bd90a244394566b9 (patch)
treee646d554cbc0a66db0ca51b3f615d0ac7fa6a9b0 /alembic/script
parent3398e6b5a5175009c9d7047df0c1bbf9891c6610 (diff)
downloadalembic-51b307247de12f77b2e482b7bd90a244394566b9.tar.gz
Provide revision post-write hooks
Added "post write hooks" to revision generation. The primary rationale is to provide for code formatting tools to automatically format new revisions, however any arbitrary script or Python function may be invoked as a hook. The hooks are enabled by providing a ``[post_write_hooks]`` section in the alembic.ini file. The provided hook is a command-line runner which includes configuration examples for running Black or autopep8 on newly generated revision scripts. The documentation also illustrates a custom hook that converts Python source spaces to tabs, as requested in #577. Co-authored-by: Mike Bayer <mike_mp@zzzcomputing.com> Fixes: #307 Fixes: #577 Change-Id: I9d2092d20ec23f62ed3b33d979c16b979a450b48
Diffstat (limited to 'alembic/script')
-rw-r--r--alembic/script/base.py11
-rw-r--r--alembic/script/write_hooks.py113
2 files changed, 123 insertions, 1 deletions
diff --git a/alembic/script/base.py b/alembic/script/base.py
index b386dea..8aff3b5 100644
--- a/alembic/script/base.py
+++ b/alembic/script/base.py
@@ -7,6 +7,7 @@ import shutil
from dateutil import tz
from . import revision
+from . import write_hooks
from .. import util
from ..runtime import migration
from ..util import compat
@@ -17,7 +18,7 @@ _legacy_rev = re.compile(r"([a-f0-9]+)\.py$")
_mod_def_re = re.compile(r"(upgrade|downgrade)_([a-z0-9]+)")
_slug_re = re.compile(r"\w+")
_default_file_template = "%(rev)s_%(slug)s"
-_split_on_space_comma = re.compile(r",|(?: +)")
+_split_on_space_comma = re.compile(r", *|(?: +)")
class ScriptDirectory(object):
@@ -50,6 +51,7 @@ class ScriptDirectory(object):
sourceless=False,
output_encoding="utf-8",
timezone=None,
+ hook_config=None,
):
self.dir = dir
self.file_template = file_template
@@ -59,6 +61,7 @@ class ScriptDirectory(object):
self.output_encoding = output_encoding
self.revision_map = revision.RevisionMap(self._load_revisions)
self.timezone = timezone
+ self.hook_config = hook_config
if not os.access(dir, os.F_OK):
raise util.CommandError(
@@ -143,6 +146,7 @@ class ScriptDirectory(object):
output_encoding=config.get_main_option("output_encoding", "utf-8"),
version_locations=version_locations,
timezone=config.get_main_option("timezone"),
+ hook_config=config.get_section("post_write_hooks", {}),
)
@contextmanager
@@ -637,6 +641,11 @@ class ScriptDirectory(object):
message=message if message is not None else ("empty message"),
**kw
)
+
+ post_write_hooks = self.hook_config
+ if post_write_hooks:
+ write_hooks._run_hooks(path, post_write_hooks)
+
try:
script = Script._from_path(self, path)
except revision.RevisionError as err:
diff --git a/alembic/script/write_hooks.py b/alembic/script/write_hooks.py
new file mode 100644
index 0000000..61a6a27
--- /dev/null
+++ b/alembic/script/write_hooks.py
@@ -0,0 +1,113 @@
+import subprocess
+import sys
+
+from .. import util
+from ..util import compat
+
+
+_registry = {}
+
+
+def register(name):
+ """A function decorator that will register that function as a write hook.
+
+ See the documentation linked below for an example.
+
+ .. versionadded:: 1.2.0
+
+ .. seealso::
+
+ :ref:`post_write_hooks_custom`
+
+
+ """
+
+ def decorate(fn):
+ _registry[name] = fn
+
+ return decorate
+
+
+def _invoke(name, revision, options):
+ """Invokes the formatter registered for the given name.
+
+ :param name: The name of a formatter in the registry
+ :param revision: A :class:`.MigrationRevision` instance
+ :param options: A dict containing kwargs passed to the
+ specified formatter.
+ :raises: :class:`alembic.util.CommandError`
+ """
+ try:
+ hook = _registry[name]
+ except KeyError:
+ compat.raise_from_cause(
+ util.CommandError("No formatter with name '%s' registered" % name)
+ )
+ else:
+ return hook(revision, options)
+
+
+def _run_hooks(path, hook_config):
+ """Invoke hooks for a generated revision.
+
+ """
+
+ from .base import _split_on_space_comma
+
+ names = _split_on_space_comma.split(hook_config.get("hooks", ""))
+
+ for name in names:
+ if not name:
+ continue
+ opts = {
+ key[len(name) + 1 :]: hook_config[key]
+ for key in hook_config
+ if key.startswith(name + ".")
+ }
+ opts["_hook_name"] = name
+ try:
+ type_ = opts["type"]
+ except KeyError:
+ compat.raise_from_cause(
+ util.CommandError(
+ "Key %s.type is required for post write hook %r"
+ % (name, name)
+ )
+ )
+ else:
+ util.status(
+ 'Running post write hook "%s"' % name,
+ _invoke,
+ type_,
+ path,
+ opts,
+ newline=True,
+ )
+
+
+@register("console_scripts")
+def console_scripts(path, options):
+ import pkg_resources
+
+ try:
+ entrypoint_name = options["entrypoint"]
+ except KeyError:
+ compat.raise_from_cause(
+ util.CommandError(
+ "Key %s.entrypoint is required for post write hook %r"
+ % (options["_hook_name"], options["_hook_name"])
+ )
+ )
+ iter_ = pkg_resources.iter_entry_points("console_scripts", entrypoint_name)
+ impl = next(iter_)
+ options = options.get("options", "")
+ subprocess.run(
+ [
+ sys.executable,
+ "-c",
+ "import %s; %s()"
+ % (impl.module_name, ".".join((impl.module_name,) + impl.attrs)),
+ path,
+ ]
+ + options.split()
+ )