diff options
-rw-r--r-- | alembic/command.py | 13 | ||||
-rw-r--r-- | alembic/config.py | 7 | ||||
-rw-r--r-- | alembic/util/__init__.py | 2 | ||||
-rw-r--r-- | alembic/util/os_helpers.py | 49 | ||||
-rw-r--r-- | tests/test_command.py | 48 | ||||
-rw-r--r-- | tests/test_os_helpers.py | 44 |
6 files changed, 163 insertions, 0 deletions
diff --git a/alembic/command.py b/alembic/command.py index 3ce5131..644a169 100644 --- a/alembic/command.py +++ b/alembic/command.py @@ -353,3 +353,16 @@ def stamp(config, revision, sql=False, tag=None): tag=tag ): script.run_env() + + +def edit(config): + """Edit the latest ervision""" + + script = ScriptDirectory.from_config(config) + revisions = script.walk_revisions() + head = next(revisions) + + try: + util.open_in_editor(head.path) + except OSError as exc: + raise util.CommandError('Error executing editor (%s)' % (exc,)) diff --git a/alembic/config.py b/alembic/config.py index b3fc36f..3d11916 100644 --- a/alembic/config.py +++ b/alembic/config.py @@ -351,6 +351,13 @@ class CommandLine(object): action="store", help="Specify a revision range; " "format is [start]:[end]") + ), + 'edit': ( + "--edit", + dict( + action="store_true", + help="Edit the latest revision" + ) ) } positional_help = { diff --git a/alembic/util/__init__.py b/alembic/util/__init__.py index bd7196c..0fe9f3c 100644 --- a/alembic/util/__init__.py +++ b/alembic/util/__init__.py @@ -9,6 +9,8 @@ from .pyfiles import ( # noqa from .sqla_compat import ( # noqa sqla_07, sqla_079, sqla_08, sqla_083, sqla_084, sqla_09, sqla_092, sqla_094, sqla_094, sqla_099, sqla_100, sqla_105) +from .os_helpers import ( # noqa + open_in_editor) class CommandError(Exception): diff --git a/alembic/util/os_helpers.py b/alembic/util/os_helpers.py new file mode 100644 index 0000000..ccbfa11 --- /dev/null +++ b/alembic/util/os_helpers.py @@ -0,0 +1,49 @@ +from os.path import join, exists +from subprocess import check_call +import os + + +def open_in_editor(filename, environ=None): + ''' + Opens the given file in a text editor. If the environment vaiable ``EDITOR`` + is set, this is taken as preference. + + Otherwise, a list of commonly installed editors is tried. + + If no editor matches, an :py:exc:`OSError` is raised. + + :param filename: The filename to open. Will be passed verbatim to the + editor command. + :param environ: An optional drop-in replacement for ``os.environ``. Used + mainly for testing. + ''' + + environ = environ or os.environ + + # Look for an editor. Prefer the user's choice by env-var, fall back to most + # commonly installed editor (nano/vim) + candidates = [ + '/usr/bin/sensible-editor', + '/usr/bin/nano', + '/usr/bin/vim', + ] + + if 'EDITOR' in environ: + user_choice = environ['EDITOR'] + if '/' not in user_choice: + # Assuming this is on the PATH, we need to determine it's absolute + # location. Otherwise, ``check_call`` will fail + for path in environ.get('PATH', '').split(os.pathsep): + if exists(join(path, user_choice)): + user_choice = join(path, user_choice) + break + candidates.insert(0, user_choice) + + for path in candidates: + if exists(path): + editor = path + break + else: + raise OSError('No suitable editor found. Please set the ' + '"EDITOR" environment variable') + check_call([editor, filename]) diff --git a/tests/test_command.py b/tests/test_command.py index 0061023..ffb659b 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -1,4 +1,5 @@ from alembic import command +from mock import patch from io import TextIOWrapper, BytesIO from alembic.script import ScriptDirectory from alembic.testing.fixtures import TestBase, capture_context_buffer @@ -371,3 +372,50 @@ down_revision = '%s' self.bind.scalar("select version_num from alembic_version"), self.a ) + + +class EditTest(TestBase): + + @classmethod + def setup_class(cls): + cls.env = staging_env() + cls.cfg = _sqlite_testing_config() + cls.a, cls.b, cls.c = three_rev_fixture(cls.cfg) + + @classmethod + def teardown_class(cls): + clear_staging_env() + + def test_edit_with_user_editor(self): + expected_call_arg = '%s/scripts/versions/%s_revision_c.py' % ( + EditTest.cfg.config_args['here'], + EditTest.c + ) + + with patch('alembic.util.os_helpers.check_call') as check_call, \ + patch('alembic.util.os_helpers.exists') as exists: + exists.side_effect = lambda fname: fname == '/usr/bin/vim' + command.edit(self.cfg) + check_call.assert_called_with(['/usr/bin/vim', expected_call_arg]) + + def test_edit_with_default_editor(self): + expected_call_arg = '%s/scripts/versions/%s_revision_c.py' % ( + EditTest.cfg.config_args['here'], + EditTest.c + ) + + with patch('alembic.util.os_helpers.check_call') as check_call, \ + patch('alembic.util.os_helpers.exists') as exists: + exists.side_effect = lambda fname: fname == '/usr/bin/vim' + command.edit(self.cfg) + check_call.assert_called_with(['/usr/bin/vim', expected_call_arg]) + + def test_edit_with_missing_editor(self): + with patch('alembic.util.os_helpers.check_call'), \ + patch('alembic.util.os_helpers.exists') as exists: + exists.return_value = False + assert_raises_message( + util.CommandError, + 'EDITOR', + command.edit, + self.cfg) diff --git a/tests/test_os_helpers.py b/tests/test_os_helpers.py new file mode 100644 index 0000000..220e114 --- /dev/null +++ b/tests/test_os_helpers.py @@ -0,0 +1,44 @@ +from alembic import util +from alembic.testing import assert_raises_message +from alembic.testing.fixtures import TestBase + +try: + from unittest.mock import patch +except ImportError: + from mock import patch # noqa + + +class TestHelpers(TestBase): + + def test_edit_with_user_editor(self): + test_environ = { + 'EDITOR': 'myvim', + 'PATH': '/usr/bin' + } + + with patch('alembic.util.os_helpers.check_call') as check_call, \ + patch('alembic.util.os_helpers.exists') as exists: + exists.side_effect = lambda fname: fname == '/usr/bin/myvim' + util.open_in_editor('myfile', test_environ) + check_call.assert_called_with(['/usr/bin/myvim', 'myfile']) + + def test_edit_with_default_editor(self): + test_environ = {} + + with patch('alembic.util.os_helpers.check_call') as check_call, \ + patch('alembic.util.os_helpers.exists') as exists: + exists.side_effect = lambda fname: fname == '/usr/bin/vim' + util.open_in_editor('myfile', test_environ) + check_call.assert_called_with(['/usr/bin/vim', 'myfile']) + + def test_edit_with_missing_editor(self): + test_environ = {} + with patch('alembic.util.os_helpers.check_call'), \ + patch('alembic.util.os_helpers.exists') as exists: + exists.return_value = False + assert_raises_message( + OSError, + 'EDITOR', + util.open_in_editor, + 'myfile', + test_environ) |