Merge lp:~frankban/juju-quickstart/improve-input-handling into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 39
Proposed branch: lp:~frankban/juju-quickstart/improve-input-handling
Merge into: lp:juju-quickstart
Diff against target: 337 lines (+110/-41)
6 files modified
cli-app-demo.py (+3/-0)
quickstart/cli/base.py (+17/-2)
quickstart/cli/views.py (+25/-17)
quickstart/tests/cli/helpers.py (+31/-0)
quickstart/tests/cli/test_base.py (+20/-1)
quickstart/tests/cli/test_views.py (+14/-21)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/improve-input-handling
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+199640@code.launchpad.net

Description of the change

Improve views input handling.

Add a new set_return_value_on_exit API that
views can call to set the value encapsulated
in the AppExit exception raised when the user
quits the application with ^X.

Tests: `make check`

QA: start the demo app
(`make` and `./cli-app-demo.py).
Try exiting with ^X: you should see the
"no environment selected" message.
Restart and exit selecting an environment:
you should see a message indicating that
environment has been selected.
Restart and change the default environment,
then exit: you should see the new env_db
printed and the new default environment.

https://codereview.appspot.com/43600047/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

Reviewers: mp+199640_code.launchpad.net,

Message:
Please take a look.

Description:
Improve views input handling.

Add a new set_return_value_on_exit API that
views can call to set the value encapsulated
in the AppExit exception raised when the user
quits the application with ^X.

Tests: `make check`

QA: start the demo app
(`make` and `./cli-app-demo.py).
Try exiting with ^X: you should see the
"no environment selected" message.
Restart and exit selecting an environment:
you should see a message indicating that
environment has been selected.
Restart and change the default environment,
then exit: you should see the new env_db
printed and the new default environment.

https://code.launchpad.net/~frankban/juju-quickstart/improve-input-handling/+merge/199640

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/43600047/

Affected files (+110, -41 lines):
   A [revision details]
   M cli-app-demo.py
   M quickstart/cli/base.py
   M quickstart/cli/views.py
   A quickstart/tests/cli/helpers.py
   M quickstart/tests/cli/test_base.py
   M quickstart/tests/cli/test_views.py

Revision history for this message
Madison Scott-Clary (makyo) wrote :
44. By Francesco Banconi

Add missing import unicode_literals.

Revision history for this message
Francesco Banconi (frankban) wrote :

*** Submitted:

Improve views input handling.

Add a new set_return_value_on_exit API that
views can call to set the value encapsulated
in the AppExit exception raised when the user
quits the application with ^X.

Tests: `make check`

QA: start the demo app
(`make` and `./cli-app-demo.py).
Try exiting with ^X: you should see the
"no environment selected" message.
Restart and exit selecting an environment:
you should see a message indicating that
environment has been selected.
Restart and change the default environment,
then exit: you should see the new env_db
printed and the new default environment.

R=matthew.scott
CC=
https://codereview.appspot.com/43600047

https://codereview.appspot.com/43600047/

Revision history for this message
Francesco Banconi (frankban) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cli-app-demo.py'
2--- cli-app-demo.py 2013-12-18 15:41:31 +0000
3+++ cli-app-demo.py 2013-12-19 17:41:55 +0000
4@@ -27,6 +27,8 @@
5 unicode_literals,
6 )
7
8+import pprint
9+
10 from quickstart.cli import views
11 from quickstart.models import envs
12 from quickstart.tests import helpers
13@@ -43,6 +45,7 @@
14 views.env_index, env_type_db, env_db, save_callable)
15 if new_env_db != env_db:
16 print('saved a new env db')
17+ pprint.pprint(new_env_db)
18 print('default: {}'.format(new_env_db.get('default')))
19 if env_data is None:
20 print('no environment selected')
21
22=== modified file 'quickstart/cli/base.py'
23--- quickstart/cli/base.py 2013-12-19 09:12:37 +0000
24+++ quickstart/cli/base.py 2013-12-19 17:41:55 +0000
25@@ -40,6 +40,7 @@
26 'set_contents', 'get_contents',
27 'set_status', 'get_status',
28 'set_message', 'get_message',
29+ 'set_return_value_on_exit',
30 ],
31 )
32
33@@ -102,7 +103,12 @@
34 - set_message(text): set/change a notification message, which is
35 displayed in the footer for a couple of seconds before disappearing;
36 - get_message(): return the message currently displayed in the
37- notifications area.
38+ notifications area;
39+
40+ - set_return_value_on_exit(value): set the value to be encapsulated
41+ in the AppExit exception raised when the user quits the application
42+ with the exit shortcut. See the quickstart.cli.views module docstring
43+ for more information about this functionality.
44 """
45 # Set up the application header.
46 title = urwid.Text('\npreparing...')
47@@ -143,10 +149,18 @@
48 valign='middle', height=('relative', 90),
49 min_width=78, min_height=20)
50 # Instantiate the Urwid main loop.
51- loop = _MainLoop(top_widget, palette=ui.PALETTE)
52+ loop = _MainLoop(
53+ top_widget, palette=ui.PALETTE,
54+ unhandled_input=ui.exit_and_return(None))
55 # Add a timeout to the notification message.
56 timeout_message = ui.TimeoutText(
57 message, 3, loop.set_alarm_in, loop.remove_alarm)
58+
59+ # Allow views to set the value returned when the user quits the session.
60+ def set_return_value_on_exit(return_value):
61+ unhandled_input = ui.exit_and_return(return_value)
62+ loop.set_unhandled_input(unhandled_input)
63+
64 # Create the App named tuple. If, in the future, we have a view that
65 # requires additional capabilities or API access, this is the place to add
66 # those.
67@@ -159,5 +173,6 @@
68 get_status=lambda: status.text,
69 set_message=lambda msg: timeout_message.set_text(('message', msg)),
70 get_message=lambda: timeout_message.text,
71+ set_return_value_on_exit=set_return_value_on_exit,
72 )
73 return loop, app
74
75=== modified file 'quickstart/cli/views.py'
76--- quickstart/cli/views.py 2013-12-19 09:30:08 +0000
77+++ quickstart/cli/views.py 2013-12-19 17:41:55 +0000
78@@ -38,7 +38,7 @@
79
80 def myview(app, title):
81 app.set_title(title)
82- return 42
83+ app.set_return_value_on_exit(42)
84
85 The view above, requiring a title argument, can be started this way:
86
87@@ -52,9 +52,12 @@
88 2) a view decides it is time to quit (e.g. reacting to an event/input).
89
90 In both cases, the show function returns something to the caller:
91- 1) when the user explicitly requests to quit, the value returned by the
92- view itself (the callable passed to show()) is returned. For instance,
93- the show(myview...) call above would return 42;
94+ 1) when the user explicitly requests to quit, None is returned by default.
95+ However, the view can change this default value by calling
96+ app.set_return_value_on_exit(some value). For instance, the
97+ show(myview...) call above would return 42. It is safe to call the
98+ set_return_value_on_exit API multiple times in order to overwrite the
99+ value returned on user exit;
100 2) to force the end of the interactive session, a view can raise a
101 quickstart.cli.ui.AppExit exception, passing a return value: if the
102 application is exited this way, then show() returns the value
103@@ -71,18 +74,19 @@
104 def exit():
105 raise ui.AppExit(True)
106
107+ app.set_return_value_on_exit(False)
108 app.set_title('behold the button below')
109 button = ui.MenuButton('press to exit', ui.thunk(exit))
110 widgets = urwid.ListBox(urwid.SimpleFocusListWalker([button]))
111 app.set_contents(widgets)
112- return False
113
114 pressed = views.show(button_view)
115
116 In this example the button_view function configures the App to show a button.
117-Clicking that button an AppExit(True) is raised. The view itself instead just
118-returns False. This means that "pressed" will be True if the user exited using
119-the button, or False if the user exited using the global shortcut.
120+Clicking that button an AppExit(True) is raised. The return value on exit
121+instead is set by the view itself to False. This means that "pressed" will be
122+True if the user exited using the button, or False if the user exited using the
123+global shortcut.
124
125 As a final note, it is absolutely safe for a view to call, directly or
126 indirectly, other views, as long as all the arguments required by the other
127@@ -111,16 +115,14 @@
128 The view is called passing an App named tuple and the provided *args.
129
130 Block until the main loop is stopped, either by the user with the exit
131- shortcut or by the view itself with the AppExit exception. In the former
132- case, return what is returned by the view. In the AppExit case, return
133- the value encapsulated in the exception.
134+ shortcut or by the view itself. In both cases, an ui.AppExit is raised, and
135+ the return value is encapsulated in the exception.
136 """
137 loop, app = base.setup_urwid_app()
138- default_return_value = view(app, *args)
139- unhandled_input = ui.exit_and_return(default_return_value)
140- loop.set_unhandled_input(unhandled_input)
141- # Start the Urwid interactive session (main loop).
142 try:
143+ # Execute the view.
144+ view(app, *args)
145+ # Start the Urwid interactive session (main loop).
146 loop.run()
147 except ui.AppExit as err:
148 return err.return_value
149@@ -137,6 +139,10 @@
150 - save_callable: a function called to save a new environment database.
151 """
152 env_db = copy.deepcopy(env_db)
153+ # All the environment views return a tuple (new_env_db, env_data).
154+ # Set the env_data to None in the case the user quits the application
155+ # without selecting an environment to use.
156+ app.set_return_value_on_exit((env_db, None))
157 detail_view = functools.partial(
158 env_detail, app, env_type_db, env_db, save_callable)
159 # Alphabetically sort the existing environments.
160@@ -185,7 +191,6 @@
161 if status:
162 app.set_status(status)
163 app.set_contents(contents)
164- return env_db, None
165
166
167 def env_detail(app, env_type_db, env_db, save_callable, env_data):
168@@ -201,6 +206,10 @@
169 - env_data: the environment data.
170 """
171 env_db = copy.deepcopy(env_db)
172+ # All the environment views return a tuple (new_env_db, env_data).
173+ # Set the env_data to None in the case the user quits the application
174+ # without selecting an environment to use.
175+ app.set_return_value_on_exit((env_db, None))
176 index_view = functools.partial(
177 env_index, app, env_type_db, env_db, save_callable)
178
179@@ -250,4 +259,3 @@
180 widgets.append(ui.create_controls(*controls))
181 listbox = urwid.ListBox(urwid.SimpleFocusListWalker(widgets))
182 app.set_contents(listbox)
183- return env_db, None
184
185=== added file 'quickstart/tests/cli/helpers.py'
186--- quickstart/tests/cli/helpers.py 1970-01-01 00:00:00 +0000
187+++ quickstart/tests/cli/helpers.py 2013-12-19 17:41:55 +0000
188@@ -0,0 +1,31 @@
189+# This file is part of the Juju Quickstart Plugin, which lets users set up a
190+# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
191+# Copyright (C) 2013 Canonical Ltd.
192+#
193+# This program is free software: you can redistribute it and/or modify it under
194+# the terms of the GNU Affero General Public License version 3, as published by
195+# the Free Software Foundation.
196+#
197+# This program is distributed in the hope that it will be useful, but WITHOUT
198+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
199+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
200+# Affero General Public License for more details.
201+#
202+# You should have received a copy of the GNU Affero General Public License
203+# along with this program. If not, see <http://www.gnu.org/licenses/>.
204+
205+"""Test helpers for the Juju Quickstart CLI infrastructure."""
206+
207+from __future__ import unicode_literals
208+
209+from quickstart.cli import ui
210+
211+
212+class CliAppTestsMixin(object):
213+ """Helper methods to test Quickstart CLI applications."""
214+
215+ def get_on_exit_return_value(self, loop):
216+ """Return the value returned by the application when the user quits."""
217+ with self.assertRaises(ui.AppExit) as context_manager:
218+ loop.unhandled_input(ui.EXIT_KEY)
219+ return context_manager.exception.return_value
220
221=== modified file 'quickstart/tests/cli/test_base.py'
222--- quickstart/tests/cli/test_base.py 2013-12-19 09:12:37 +0000
223+++ quickstart/tests/cli/test_base.py 2013-12-19 17:41:55 +0000
224@@ -23,6 +23,7 @@
225 import urwid
226
227 from quickstart.cli import base
228+from quickstart.tests.cli import helpers as cli_helpers
229
230
231 class TestMainLoop(unittest.TestCase):
232@@ -57,7 +58,7 @@
233 self.assertEqual(1, sum(times_called))
234
235
236-class TestSetupUrwidApp(unittest.TestCase):
237+class TestSetupUrwidApp(cli_helpers.CliAppTestsMixin, unittest.TestCase):
238
239 def setUp(self):
240 # Set up the base Urwid application.
241@@ -164,3 +165,21 @@
242 message_widget = self.get_message_widget(self.loop)
243 message_widget.set_text('42')
244 self.assertEqual('42', self.app.get_message())
245+
246+ def test_default_unhandled_input(self):
247+ # The default unhandled_input function is configured so that a
248+ # quickstart.cli.ui.AppExit exception is raised. The exception's
249+ # return value is None by default.
250+ return_value = self.get_on_exit_return_value(self.loop)
251+ self.assertIsNone(return_value)
252+
253+ def test_set_return_value_on_exit(self):
254+ # It is possible to change the value returned by the AppExit exception
255+ # when the user quits the application using the exit shortcut.
256+ self.app.set_return_value_on_exit(42)
257+ return_value = self.get_on_exit_return_value(self.loop)
258+ self.assertEqual(42, return_value)
259+ # The value can be changed multiple times.
260+ self.app.set_return_value_on_exit([47, None])
261+ return_value = self.get_on_exit_return_value(self.loop)
262+ self.assertEqual([47, None], return_value)
263
264=== modified file 'quickstart/tests/cli/test_views.py'
265--- quickstart/tests/cli/test_views.py 2013-12-19 09:30:08 +0000
266+++ quickstart/tests/cli/test_views.py 2013-12-19 17:41:55 +0000
267@@ -31,6 +31,7 @@
268 )
269 from quickstart.models import envs
270 from quickstart.tests import helpers
271+from quickstart.tests.cli import helpers as cli_helpers
272
273
274 class TestShow(unittest.TestCase):
275@@ -70,17 +71,6 @@
276 return_value = views.show(view)
277 self.assertEqual('bad wolf', return_value)
278
279- def test_unhandled_input(self):
280- # The unhandled_input callable is properly set up and registered to
281- # the main loop.
282- view = mock.Mock(return_value=42)
283- with self.patch_setup_urwid_app() as (mock_loop, mock_app):
284- views.show(view)
285- unhandled_input = mock_loop.set_unhandled_input.call_args[0][0]
286- with self.assertRaises(ui.AppExit) as context_manager:
287- unhandled_input(ui.EXIT_KEY)
288- self.assertEqual(42, context_manager.exception.return_value)
289-
290 def test_view_arguments(self):
291 # The view is invoked passing the app and all the optional show
292 # function arguments.
293@@ -90,7 +80,7 @@
294 view.assert_called_once_with(mock_app, 'arg1', 'arg2')
295
296
297-class EnvViewTestsMixin(object):
298+class EnvViewTestsMixin(cli_helpers.CliAppTestsMixin):
299 """Shared helpers for testing environment views."""
300
301 env_type_db = envs.get_env_type_db()
302@@ -145,12 +135,13 @@
303
304 class TestEnvIndex(EnvViewTestsMixin, unittest.TestCase):
305
306- def test_view_return_value(self):
307- # The view returns a tuple including a copy of the given env_db and
308- # None, the latter meaning no environment has been selected.
309+ def test_view_default_return_value_on_exit(self):
310+ # The view configures the app so that the return value on user exit is
311+ # a tuple including a copy of the given env_db and None, the latter
312+ # meaning no environment has been selected.
313 env_db = helpers.make_env_db()
314- new_env_db, env_data = views.env_index(
315- self.app, self.env_type_db, env_db, self.save_callable)
316+ views.env_index(self.app, self.env_type_db, env_db, self.save_callable)
317+ new_env_db, env_data = self.get_on_exit_return_value(self.loop)
318 self.assertEqual(env_db, new_env_db)
319 self.assertIsNot(env_db, new_env_db)
320 self.assertIsNone(env_data)
321@@ -249,10 +240,12 @@
322 self.app, self.env_type_db, self.env_db, self.save_callable,
323 self.env_data)
324
325- def test_view_return_value(self):
326- # The view returns a tuple including a copy of the given env_db and
327- # None, the latter meaning no environment has been selected (for now).
328- new_env_db, env_data = self.call_view()
329+ def test_view_default_return_value_on_exit(self):
330+ # The view configures the app so that the return value on user exit is
331+ # a tuple including a copy of the given env_db and None, the latter
332+ # meaning no environment has been selected (for now).
333+ self.call_view()
334+ new_env_db, env_data = self.get_on_exit_return_value(self.loop)
335 self.assertEqual(self.env_db, new_env_db)
336 self.assertIsNot(self.env_db, new_env_db)
337 self.assertIsNone(env_data)

Subscribers

People subscribed via source and target branches