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
=== modified file 'cli-app-demo.py'
--- cli-app-demo.py 2013-12-18 15:41:31 +0000
+++ cli-app-demo.py 2013-12-19 17:41:55 +0000
@@ -27,6 +27,8 @@
27 unicode_literals,27 unicode_literals,
28)28)
2929
30import pprint
31
30from quickstart.cli import views32from quickstart.cli import views
31from quickstart.models import envs33from quickstart.models import envs
32from quickstart.tests import helpers34from quickstart.tests import helpers
@@ -43,6 +45,7 @@
43 views.env_index, env_type_db, env_db, save_callable)45 views.env_index, env_type_db, env_db, save_callable)
44 if new_env_db != env_db:46 if new_env_db != env_db:
45 print('saved a new env db')47 print('saved a new env db')
48 pprint.pprint(new_env_db)
46 print('default: {}'.format(new_env_db.get('default')))49 print('default: {}'.format(new_env_db.get('default')))
47 if env_data is None:50 if env_data is None:
48 print('no environment selected')51 print('no environment selected')
4952
=== modified file 'quickstart/cli/base.py'
--- quickstart/cli/base.py 2013-12-19 09:12:37 +0000
+++ quickstart/cli/base.py 2013-12-19 17:41:55 +0000
@@ -40,6 +40,7 @@
40 'set_contents', 'get_contents',40 'set_contents', 'get_contents',
41 'set_status', 'get_status',41 'set_status', 'get_status',
42 'set_message', 'get_message',42 'set_message', 'get_message',
43 'set_return_value_on_exit',
43 ],44 ],
44)45)
4546
@@ -102,7 +103,12 @@
102 - set_message(text): set/change a notification message, which is103 - set_message(text): set/change a notification message, which is
103 displayed in the footer for a couple of seconds before disappearing;104 displayed in the footer for a couple of seconds before disappearing;
104 - get_message(): return the message currently displayed in the105 - get_message(): return the message currently displayed in the
105 notifications area.106 notifications area;
107
108 - set_return_value_on_exit(value): set the value to be encapsulated
109 in the AppExit exception raised when the user quits the application
110 with the exit shortcut. See the quickstart.cli.views module docstring
111 for more information about this functionality.
106 """112 """
107 # Set up the application header.113 # Set up the application header.
108 title = urwid.Text('\npreparing...')114 title = urwid.Text('\npreparing...')
@@ -143,10 +149,18 @@
143 valign='middle', height=('relative', 90),149 valign='middle', height=('relative', 90),
144 min_width=78, min_height=20)150 min_width=78, min_height=20)
145 # Instantiate the Urwid main loop.151 # Instantiate the Urwid main loop.
146 loop = _MainLoop(top_widget, palette=ui.PALETTE)152 loop = _MainLoop(
153 top_widget, palette=ui.PALETTE,
154 unhandled_input=ui.exit_and_return(None))
147 # Add a timeout to the notification message.155 # Add a timeout to the notification message.
148 timeout_message = ui.TimeoutText(156 timeout_message = ui.TimeoutText(
149 message, 3, loop.set_alarm_in, loop.remove_alarm)157 message, 3, loop.set_alarm_in, loop.remove_alarm)
158
159 # Allow views to set the value returned when the user quits the session.
160 def set_return_value_on_exit(return_value):
161 unhandled_input = ui.exit_and_return(return_value)
162 loop.set_unhandled_input(unhandled_input)
163
150 # Create the App named tuple. If, in the future, we have a view that164 # Create the App named tuple. If, in the future, we have a view that
151 # requires additional capabilities or API access, this is the place to add165 # requires additional capabilities or API access, this is the place to add
152 # those.166 # those.
@@ -159,5 +173,6 @@
159 get_status=lambda: status.text,173 get_status=lambda: status.text,
160 set_message=lambda msg: timeout_message.set_text(('message', msg)),174 set_message=lambda msg: timeout_message.set_text(('message', msg)),
161 get_message=lambda: timeout_message.text,175 get_message=lambda: timeout_message.text,
176 set_return_value_on_exit=set_return_value_on_exit,
162 )177 )
163 return loop, app178 return loop, app
164179
=== modified file 'quickstart/cli/views.py'
--- quickstart/cli/views.py 2013-12-19 09:30:08 +0000
+++ quickstart/cli/views.py 2013-12-19 17:41:55 +0000
@@ -38,7 +38,7 @@
3838
39 def myview(app, title):39 def myview(app, title):
40 app.set_title(title)40 app.set_title(title)
41 return 4241 app.set_return_value_on_exit(42)
4242
43The view above, requiring a title argument, can be started this way:43The view above, requiring a title argument, can be started this way:
4444
@@ -52,9 +52,12 @@
52 2) a view decides it is time to quit (e.g. reacting to an event/input).52 2) a view decides it is time to quit (e.g. reacting to an event/input).
5353
54In both cases, the show function returns something to the caller:54In both cases, the show function returns something to the caller:
55 1) when the user explicitly requests to quit, the value returned by the55 1) when the user explicitly requests to quit, None is returned by default.
56 view itself (the callable passed to show()) is returned. For instance,56 However, the view can change this default value by calling
57 the show(myview...) call above would return 42;57 app.set_return_value_on_exit(some value). For instance, the
58 show(myview...) call above would return 42. It is safe to call the
59 set_return_value_on_exit API multiple times in order to overwrite the
60 value returned on user exit;
58 2) to force the end of the interactive session, a view can raise a61 2) to force the end of the interactive session, a view can raise a
59 quickstart.cli.ui.AppExit exception, passing a return value: if the62 quickstart.cli.ui.AppExit exception, passing a return value: if the
60 application is exited this way, then show() returns the value63 application is exited this way, then show() returns the value
@@ -71,18 +74,19 @@
71 def exit():74 def exit():
72 raise ui.AppExit(True)75 raise ui.AppExit(True)
7376
77 app.set_return_value_on_exit(False)
74 app.set_title('behold the button below')78 app.set_title('behold the button below')
75 button = ui.MenuButton('press to exit', ui.thunk(exit))79 button = ui.MenuButton('press to exit', ui.thunk(exit))
76 widgets = urwid.ListBox(urwid.SimpleFocusListWalker([button]))80 widgets = urwid.ListBox(urwid.SimpleFocusListWalker([button]))
77 app.set_contents(widgets)81 app.set_contents(widgets)
78 return False
7982
80 pressed = views.show(button_view)83 pressed = views.show(button_view)
8184
82In this example the button_view function configures the App to show a button.85In this example the button_view function configures the App to show a button.
83Clicking that button an AppExit(True) is raised. The view itself instead just86Clicking that button an AppExit(True) is raised. The return value on exit
84returns False. This means that "pressed" will be True if the user exited using87instead is set by the view itself to False. This means that "pressed" will be
85the button, or False if the user exited using the global shortcut.88True if the user exited using the button, or False if the user exited using the
89global shortcut.
8690
87As a final note, it is absolutely safe for a view to call, directly or91As a final note, it is absolutely safe for a view to call, directly or
88indirectly, other views, as long as all the arguments required by the other92indirectly, other views, as long as all the arguments required by the other
@@ -111,16 +115,14 @@
111 The view is called passing an App named tuple and the provided *args.115 The view is called passing an App named tuple and the provided *args.
112116
113 Block until the main loop is stopped, either by the user with the exit117 Block until the main loop is stopped, either by the user with the exit
114 shortcut or by the view itself with the AppExit exception. In the former118 shortcut or by the view itself. In both cases, an ui.AppExit is raised, and
115 case, return what is returned by the view. In the AppExit case, return119 the return value is encapsulated in the exception.
116 the value encapsulated in the exception.
117 """120 """
118 loop, app = base.setup_urwid_app()121 loop, app = base.setup_urwid_app()
119 default_return_value = view(app, *args)
120 unhandled_input = ui.exit_and_return(default_return_value)
121 loop.set_unhandled_input(unhandled_input)
122 # Start the Urwid interactive session (main loop).
123 try:122 try:
123 # Execute the view.
124 view(app, *args)
125 # Start the Urwid interactive session (main loop).
124 loop.run()126 loop.run()
125 except ui.AppExit as err:127 except ui.AppExit as err:
126 return err.return_value128 return err.return_value
@@ -137,6 +139,10 @@
137 - save_callable: a function called to save a new environment database.139 - save_callable: a function called to save a new environment database.
138 """140 """
139 env_db = copy.deepcopy(env_db)141 env_db = copy.deepcopy(env_db)
142 # All the environment views return a tuple (new_env_db, env_data).
143 # Set the env_data to None in the case the user quits the application
144 # without selecting an environment to use.
145 app.set_return_value_on_exit((env_db, None))
140 detail_view = functools.partial(146 detail_view = functools.partial(
141 env_detail, app, env_type_db, env_db, save_callable)147 env_detail, app, env_type_db, env_db, save_callable)
142 # Alphabetically sort the existing environments.148 # Alphabetically sort the existing environments.
@@ -185,7 +191,6 @@
185 if status:191 if status:
186 app.set_status(status)192 app.set_status(status)
187 app.set_contents(contents)193 app.set_contents(contents)
188 return env_db, None
189194
190195
191def env_detail(app, env_type_db, env_db, save_callable, env_data):196def env_detail(app, env_type_db, env_db, save_callable, env_data):
@@ -201,6 +206,10 @@
201 - env_data: the environment data.206 - env_data: the environment data.
202 """207 """
203 env_db = copy.deepcopy(env_db)208 env_db = copy.deepcopy(env_db)
209 # All the environment views return a tuple (new_env_db, env_data).
210 # Set the env_data to None in the case the user quits the application
211 # without selecting an environment to use.
212 app.set_return_value_on_exit((env_db, None))
204 index_view = functools.partial(213 index_view = functools.partial(
205 env_index, app, env_type_db, env_db, save_callable)214 env_index, app, env_type_db, env_db, save_callable)
206215
@@ -250,4 +259,3 @@
250 widgets.append(ui.create_controls(*controls))259 widgets.append(ui.create_controls(*controls))
251 listbox = urwid.ListBox(urwid.SimpleFocusListWalker(widgets))260 listbox = urwid.ListBox(urwid.SimpleFocusListWalker(widgets))
252 app.set_contents(listbox)261 app.set_contents(listbox)
253 return env_db, None
254262
=== added file 'quickstart/tests/cli/helpers.py'
--- quickstart/tests/cli/helpers.py 1970-01-01 00:00:00 +0000
+++ quickstart/tests/cli/helpers.py 2013-12-19 17:41:55 +0000
@@ -0,0 +1,31 @@
1# This file is part of the Juju Quickstart Plugin, which lets users set up a
2# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
3# Copyright (C) 2013 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it under
6# the terms of the GNU Affero General Public License version 3, as published by
7# the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Test helpers for the Juju Quickstart CLI infrastructure."""
18
19from __future__ import unicode_literals
20
21from quickstart.cli import ui
22
23
24class CliAppTestsMixin(object):
25 """Helper methods to test Quickstart CLI applications."""
26
27 def get_on_exit_return_value(self, loop):
28 """Return the value returned by the application when the user quits."""
29 with self.assertRaises(ui.AppExit) as context_manager:
30 loop.unhandled_input(ui.EXIT_KEY)
31 return context_manager.exception.return_value
032
=== modified file 'quickstart/tests/cli/test_base.py'
--- quickstart/tests/cli/test_base.py 2013-12-19 09:12:37 +0000
+++ quickstart/tests/cli/test_base.py 2013-12-19 17:41:55 +0000
@@ -23,6 +23,7 @@
23import urwid23import urwid
2424
25from quickstart.cli import base25from quickstart.cli import base
26from quickstart.tests.cli import helpers as cli_helpers
2627
2728
28class TestMainLoop(unittest.TestCase):29class TestMainLoop(unittest.TestCase):
@@ -57,7 +58,7 @@
57 self.assertEqual(1, sum(times_called))58 self.assertEqual(1, sum(times_called))
5859
5960
60class TestSetupUrwidApp(unittest.TestCase):61class TestSetupUrwidApp(cli_helpers.CliAppTestsMixin, unittest.TestCase):
6162
62 def setUp(self):63 def setUp(self):
63 # Set up the base Urwid application.64 # Set up the base Urwid application.
@@ -164,3 +165,21 @@
164 message_widget = self.get_message_widget(self.loop)165 message_widget = self.get_message_widget(self.loop)
165 message_widget.set_text('42')166 message_widget.set_text('42')
166 self.assertEqual('42', self.app.get_message())167 self.assertEqual('42', self.app.get_message())
168
169 def test_default_unhandled_input(self):
170 # The default unhandled_input function is configured so that a
171 # quickstart.cli.ui.AppExit exception is raised. The exception's
172 # return value is None by default.
173 return_value = self.get_on_exit_return_value(self.loop)
174 self.assertIsNone(return_value)
175
176 def test_set_return_value_on_exit(self):
177 # It is possible to change the value returned by the AppExit exception
178 # when the user quits the application using the exit shortcut.
179 self.app.set_return_value_on_exit(42)
180 return_value = self.get_on_exit_return_value(self.loop)
181 self.assertEqual(42, return_value)
182 # The value can be changed multiple times.
183 self.app.set_return_value_on_exit([47, None])
184 return_value = self.get_on_exit_return_value(self.loop)
185 self.assertEqual([47, None], return_value)
167186
=== modified file 'quickstart/tests/cli/test_views.py'
--- quickstart/tests/cli/test_views.py 2013-12-19 09:30:08 +0000
+++ quickstart/tests/cli/test_views.py 2013-12-19 17:41:55 +0000
@@ -31,6 +31,7 @@
31)31)
32from quickstart.models import envs32from quickstart.models import envs
33from quickstart.tests import helpers33from quickstart.tests import helpers
34from quickstart.tests.cli import helpers as cli_helpers
3435
3536
36class TestShow(unittest.TestCase):37class TestShow(unittest.TestCase):
@@ -70,17 +71,6 @@
70 return_value = views.show(view)71 return_value = views.show(view)
71 self.assertEqual('bad wolf', return_value)72 self.assertEqual('bad wolf', return_value)
7273
73 def test_unhandled_input(self):
74 # The unhandled_input callable is properly set up and registered to
75 # the main loop.
76 view = mock.Mock(return_value=42)
77 with self.patch_setup_urwid_app() as (mock_loop, mock_app):
78 views.show(view)
79 unhandled_input = mock_loop.set_unhandled_input.call_args[0][0]
80 with self.assertRaises(ui.AppExit) as context_manager:
81 unhandled_input(ui.EXIT_KEY)
82 self.assertEqual(42, context_manager.exception.return_value)
83
84 def test_view_arguments(self):74 def test_view_arguments(self):
85 # The view is invoked passing the app and all the optional show75 # The view is invoked passing the app and all the optional show
86 # function arguments.76 # function arguments.
@@ -90,7 +80,7 @@
90 view.assert_called_once_with(mock_app, 'arg1', 'arg2')80 view.assert_called_once_with(mock_app, 'arg1', 'arg2')
9181
9282
93class EnvViewTestsMixin(object):83class EnvViewTestsMixin(cli_helpers.CliAppTestsMixin):
94 """Shared helpers for testing environment views."""84 """Shared helpers for testing environment views."""
9585
96 env_type_db = envs.get_env_type_db()86 env_type_db = envs.get_env_type_db()
@@ -145,12 +135,13 @@
145135
146class TestEnvIndex(EnvViewTestsMixin, unittest.TestCase):136class TestEnvIndex(EnvViewTestsMixin, unittest.TestCase):
147137
148 def test_view_return_value(self):138 def test_view_default_return_value_on_exit(self):
149 # The view returns a tuple including a copy of the given env_db and139 # The view configures the app so that the return value on user exit is
150 # None, the latter meaning no environment has been selected.140 # a tuple including a copy of the given env_db and None, the latter
141 # meaning no environment has been selected.
151 env_db = helpers.make_env_db()142 env_db = helpers.make_env_db()
152 new_env_db, env_data = views.env_index(143 views.env_index(self.app, self.env_type_db, env_db, self.save_callable)
153 self.app, self.env_type_db, env_db, self.save_callable)144 new_env_db, env_data = self.get_on_exit_return_value(self.loop)
154 self.assertEqual(env_db, new_env_db)145 self.assertEqual(env_db, new_env_db)
155 self.assertIsNot(env_db, new_env_db)146 self.assertIsNot(env_db, new_env_db)
156 self.assertIsNone(env_data)147 self.assertIsNone(env_data)
@@ -249,10 +240,12 @@
249 self.app, self.env_type_db, self.env_db, self.save_callable,240 self.app, self.env_type_db, self.env_db, self.save_callable,
250 self.env_data)241 self.env_data)
251242
252 def test_view_return_value(self):243 def test_view_default_return_value_on_exit(self):
253 # The view returns a tuple including a copy of the given env_db and244 # The view configures the app so that the return value on user exit is
254 # None, the latter meaning no environment has been selected (for now).245 # a tuple including a copy of the given env_db and None, the latter
255 new_env_db, env_data = self.call_view()246 # meaning no environment has been selected (for now).
247 self.call_view()
248 new_env_db, env_data = self.get_on_exit_return_value(self.loop)
256 self.assertEqual(self.env_db, new_env_db)249 self.assertEqual(self.env_db, new_env_db)
257 self.assertIsNot(self.env_db, new_env_db)250 self.assertIsNot(self.env_db, new_env_db)
258 self.assertIsNone(env_data)251 self.assertIsNone(env_data)

Subscribers

People subscribed via source and target branches