Skip to content

Commit 2cba45e

Browse files
committed
Merge branch 'better-dot'
* better-dot: Incorporate review feedback Extract dot commands to separate object
2 parents 6cadb6c + b2b5b91 commit 2cba45e

File tree

3 files changed

+133
-12
lines changed

3 files changed

+133
-12
lines changed

awsshell/app.py

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
55
"""
66
from __future__ import unicode_literals
7+
import os
78
import tempfile
89
import subprocess
910
import logging
11+
import sys
1012

1113
from prompt_toolkit.document import Document
1214
from prompt_toolkit.shortcuts import create_eventloop
@@ -25,6 +27,7 @@
2527
from awsshell.style import StyleFactory
2628
from awsshell.toolbar import Toolbar
2729
from awsshell.utils import build_config_file_path
30+
from awsshell import compat
2831

2932

3033
LOG = logging.getLogger(__name__)
@@ -43,6 +46,76 @@ class InputInterrupt(Exception):
4346
pass
4447

4548

49+
class EditHandler(object):
50+
def __init__(self, popen_cls=None, env=None):
51+
if popen_cls is None:
52+
popen_cls = subprocess.Popen
53+
self._popen_cls = popen_cls
54+
if env is None:
55+
env = os.environ
56+
self._env = env
57+
58+
def _get_editor_command(self):
59+
if 'EDITOR' in self._env:
60+
return self._env['EDITOR']
61+
else:
62+
return compat.default_editor()
63+
64+
def run(self, command, application):
65+
"""Open application's history buffer in an editor.
66+
67+
:type command: list
68+
:param command: The dot command as a list split
69+
on whitespace, e.g ``['.foo', 'arg1', 'arg2']``
70+
71+
:type application: AWSShell
72+
:param application: The application object.
73+
74+
"""
75+
all_commands = '\n'.join(
76+
['aws ' + h for h in list(application.history)
77+
if not h.startswith(('.', '!'))])
78+
with tempfile.NamedTemporaryFile('w') as f:
79+
f.write(all_commands)
80+
f.flush()
81+
editor = self._get_editor_command()
82+
p = self._popen_cls([editor, f.name])
83+
p.communicate()
84+
85+
86+
class DotCommandHandler(object):
87+
HANDLER_CLASSES = {
88+
'edit': EditHandler,
89+
}
90+
91+
def __init__(self, output=sys.stdout, err=sys.stderr):
92+
self._output = output
93+
self._err = err
94+
95+
def handle_cmd(self, command, application):
96+
"""Handles running a given dot command from a user.
97+
98+
:type command: str
99+
:param command: The full dot command string, e.g. ``.edit``,
100+
of ``.profile prod``.
101+
102+
:type application: AWSShell
103+
:param application: The application object.
104+
105+
"""
106+
parts = command.split()
107+
cmd_name = parts[0][1:]
108+
if cmd_name not in self.HANDLER_CLASSES:
109+
self._unknown_cmd(parts, application)
110+
else:
111+
# Note we expect the class to support no-arg
112+
# instantiation.
113+
self.HANDLER_CLASSES[cmd_name]().run(parts, application)
114+
115+
def _unknown_cmd(self, cmd_parts, application):
116+
self._err.write("Unknown dot command: %s\n" % cmd_parts[0])
117+
118+
46119
class AWSShell(object):
47120
"""Encapsulates the ui, completer, command history, docs, and config.
48121
@@ -81,12 +154,13 @@ class AWSShell(object):
81154
def __init__(self, completer, model_completer, docs):
82155
self.completer = completer
83156
self.model_completer = model_completer
84-
self.memory_history = InMemoryHistory()
157+
self.history = InMemoryHistory()
85158
self.file_history = FileHistory(build_config_file_path('history'))
86159
self._cli = None
87160
self._docs = docs
88161
self.current_docs = u''
89162
self.refresh_cli = False
163+
self._dot_cmd = DotCommandHandler()
90164
self.load_config()
91165

92166
def load_config(self):
@@ -136,23 +210,14 @@ def run(self):
136210
if text.startswith('.'):
137211
# These are special commands. The only one supported for
138212
# now is .edit.
139-
if text.startswith('.edit'):
140-
# TODO: Use EDITOR env var.
141-
all_commands = '\n'.join(
142-
['aws ' + h for h in list(self.memory_history)
143-
if not h.startswith(('.', '!'))])
144-
with tempfile.NamedTemporaryFile('w') as f:
145-
f.write(all_commands)
146-
f.flush()
147-
p = subprocess.Popen(['vim', f.name])
148-
p.communicate()
213+
self._dot_cmd.handle_cmd(text, application=self)
149214
else:
150215
if text.startswith('!'):
151216
# Then run the rest as a normally shell command.
152217
full_cmd = text[1:]
153218
else:
154219
full_cmd = 'aws ' + text
155-
self.memory_history.append(full_cmd)
220+
self.history.append(full_cmd)
156221
self.current_docs = u''
157222
self.cli.buffers['clidocs'].reset(
158223
initial_document=Document(self.current_docs,

awsshell/compat.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
from __future__ import print_function
22
import sys
3+
import platform
4+
35

46
PY3 = sys.version_info[0] == 3
7+
ON_WINDOWS = platform.system() == 'Windows'
8+
59

610
if PY3:
711
from html.parser import HTMLParser
@@ -13,3 +17,11 @@
1317
text_type = unicode
1418
from cStringIO import StringIO
1519
import anydbm as dbm
20+
21+
22+
if ON_WINDOWS:
23+
def default_editor():
24+
return 'notepad.exe'
25+
else:
26+
def default_editor():
27+
return 'vim'

tests/unit/test_app.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import pytest
2+
import mock
3+
4+
5+
from awsshell import app
6+
from awsshell import compat
7+
8+
9+
@pytest.fixture
10+
def errstream():
11+
return compat.StringIO()
12+
13+
14+
def test_can_dispatch_dot_commands():
15+
call_args = []
16+
class CustomHandler(object):
17+
def run(self, command, context):
18+
call_args.append((command, context))
19+
handler = app.DotCommandHandler()
20+
handler.HANDLER_CLASSES['foo'] = CustomHandler
21+
context = object()
22+
23+
handler.handle_cmd('.foo a b c', context)
24+
25+
assert call_args == [(['.foo', 'a', 'b', 'c'], context)]
26+
27+
28+
def test_edit_handler():
29+
env = {'EDITOR': 'my-editor'}
30+
popen_cls = mock.Mock()
31+
context = mock.Mock()
32+
context.history = []
33+
handler = app.EditHandler(popen_cls, env)
34+
handler.run(['.edit'], context)
35+
# Ensure our editor was called with some arbitrary temp filename.
36+
command_run = popen_cls.call_args[0][0]
37+
assert len(command_run) == 2
38+
assert command_run[0] == 'my-editor'
39+
40+
41+
def test_prints_error_message_on_unknown_dot_command(errstream):
42+
handler = app.DotCommandHandler(err=errstream)
43+
handler.handle_cmd(".unknown foo bar", None)
44+
assert errstream.getvalue() == "Unknown dot command: .unknown\n"

0 commit comments

Comments
 (0)