Skip to content

Commit

Permalink
Merge pull request #16 from corydodt/15-hotedit
Browse files Browse the repository at this point in the history
#15 hotedit api
  • Loading branch information
corydodt authored Nov 26, 2019
2 parents 965c093 + 392f94e commit 245d2b2
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 0 deletions.
103 changes: 103 additions & 0 deletions codado/hotedit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""
Invoke user's editor on a temp file and present the edited contents back to the invoking code
"""
from __future__ import print_function

import os
import pipes
import shlex
import subprocess
import tempfile


TEMP_EXT = '.hotedit'
EDITOR_FALLBACK = "vi"


class HoteditException(Exception):
"""
Base for any hotedit exception
"""


class EditingException(HoteditException):
"""
Raised to signal that an edited file was left unchanged
"""


class Unchanged(HoteditException):
"""
Raised to signal that an edited file was left unchanged
"""


def determine_editor(fallback=EDITOR_FALLBACK):
"""
Figure out which of many possible options is the editor, starting with:
-- git config core.editor
-- EDITOR
-- VISUAL
If none of these is a usable editor, return the value of `fallback'.
If fallback=None, and none of these is a usable editor, raise OSError
"""
try:
ret = subprocess.check_output(shlex.split('git config core.editor'))
return ret.strip()
except subprocess.CalledProcessError:
"git config core.editor didn't work, falling back to environment"

if os.environ.get('EDITOR'):
return os.environ['EDITOR']

if os.environ.get('VISUAL'):
return os.environ['VISUAL']

if fallback:
return fallback

raise OSError("No editor found (checked git, $EDITOR and $VISUAL)")


_remove = os.remove


def hotedit(initial=None, validate_unchanged=False, delete_temp=True, find_editor=determine_editor):
"""
Edit `initial' string as the contents of a temp file in a local editor
With `validate_unchanged' == True, raise the Unchanged exception when the file is closed with no changes
With `delete_temp' == False, leave the temp file alone after editing, otherwise remove it by default
`find_editor' is a zero-argument callable that returns an editor string, using hotedit.determine_editor by default
Exceptions while invoking the editor are re-raised
@returns the string after editing.
"""
path = None
try:
handle, path = tempfile.mkstemp(TEMP_EXT)
with os.fdopen(handle, 'w') as f:
f.write(initial)

cmd = shlex.split(find_editor()) + [path]
rc = subprocess.call(cmd)
if rc != 0:
quoted = " ".join([pipes.quote(x) for x in cmd])
raise EditingException("Command '%s' returned non-zero exit status %s" % (quoted, rc))

with open(path, 'r') as tmp:
edited = tmp.read()

if validate_unchanged and edited.strip() == initial.strip():
raise Unchanged("No changes since editing started")

return edited

finally:
if delete_temp and path and os.path.exists(path):
_remove(path)
78 changes: 78 additions & 0 deletions codado/test/test_hotedit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
Test hotedit, the tool for invoking a local editor on a temp file
"""
import os
import subprocess

from mock import ANY, patch

from pytest import raises

from codado import hotedit


def test_determine_editor():
"""
Do I figure out the right editor based on the running environment?
"""
with patch.object(subprocess, 'check_output', return_value='giteditor'):
assert hotedit.determine_editor() == 'giteditor'

pCheckOutput = patch.object(subprocess, 'check_output',
side_effect=subprocess.CalledProcessError('ohh', 'noo'))
env = os.environ.copy()
pEnviron = patch.object(os, 'environ', env)
with pCheckOutput, pEnviron:
env['VISUAL'] = 'visual'
assert hotedit.determine_editor() == 'visual'

env['EDITOR'] = 'editor'
assert hotedit.determine_editor() == 'editor'

del env['VISUAL'], env['EDITOR']

assert hotedit.determine_editor() == 'vi'

with raises(OSError):
hotedit.determine_editor(fallback=None)


def test_hotedit():
"""
What happens if hotedit is called with an editor we can't find?
"""
find_editor = lambda: '---asdfasdfwsdf---'

input_ = "asdfasdfa"

# 1. cmd does not exist -> OSError
with raises(OSError):
hotedit.hotedit(input_, find_editor=find_editor)

# 2. error while editing -> EditingException
pSubprocessCall = patch.object(subprocess, 'call', return_value=19)
with pSubprocessCall, raises(hotedit.EditingException):
hotedit.hotedit(input_, find_editor=find_editor)

pRemove = patch.object(hotedit, "_remove", autospec=True)

# 3. unchanged while editing + validate_unchanged -> Unchanged
# 5. any exception -> temp is removed
pSubprocessCall = patch.object(subprocess, 'call', return_value=0)
with pRemove as mRemove, pSubprocessCall, raises(hotedit.Unchanged):
hotedit.hotedit(input_, find_editor=find_editor, validate_unchanged=True)
mRemove.assert_called_once_with(ANY)

# 4. unchanged while editing + !validate_unchanged -> string
# 6. happy path -> return edited string, temp is removed
pSubprocessCall = patch.object(subprocess, 'call', return_value=0)
with pRemove as mRemove, pSubprocessCall:
ret = hotedit.hotedit(input_, find_editor=find_editor, validate_unchanged=False)
assert ret == input_
mRemove.assert_called_once_with(ANY)

# 7. happy path + !delete_temp -> return edited string, temp is left
with pRemove as mRemove, pSubprocessCall:
ret = hotedit.hotedit(input_, find_editor=find_editor, validate_unchanged=False, delete_temp=False)
assert ret == input_
assert mRemove.call_count == 0

0 comments on commit 245d2b2

Please sign in to comment.