-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from corydodt/15-hotedit
#15 hotedit api
- Loading branch information
Showing
2 changed files
with
181 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |