Merge lp:~frankban/juju-quickstart/env-manage-base-views into lp:juju-quickstart
- env-manage-base-views
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 35 |
Proposed branch: | lp:~frankban/juju-quickstart/env-manage-base-views |
Merge into: | lp:juju-quickstart |
Diff against target: |
1025 lines (+930/-1) 13 files modified
cli-app-demo.py (+52/-0) quickstart/cli/__init__.py (+33/-0) quickstart/cli/base.py (+155/-0) quickstart/cli/ui.py (+118/-0) quickstart/cli/views.py (+119/-0) quickstart/models/envs.py (+2/-1) quickstart/tests/cli/__init__.py (+15/-0) quickstart/tests/cli/test_base.py (+174/-0) quickstart/tests/cli/test_ui.py (+117/-0) quickstart/tests/cli/test_views.py (+86/-0) quickstart/tests/test_utils.py (+25/-0) quickstart/utils.py (+33/-0) requirements.pip (+1/-0) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/env-manage-base-views |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email:
|
Commit message
Description of the change
Quickstart base structure for views.
This branch implements a way to start
Urwid interactive sessions in quickstart.
This infrastructure will be used in
future branches to implement the
quickstart environment management system.
My apologies for the long diff, but you
can safely ignore all the file headers,
and the code includes long module
docstrings which can help reviewing the
branch.
Tests: `make check`.
QA:
run `make` and then `./cli-
this will start a demo application using
the views infrastructure.
Thank you!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
Ran out of time, but I think this looks great. Count me as an LGTM
'cause I know bac will do a great review. :-)
https:/
File quickstart/
https:/
quickstart/
application.
Might be nice to describe the columns. I assume they are class name (or
None for default), text style, background style.
https:/
quickstart/
Huh. So...this effectively produces a thunk that discards/ignores the
argument. You could call it that, I guess. This functionality is
really generic. If you didn't need to discard the argument, you could
just use functools.partial.
def make_aggressive
def thunk(*ignored, **ignored_kwargs):
return function(*args, **kwargs)
return thunk
<shrug> :-)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Brad Crittenden (bac) wrote : | # |
LGTM and QA-OK. Some changes suggested below but nothing substantial.
Thanks for the interesting work.
https:/
File quickstart/
https:/
quickstart/
Quickstart CLI application
change 'building' -> 'in building' or 'build'
https:/
quickstart/
so that code and tests
Improves
https:/
quickstart/
In the docstring you call the param a generic 'widget'. They should
match. If not generic then should you validate that the param is a
listbox? I think using a generic widget is better.
https:/
quickstart/
original_widget seems like an odd name since it is really just the
current widget.
https:/
File quickstart/
https:/
quickstart/
by pressing
https:/
File quickstart/utils.py (right):
https:/
quickstart/
http://
Does the use here complicate our copyright statement above? You've
given attribution but it looks like Canonical is claiming copyright to
it.
Perhaps we include at the top the copyright statement from tornado with
the reference to ObjectDict.
# The ObjectDict code:
#
# Copyright 2009 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you
may
# not use this file except in compliance with the License. You may
obtain
# a copy of the License at
#
# http://
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
the
# License for the specific language governing permissions and
limitations
# under the License.
- 55. By Francesco Banconi
-
Changes as per review.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Quickstart base structure for views.
This branch implements a way to start
Urwid interactive sessions in quickstart.
This infrastructure will be used in
future branches to implement the
quickstart environment management system.
My apologies for the long diff, but you
can safely ignore all the file headers,
and the code includes long module
docstrings which can help reviewing the
branch.
Tests: `make check`.
QA:
run `make` and then `./cli-
this will start a demo application using
the views infrastructure.
Thank you!
R=gary.poster, bac
CC=
https:/
https:/
File quickstart/
https:/
quickstart/
Quickstart CLI application
On 2013/12/17 14:30:47, bac wrote:
> change 'building' -> 'in building' or 'build'
Done.
https:/
quickstart/
so that code and tests
On 2013/12/17 14:30:47, bac wrote:
> Improves
Done.
https:/
quickstart/
On 2013/12/17 14:30:47, bac wrote:
> In the docstring you call the param a generic 'widget'. They should
match. If
> not generic then should you validate that the param is a listbox? I
think using
> a generic widget is better.
Done.
https:/
quickstart/
On 2013/12/17 14:30:47, bac wrote:
> original_widget seems like an odd name since it is really just the
current
> widget.
Unfortunately this is how the wrapped widget is named in Urwid.
https:/
File quickstart/
https:/
quickstart/
application.
On 2013/12/17 14:00:43, gary.poster wrote:
> Might be nice to describe the columns. I assume they are class name
(or None
> for default), text style, background style.
Done.
https:/
quickstart/
On 2013/12/17 14:00:43, gary.poster wrote:
> Huh. So...this effectively produces a thunk that discards/ignores the
argument.
> You could call it that, I guess. This functionality is really
generic. If you
> didn't need to discard the argument, you could just use
functools.partial.
> def make_aggressive
> def thunk(*ignored, **ignored_kwargs):
> return function(*args, **kwargs)
> return thunk
> <shrug> :-)
Renamed to thunk.
https:/
File quickstart/
https:/
quickstart/...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
Thank you both!
Preview Diff
1 | === added file 'cli-app-demo.py' |
2 | --- cli-app-demo.py 1970-01-01 00:00:00 +0000 |
3 | +++ cli-app-demo.py 2013-12-17 15:23:53 +0000 |
4 | @@ -0,0 +1,52 @@ |
5 | +#!.venv/bin/python |
6 | + |
7 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
8 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
9 | +# Copyright (C) 2013 Canonical Ltd. |
10 | +# |
11 | +# This program is free software: you can redistribute it and/or modify it under |
12 | +# the terms of the GNU Affero General Public License version 3, as published by |
13 | +# the Free Software Foundation. |
14 | +# |
15 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
16 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
17 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
18 | +# Affero General Public License for more details. |
19 | +# |
20 | +# You should have received a copy of the GNU Affero General Public License |
21 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
22 | + |
23 | +"""Juju Quickstart CLI application demo.""" |
24 | + |
25 | +# XXX frankban (2013-12-16): this file is only for demonstration/QA purposes. |
26 | +# Remove this file when the environment management integration is completed. |
27 | +# Run "make" before running this file. |
28 | + |
29 | +from __future__ import ( |
30 | + print_function, |
31 | + unicode_literals, |
32 | +) |
33 | + |
34 | +import urwid |
35 | + |
36 | +from quickstart.cli import views |
37 | + |
38 | + |
39 | +def example_view(app, message): |
40 | + """An example Quickstart view.""" |
41 | + app.set_title('This is the title') |
42 | + widgets = [ |
43 | + urwid.Text('These are the app contents. Press ^X to exit.'), |
44 | + urwid.Divider(), |
45 | + urwid.Text('Message: {}'.format(message)), |
46 | + ] |
47 | + contents = urwid.ListBox(urwid.SimpleFocusListWalker(widgets)) |
48 | + app.set_contents(contents) |
49 | + app.set_status(('status error', '\u2622 Look! A toxic status! \u2622')) |
50 | + app.set_message('this notification will disappear in three seconds') |
51 | + return 'Goodbye, world' |
52 | + |
53 | + |
54 | +if __name__ == '__main__': |
55 | + return_value = views.show(example_view, 'Hello, world') |
56 | + print(return_value) |
57 | |
58 | === added directory 'quickstart/cli' |
59 | === added file 'quickstart/cli/__init__.py' |
60 | --- quickstart/cli/__init__.py 1970-01-01 00:00:00 +0000 |
61 | +++ quickstart/cli/__init__.py 2013-12-17 15:23:53 +0000 |
62 | @@ -0,0 +1,33 @@ |
63 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
64 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
65 | +# Copyright (C) 2013 Canonical Ltd. |
66 | +# |
67 | +# This program is free software: you can redistribute it and/or modify it under |
68 | +# the terms of the GNU Affero General Public License version 3, as published by |
69 | +# the Free Software Foundation. |
70 | +# |
71 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
72 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
73 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
74 | +# Affero General Public License for more details. |
75 | +# |
76 | +# You should have received a copy of the GNU Affero General Public License |
77 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
78 | + |
79 | +"""Juju Quickstart command line interface management. |
80 | + |
81 | +The functions and objects included in this package can be used to build |
82 | +rich command line interfaces using the Urwid console interface library |
83 | +(see <http://excess.org/urwid/>). |
84 | + |
85 | +This package is organized in several modules: |
86 | + - base: the base pieces used to set up Urwid applications; |
87 | + - views: view functions responsible for showing specific contents in the |
88 | + context of a Urwid app; |
89 | + - ui: Urwid related utility objects, including callback wrappers, |
90 | + customized widgets and style specific helpers. |
91 | + |
92 | +Client code usually starts a Quickstart terminal application calling |
93 | +views.show() with the view to display along with the view required arguments. |
94 | +See the quickstart.cli.views module docstring for further details. |
95 | +""" |
96 | |
97 | === added file 'quickstart/cli/base.py' |
98 | --- quickstart/cli/base.py 1970-01-01 00:00:00 +0000 |
99 | +++ quickstart/cli/base.py 2013-12-17 15:23:53 +0000 |
100 | @@ -0,0 +1,155 @@ |
101 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
102 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
103 | +# Copyright (C) 2013 Canonical Ltd. |
104 | +# |
105 | +# This program is free software: you can redistribute it and/or modify it under |
106 | +# the terms of the GNU Affero General Public License version 3, as published by |
107 | +# the Free Software Foundation. |
108 | +# |
109 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
110 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
111 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
112 | +# Affero General Public License for more details. |
113 | +# |
114 | +# You should have received a copy of the GNU Affero General Public License |
115 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
116 | + |
117 | +"""Juju Quickstart Urwid application base handling. |
118 | + |
119 | +A collection of objects which help in building a Quickstart CLI application |
120 | +skeleton. Views use these functions to set up the Urwid top widget and to start |
121 | +the application main loop. |
122 | + |
123 | +See the quickstart.cli.views module docstring for further details. |
124 | +""" |
125 | + |
126 | +from __future__ import unicode_literals |
127 | + |
128 | +import urwid |
129 | + |
130 | +from quickstart import ( |
131 | + get_version, |
132 | + utils, |
133 | +) |
134 | +from quickstart.cli import ui |
135 | + |
136 | + |
137 | +class _MainLoop(urwid.MainLoop): |
138 | + """A customized Urwid loop. |
139 | + |
140 | + Allow for setting the unhandled_input callable after the loop |
141 | + initialization. |
142 | + """ |
143 | + |
144 | + def set_unhandled_input(self, unhandled_input): |
145 | + """Set the unhandled_input callable. |
146 | + |
147 | + The passed unhandled_input is a callable called when input is not |
148 | + handled by the application top widget. |
149 | + """ |
150 | + self._unhandled_input = unhandled_input |
151 | + |
152 | + def get_alarms(self): |
153 | + """Return all the alarms set for this loop. |
154 | + |
155 | + Improves the level of event loop introspection so that code and tests |
156 | + can easily access the alarms list. |
157 | + |
158 | + The alarms list is a sequence of (time, callback) tuples. |
159 | + """ |
160 | + return self.event_loop._alarms |
161 | + |
162 | + |
163 | +def setup_urwid_app(): |
164 | + """Configure a Urwid application suitable for being used by views. |
165 | + |
166 | + Build the Urwid top widget and instantiate a main loop. The top widget |
167 | + is basically a frame, composed by a header, some contents, and a footer. |
168 | + This application skeleton is Quickstart branded, and exposes functions |
169 | + that can be used by views to change the contents of the frame. |
170 | + |
171 | + Return a tuple (loop, app) where loop is the interactive session main loop |
172 | + (ready to be started invoking loop.run()) and app is an ObjectDict exposing |
173 | + an API to be used by views to customize the application. |
174 | + |
175 | + The API exposed by app is limited by design, and includes: |
176 | + |
177 | + - set_title(text): set/change the title displayed in the application |
178 | + header (e.g.: app.set_title('my title')); |
179 | + - get_title(): return the current application title; |
180 | + |
181 | + - set_contents(widget): set/change the application body contents. A |
182 | + Urwid ListBox widget instance is usually provided, which replaces the |
183 | + current application contents; |
184 | + - get_contents(): return the current application contents widget; |
185 | + |
186 | + - set_status(text): set/change the status text displayed in the |
187 | + application footer (e.g.: set_status('press play on tape')). The |
188 | + status message can also be passed as a (style, text) tuple, as usual |
189 | + in Urwid code, e.g.: app.set_status(('error', 'error message')); |
190 | + - get_status(): return the current status message; |
191 | + |
192 | + - set_message(text): set/change a notification message, which is |
193 | + displayed in the footer for a couple of seconds before disappearing; |
194 | + - get_message(): return the message currently displayed in the |
195 | + notifications area. |
196 | + """ |
197 | + # Set up the application header. |
198 | + title = urwid.Text('\npreparing...') |
199 | + header_line = urwid.Divider('\N{LOWER ONE QUARTER BLOCK}') |
200 | + header = urwid.Pile([ |
201 | + urwid.AttrMap(ui.padding(title), 'header'), |
202 | + urwid.AttrMap(header_line, 'line header'), |
203 | + urwid.Divider(), |
204 | + ]) |
205 | + # Set up the application default contents. |
206 | + # View code is assumed to replace the placeholder widget using |
207 | + # app.set_contents(widget). |
208 | + placeholder = urwid.ListBox(urwid.SimpleFocusListWalker([])) |
209 | + contents = ui.padding(urwid.AttrMap(placeholder, None)) |
210 | + |
211 | + def set_contents(widget): |
212 | + contents.original_widget = widget |
213 | + |
214 | + # Set up the application footer. |
215 | + # The CTRL-x shortcut is automatically set up by views.show(). |
216 | + brand_message = 'juju-quickstart v{} - ^X exit '.format(get_version()) |
217 | + brand = urwid.Text(brand_message) |
218 | + status = urwid.Text('') |
219 | + message = urwid.Text('', align='right') |
220 | + footer_line = urwid.Divider('\N{UPPER ONE EIGHTH BLOCK}') |
221 | + status_columns = urwid.Columns([('pack', brand), status, message]) |
222 | + footer = urwid.Pile([ |
223 | + urwid.Divider(), |
224 | + urwid.AttrMap(ui.padding(status_columns), 'footer'), |
225 | + urwid.AttrMap(footer_line, 'line footer'), |
226 | + ]) |
227 | + # Compose the components in a frame, and set up the top widget. The top |
228 | + # widget is the topmost widget used for painting the screen. |
229 | + page = urwid.Frame(contents, header=header, footer=footer) |
230 | + top_widget = urwid.Overlay( |
231 | + page, urwid.SolidFill('\N{MEDIUM SHADE}'), |
232 | + align='center', width=('relative', 90), |
233 | + valign='middle', height=('relative', 90), |
234 | + min_width=78, min_height=20) |
235 | + # Instantiate the Urwid main loop. |
236 | + loop = _MainLoop(top_widget, palette=ui.PALETTE) |
237 | + |
238 | + def set_message(msg): |
239 | + message.set_text(('message', msg)) |
240 | + loop.set_alarm_in(3, lambda *args: message.set_text('')) |
241 | + |
242 | + # Create the app ObjectDict. If in the future we will find a view to |
243 | + # require more capabilities/API access than the current one, this is the |
244 | + # place where to add new API functions. |
245 | + app = utils.ObjectDict( |
246 | + set_title=lambda msg: title.set_text('\n{}'.format(msg)), |
247 | + get_title=lambda: title.text.lstrip(), |
248 | + set_contents=set_contents, |
249 | + get_contents=lambda: contents.original_widget, |
250 | + set_status=lambda msg: status.set_text(msg), |
251 | + get_status=lambda: status.text, |
252 | + set_message=set_message, |
253 | + get_message=lambda: message.text, |
254 | + ) |
255 | + return loop, app |
256 | |
257 | === added file 'quickstart/cli/ui.py' |
258 | --- quickstart/cli/ui.py 1970-01-01 00:00:00 +0000 |
259 | +++ quickstart/cli/ui.py 2013-12-17 15:23:53 +0000 |
260 | @@ -0,0 +1,118 @@ |
261 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
262 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
263 | +# Copyright (C) 2013 Canonical Ltd. |
264 | +# |
265 | +# This program is free software: you can redistribute it and/or modify it under |
266 | +# the terms of the GNU Affero General Public License version 3, as published by |
267 | +# the Free Software Foundation. |
268 | +# |
269 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
270 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
271 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
272 | +# Affero General Public License for more details. |
273 | +# |
274 | +# You should have received a copy of the GNU Affero General Public License |
275 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
276 | + |
277 | +"""Juju Quickstart Urwid related utility objects.""" |
278 | + |
279 | +from __future__ import unicode_literals |
280 | + |
281 | +import functools |
282 | + |
283 | +import urwid |
284 | + |
285 | + |
286 | +# Define the shortcut used to quit the interactive session. |
287 | +EXIT_KEY = 'ctrl x' |
288 | +# Define the color palette used by the Urwid application. |
289 | +PALETTE = [ |
290 | + # Class name, foreground color, background color. |
291 | + # See <http://excess.org/urwid/docs/reference/constants.html |
292 | + # foreground-and-background-colors>. |
293 | + (None, 'light gray', 'black'), |
294 | + ('controls', 'dark gray', 'light gray'), |
295 | + ('edit', 'white,underline', 'black'), |
296 | + ('error', 'light red', 'black'), |
297 | + ('status error', 'light red', 'light gray'), |
298 | + ('footer', 'black', 'light gray'), |
299 | + ('message', 'white', 'dark green'), |
300 | + ('header', 'white', 'dark magenta'), |
301 | + ('highlight', 'white', 'black'), |
302 | + ('line header', 'dark gray', 'dark magenta'), |
303 | + ('line footer', 'light gray', 'light gray'), |
304 | + ('selected', 'white', 'dark blue'), |
305 | +] |
306 | +# Define a default padding for the Urwid application. |
307 | +padding = functools.partial(urwid.Padding, left=2, right=2) |
308 | + |
309 | + |
310 | +class AppExit(Exception): |
311 | + """Used by views to stop the interactive execution returning a value.""" |
312 | + |
313 | + def __init__(self, return_value=None): |
314 | + """Set the value to return to the view caller (default is None).""" |
315 | + self.return_value = return_value |
316 | + |
317 | + def __str__(self): |
318 | + return b'{}: {!r}'.format(self.__class__.__name__, self.return_value) |
319 | + |
320 | + |
321 | +def exit_and_return(return_value): |
322 | + """Return a function that can be used as unhandled_input for an Urwid app. |
323 | + |
324 | + The resulting function terminates the interactive session with the given |
325 | + return_value when the user hits CTRL-x. |
326 | + """ |
327 | + def unhandled_input(key): |
328 | + if key == EXIT_KEY: |
329 | + raise AppExit(return_value) |
330 | + return unhandled_input |
331 | + |
332 | + |
333 | +def create_controls(*args): |
334 | + """Create a row of control widgets surrounded by line boxes.""" |
335 | + controls = urwid.Columns([padding(urwid.LineBox(arg)) for arg in args]) |
336 | + return urwid.Pile([ |
337 | + urwid.Divider(top=1, bottom=1), |
338 | + urwid.AttrMap(controls, 'controls') |
339 | + ]) |
340 | + |
341 | + |
342 | +class MenuButton(urwid.Button): |
343 | + """A customized Urwid button widget. |
344 | + |
345 | + This behaves like a regular button, but also takes a callback that is |
346 | + called when the button is clicked. |
347 | + """ |
348 | + |
349 | + def __init__(self, caption, callback): |
350 | + super(MenuButton, self).__init__('') |
351 | + urwid.connect_signal(self, 'click', callback) |
352 | + icon = urwid.SelectableIcon(caption, 0) |
353 | + # Replace the original widget: it seems ugly but it is Urwid idiomatic. |
354 | + self._w = urwid.AttrMap(icon, None, 'selected') |
355 | + |
356 | + |
357 | +def thunk(function, *args, **kwargs): |
358 | + """Create and return a callable binding the given method and args/kwargs. |
359 | + |
360 | + This is useful when the given function is used as a signal subscriber, e.g. |
361 | + as a callback to be called when an Urwid signal is sent. In most cases, the |
362 | + widget which generated the event is sent as first argument to the callback. |
363 | + Moreover, urwid.connect_signal handles only one user argument. |
364 | + See <http://excess.org/urwid/docs/reference/signals.html>. |
365 | + |
366 | + This function helps when the callback does not require the original widget |
367 | + and/or when it instead requires more than one argument, e.g.: |
368 | + |
369 | + def save(contents, commit=False): |
370 | + ... |
371 | + |
372 | + button = MenuButton('save and commit', ui.thunk(save, contents, True)) |
373 | + |
374 | + This example uses the MenuButton widget defined above in this module. |
375 | + """ |
376 | + def callback(*ignored_args, **ignored_kwargs): |
377 | + return function(*args, **kwargs) |
378 | + return callback |
379 | |
380 | === added file 'quickstart/cli/views.py' |
381 | --- quickstart/cli/views.py 1970-01-01 00:00:00 +0000 |
382 | +++ quickstart/cli/views.py 2013-12-17 15:23:53 +0000 |
383 | @@ -0,0 +1,119 @@ |
384 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
385 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
386 | +# Copyright (C) 2013 Canonical Ltd. |
387 | +# |
388 | +# This program is free software: you can redistribute it and/or modify it under |
389 | +# the terms of the GNU Affero General Public License version 3, as published by |
390 | +# the Free Software Foundation. |
391 | +# |
392 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
393 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
394 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
395 | +# Affero General Public License for more details. |
396 | +# |
397 | +# You should have received a copy of the GNU Affero General Public License |
398 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
399 | + |
400 | +"""Juju Quickstart CLI application views. |
401 | + |
402 | +This module contains the Quickstart view implementations along with a function |
403 | +(show) to easily start a view automatically creating an Urwid application. |
404 | + |
405 | +To start a Quickstart interactive session, just run the following: |
406 | + |
407 | + show(view, *args) |
408 | + |
409 | +The code above sets up a Quickstart branded CLI application, then calls the |
410 | +given view callable passing the application object ready to be configured |
411 | +and all the given optional arguments. Finally the interactive session is |
412 | +started, and the show function blocks until the user or the view itself |
413 | +request to exit the application. |
414 | + |
415 | +A view is a callable receiving an app ObjectDict and other optional arguments |
416 | +(based on specific view needs). A view function can configure the Urwid |
417 | +application using the API exposed by the application object |
418 | +(see quickstart.cli.base.setup_urwid_app). |
419 | + |
420 | +Assume a view is defined like the following: |
421 | + |
422 | + def myview(app, title): |
423 | + app.set_title(title) |
424 | + return 42 |
425 | + |
426 | +The view above, requiring a title argument, can be started this way: |
427 | + |
428 | + show(myview, 'this title will be shown in the header') |
429 | + |
430 | +At this point the application main loop is started, and the user can interact |
431 | +with the CLI interface. There are two ways to stop the interactive session: |
432 | + 1) the user explicitly requests to exit. The Urwid application is |
433 | + automatically configured to allow the user to quit whenever she wants by |
434 | + pressing a keyboard shortcut; |
435 | + 2) a view decides it is time to quit (e.g. reacting to an event/input). |
436 | + |
437 | +In both cases, the show function returns something to the caller: |
438 | + 1) when the user explicitly requests to quit, the value returned by the |
439 | + view itself (the callable passed to show()) is returned. For instance, |
440 | + the show(myview...) call above would return 42; |
441 | + 2) to force the end of the interactive session, a view can raise a |
442 | + quickstart.cli.ui.AppExit exception, passing a return value: if the |
443 | + application is exited this way, then show() returns the value |
444 | + encapsulated in the exception. Note that this exception can be raised |
445 | + as a reaction to an event, and not in the first execution of the view |
446 | + body, i.e. during the app configuration. |
447 | + |
448 | +The above is better described by code: |
449 | + |
450 | + from quickstart.cli import views, ui |
451 | + |
452 | + def button_view(app): |
453 | + |
454 | + def exit(): |
455 | + raise ui.AppExit(True) |
456 | + |
457 | + app.set_title('behold the button below') |
458 | + button = ui.MenuButton('press to exit', ui.thunk(exit)) |
459 | + widgets = urwid.ListBox(urwid.SimpleFocusListWalker([button])) |
460 | + app.set_contents(widgets) |
461 | + return False |
462 | + |
463 | + pressed = views.show(button_view) |
464 | + |
465 | +In this example the button_view function configures the app to show a button. |
466 | +Clicking that button an AppExit(True) is raised. The view itself instead just |
467 | +returns False. This means that "pressed" will be True if the user exited using |
468 | +the button, or False if the user exited using the global shortcut. |
469 | + |
470 | +As a final note, it is absolutely safe for a view to call, directly or |
471 | +indirectly, other views, as long as all the arguments required by the other |
472 | +views, including app, are properly provided. This is effectively the proposed |
473 | +solution to build multi-views CLI applications in Quickstart. |
474 | +""" |
475 | + |
476 | +from __future__ import unicode_literals |
477 | + |
478 | +from quickstart.cli import ( |
479 | + base, |
480 | + ui, |
481 | +) |
482 | + |
483 | + |
484 | +def show(view, *args): |
485 | + """Start an Urwid interactive session showing the given view. |
486 | + |
487 | + The view is called passing an app ObjectDict and the provided *args. |
488 | + |
489 | + Block until the main loop is stopped, either by the user with the exit |
490 | + shortcut or by the view itself with the AppExit exception. In the former |
491 | + case, return what is returned by the view. In the AppExit case, return |
492 | + the value encapsulated in the exception. |
493 | + """ |
494 | + loop, app = base.setup_urwid_app() |
495 | + default_return_value = view(app, *args) |
496 | + unhandled_input = ui.exit_and_return(default_return_value) |
497 | + loop.set_unhandled_input(unhandled_input) |
498 | + # Start the Urwid interactive session (main loop). |
499 | + try: |
500 | + loop.run() |
501 | + except ui.AppExit as err: |
502 | + return err.return_value |
503 | |
504 | === modified file 'quickstart/models/envs.py' |
505 | --- quickstart/models/envs.py 2013-12-16 08:21:57 +0000 |
506 | +++ quickstart/models/envs.py 2013-12-17 15:23:53 +0000 |
507 | @@ -321,7 +321,8 @@ |
508 | label='default series', required=False, |
509 | help='the default Ubuntu series to use for the bootstrap node') |
510 | is_default_field = fields.BoolField( |
511 | - 'is-default', help='make this the default environment', required=True) |
512 | + 'is-default', label='default', allow_mixed=False, required=True, |
513 | + help='make this the default environment') |
514 | # Define data structures used as part of the metadata below. |
515 | ec2_regions = ( |
516 | 'ap-northeast-1', 'ap-southeast-1', 'ap-southeast-2', |
517 | |
518 | === added directory 'quickstart/tests/cli' |
519 | === added file 'quickstart/tests/cli/__init__.py' |
520 | --- quickstart/tests/cli/__init__.py 1970-01-01 00:00:00 +0000 |
521 | +++ quickstart/tests/cli/__init__.py 2013-12-17 15:23:53 +0000 |
522 | @@ -0,0 +1,15 @@ |
523 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
524 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
525 | +# Copyright (C) 2013 Canonical Ltd. |
526 | +# |
527 | +# This program is free software: you can redistribute it and/or modify it under |
528 | +# the terms of the GNU Affero General Public License version 3, as published by |
529 | +# the Free Software Foundation. |
530 | +# |
531 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
532 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
533 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
534 | +# Affero General Public License for more details. |
535 | +# |
536 | +# You should have received a copy of the GNU Affero General Public License |
537 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
538 | |
539 | === added file 'quickstart/tests/cli/test_base.py' |
540 | --- quickstart/tests/cli/test_base.py 1970-01-01 00:00:00 +0000 |
541 | +++ quickstart/tests/cli/test_base.py 2013-12-17 15:23:53 +0000 |
542 | @@ -0,0 +1,174 @@ |
543 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
544 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
545 | +# Copyright (C) 2013 Canonical Ltd. |
546 | +# |
547 | +# This program is free software: you can redistribute it and/or modify it under |
548 | +# the terms of the GNU Affero General Public License version 3, as published by |
549 | +# the Free Software Foundation. |
550 | +# |
551 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
552 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
553 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
554 | +# Affero General Public License for more details. |
555 | +# |
556 | +# You should have received a copy of the GNU Affero General Public License |
557 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
558 | + |
559 | +"""Tests for the Juju Quickstart Urwid application base handling.""" |
560 | + |
561 | +from __future__ import unicode_literals |
562 | + |
563 | +import unittest |
564 | + |
565 | +import urwid |
566 | + |
567 | +from quickstart import utils |
568 | +from quickstart.cli import base |
569 | + |
570 | + |
571 | +class TestMainLoop(unittest.TestCase): |
572 | + |
573 | + def setUp(self): |
574 | + # Create a loop instance. |
575 | + self.widget = urwid.ListBox(urwid.SimpleFocusListWalker([])) |
576 | + self.loop = base._MainLoop(self.widget) |
577 | + |
578 | + def test_initialization(self): |
579 | + # The customized loop is properly initialized, and it is an instance |
580 | + # of the Urwid loop. |
581 | + self.assertEqual(self.widget, self.loop.widget) |
582 | + self.assertIsInstance(self.loop, urwid.MainLoop) |
583 | + |
584 | + def test_unhandled_input(self): |
585 | + # The unhandled_input function can be set after the initialization. |
586 | + inputs = [] |
587 | + self.loop.set_unhandled_input(inputs.append) |
588 | + self.loop.unhandled_input('ctrl z') |
589 | + self.assertEqual(['ctrl z'], inputs) |
590 | + |
591 | + def test_alarms(self): |
592 | + # It is possible to retrieve the list of event loop alarms. |
593 | + times_called = [] |
594 | + self.assertEqual(0, len(self.loop.get_alarms())) |
595 | + callback = lambda *args: times_called.append(1) |
596 | + self.loop.set_alarm_in(3, callback) |
597 | + alarms = self.loop.get_alarms() |
598 | + self.assertEqual(1, len(alarms)) |
599 | + alarms[0][1]() |
600 | + self.assertEqual(1, sum(times_called)) |
601 | + |
602 | + |
603 | +class TestSetupUrwidApp(unittest.TestCase): |
604 | + |
605 | + def setUp(self): |
606 | + # Set up the base Urwid application. |
607 | + self.loop, self.app = base.setup_urwid_app() |
608 | + |
609 | + def get_title_widget(self, loop): |
610 | + """Return the title widget given the application main loop.""" |
611 | + # The frame is the main overlay's top widget. |
612 | + frame = loop.widget.top_w |
613 | + # Retrieve the header. |
614 | + header = frame.contents['header'][0] |
615 | + # The title widget is the first in the header pile. |
616 | + return header.contents[0][0].base_widget |
617 | + |
618 | + def get_contents_widget(self, loop): |
619 | + """Return the contents widget given the application main loop.""" |
620 | + # The frame is the main overlay's top widget. |
621 | + frame = loop.widget.top_w |
622 | + # Retrieve the body. |
623 | + body = frame.contents['body'][0] |
624 | + # The contents widget is the body's original widget. |
625 | + return body.original_widget |
626 | + |
627 | + def _get_footer_columns(self, loop): |
628 | + # The frame is the main overlay's top widget. |
629 | + frame = loop.widget.top_w |
630 | + # Retrieve the footer. |
631 | + footer = frame.contents['footer'][0] |
632 | + # Return the columns widget. |
633 | + return footer.contents[1][0].base_widget |
634 | + |
635 | + def get_status_widget(self, loop): |
636 | + """Return the status widget given the application main loop.""" |
637 | + columns = self._get_footer_columns(loop) |
638 | + # The status widget is the second one (brand, status, message). |
639 | + return columns.contents[1][0] |
640 | + |
641 | + def get_message_widget(self, loop): |
642 | + """Return the message widget given the application main loop.""" |
643 | + columns = self._get_footer_columns(loop) |
644 | + # The message widget is the third one (brand, status, message). |
645 | + return columns.contents[2][0] |
646 | + |
647 | + def test_loop(self): |
648 | + # The returned loop is an instance of the base customized loop. |
649 | + self.assertIsInstance(self.loop, base._MainLoop) |
650 | + |
651 | + def test_app(self): |
652 | + # The returned app is an ObjectDict including the expected keys. |
653 | + expected_keys = set([ |
654 | + 'set_title', 'get_title', |
655 | + 'set_contents', 'get_contents', |
656 | + 'set_status', 'get_status', |
657 | + 'set_message', 'get_message', |
658 | + ]) |
659 | + self.assertIsInstance(self.app, utils.ObjectDict) |
660 | + self.assertEqual(expected_keys, set(self.app.keys())) |
661 | + |
662 | + def test_set_title(self): |
663 | + # The set_title API sets the application title. |
664 | + self.app.set_title('The Inner Light') |
665 | + title_widget = self.get_title_widget(self.loop) |
666 | + self.assertEqual('\nThe Inner Light', title_widget.text) |
667 | + |
668 | + def test_get_title(self): |
669 | + # The get_title API retrieves the application title. |
670 | + title_widget = self.get_title_widget(self.loop) |
671 | + title_widget.set_text('The Outer Space') |
672 | + self.assertEqual('The Outer Space', self.app.get_title()) |
673 | + |
674 | + def test_set_contents(self): |
675 | + # The set_contents API changes the application main contents widget. |
676 | + text_widget = urwid.Text('my contents') |
677 | + self.app.set_contents(text_widget) |
678 | + contents_widget = self.get_contents_widget(self.loop) |
679 | + self.assertEqual('my contents', contents_widget.text) |
680 | + |
681 | + def test_get_contents(self): |
682 | + # The get_contents API returns the contents widget. |
683 | + contents_widget = self.get_contents_widget(self.loop) |
684 | + self.assertEqual(contents_widget, self.app.get_contents()) |
685 | + |
686 | + def test_set_status(self): |
687 | + # The set_status API sets the status message displayed in the footer. |
688 | + self.app.set_status('press play on tape') |
689 | + status_widget = self.get_status_widget(self.loop) |
690 | + self.assertEqual('press play on tape', status_widget.text) |
691 | + |
692 | + def test_get_status(self): |
693 | + # The get_status API returns the current status message. |
694 | + status_widget = self.get_status_widget(self.loop) |
695 | + status_widget.set_text('hit space to continue') |
696 | + self.assertEqual('hit space to continue', self.app.get_status()) |
697 | + |
698 | + def test_set_message(self): |
699 | + # The set_message API sets the message to be displayed in the |
700 | + # notification area. |
701 | + self.app.set_message('this will disappear') |
702 | + message_widget = self.get_message_widget(self.loop) |
703 | + self.assertEqual('this will disappear', message_widget.text) |
704 | + # An alarm is set to make this message disappear. |
705 | + alarms = self.loop.get_alarms() |
706 | + self.assertEqual(1, len(alarms)) |
707 | + # Calling the callback makes the message go away. |
708 | + _, callback = alarms[0] |
709 | + callback() |
710 | + self.assertEqual('', message_widget.text) |
711 | + |
712 | + def test_get_message(self): |
713 | + # The get_message API returns the current notification message. |
714 | + message_widget = self.get_message_widget(self.loop) |
715 | + message_widget.set_text('42') |
716 | + self.assertEqual('42', self.app.get_message()) |
717 | |
718 | === added file 'quickstart/tests/cli/test_ui.py' |
719 | --- quickstart/tests/cli/test_ui.py 1970-01-01 00:00:00 +0000 |
720 | +++ quickstart/tests/cli/test_ui.py 2013-12-17 15:23:53 +0000 |
721 | @@ -0,0 +1,117 @@ |
722 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
723 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
724 | +# Copyright (C) 2013 Canonical Ltd. |
725 | +# |
726 | +# This program is free software: you can redistribute it and/or modify it under |
727 | +# the terms of the GNU Affero General Public License version 3, as published by |
728 | +# the Free Software Foundation. |
729 | +# |
730 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
731 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
732 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
733 | +# Affero General Public License for more details. |
734 | +# |
735 | +# You should have received a copy of the GNU Affero General Public License |
736 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
737 | + |
738 | +"""Tests for the Juju Quickstart Urwid related utility objects.""" |
739 | + |
740 | +from __future__ import unicode_literals |
741 | + |
742 | +import unittest |
743 | + |
744 | +import mock |
745 | +import urwid |
746 | + |
747 | +from quickstart.cli import ui |
748 | + |
749 | + |
750 | +class TestAppExit(unittest.TestCase): |
751 | + |
752 | + def test_no_return_value(self): |
753 | + # The exception accepts a return value argument. |
754 | + exception = ui.AppExit(42) |
755 | + self.assertEqual(42, exception.return_value) |
756 | + |
757 | + def test_return_value(self): |
758 | + # The exception's return value defaults to None. |
759 | + exception = ui.AppExit() |
760 | + self.assertIsNone(exception.return_value) |
761 | + |
762 | + def test_string_representation(self): |
763 | + # The exception is correctly represented as a byte string. |
764 | + exception = ui.AppExit(42) |
765 | + str_exception = str(exception) |
766 | + self.assertIsInstance(str_exception, bytes) |
767 | + self.assertEqual(b'AppExit: 42', str_exception) |
768 | + |
769 | + |
770 | +class TestExitAndReturn(unittest.TestCase): |
771 | + |
772 | + def test_app_exit(self): |
773 | + # The function returned raises an AppExit error if CTRL-x is passed. |
774 | + function = ui.exit_and_return(42) |
775 | + with self.assertRaises(ui.AppExit) as context_manager: |
776 | + function(ui.EXIT_KEY) |
777 | + self.assertEqual(42, context_manager.exception.return_value) |
778 | + |
779 | + def test_unhandled(self): |
780 | + # Passing other keys, the resulting function is a no-op. |
781 | + function = ui.exit_and_return(42) |
782 | + self.assertIsNone(function('alt z')) |
783 | + |
784 | + |
785 | +class TestCreateControls(unittest.TestCase): |
786 | + |
787 | + def test_resulting_pile(self): |
788 | + # The resulting pile is properly structured: it includes a columns |
789 | + # widget containing the provided widgets. |
790 | + widget0 = urwid.Text('w0') |
791 | + widget1 = urwid.Text('w1') |
792 | + pile = ui.create_controls(widget0, widget1) |
793 | + divider_contents, columns_contents = pile.contents |
794 | + self.assertIsInstance(divider_contents[0], urwid.Divider) |
795 | + columns = columns_contents[0].original_widget |
796 | + widgets = [content[0].base_widget for content in columns.contents] |
797 | + self.assertIs(widget0, widgets[0]) |
798 | + self.assertIs(widget1, widgets[1]) |
799 | + |
800 | + |
801 | +class TestMenuButton(unittest.TestCase): |
802 | + |
803 | + def test_caption(self): |
804 | + # The button's caption is properly set up. |
805 | + button = ui.MenuButton('my caption', mock.Mock()) |
806 | + self.assertEqual('my caption', button._w.base_widget.text) |
807 | + |
808 | + def test_signals(self): |
809 | + # The given callback is called when the click signal is emitted. |
810 | + callback = mock.Mock() |
811 | + button = ui.MenuButton('my caption', callback) |
812 | + urwid.emit_signal(button, 'click', button) |
813 | + callback.assert_called_once_with(button) |
814 | + |
815 | + |
816 | +class TestThunk(unittest.TestCase): |
817 | + |
818 | + widget = 'test-widget' |
819 | + |
820 | + def test_no_args(self): |
821 | + # A callback can be set up without arguments. |
822 | + function = mock.Mock() |
823 | + thunk_function = ui.thunk(function) |
824 | + thunk_function(self.widget) |
825 | + function.assert_called_once_with() |
826 | + |
827 | + def test_args(self): |
828 | + # It is possible to bind arguments to the callback function. |
829 | + function = mock.Mock() |
830 | + thunk_function = ui.thunk(function, 'arg1', 'arg2') |
831 | + thunk_function(self.widget) |
832 | + function.assert_called_once_with('arg1', 'arg2') |
833 | + |
834 | + def test_return_value(self): |
835 | + # The closure returns the value returned by the original callback. |
836 | + sqr = lambda value: value * value |
837 | + thunk_function = ui.thunk(sqr, 3) |
838 | + self.assertEqual(9, thunk_function(self.widget)) |
839 | |
840 | === added file 'quickstart/tests/cli/test_views.py' |
841 | --- quickstart/tests/cli/test_views.py 1970-01-01 00:00:00 +0000 |
842 | +++ quickstart/tests/cli/test_views.py 2013-12-17 15:23:53 +0000 |
843 | @@ -0,0 +1,86 @@ |
844 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
845 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
846 | +# Copyright (C) 2013 Canonical Ltd. |
847 | +# |
848 | +# This program is free software: you can redistribute it and/or modify it under |
849 | +# the terms of the GNU Affero General Public License version 3, as published by |
850 | +# the Free Software Foundation. |
851 | +# |
852 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
853 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
854 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
855 | +# Affero General Public License for more details. |
856 | +# |
857 | +# You should have received a copy of the GNU Affero General Public License |
858 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
859 | + |
860 | +"""Tests for the Juju Quickstart CLI application views.""" |
861 | + |
862 | +from __future__ import unicode_literals |
863 | + |
864 | +from contextlib import contextmanager |
865 | +import unittest |
866 | + |
867 | +import mock |
868 | + |
869 | +from quickstart.cli import ( |
870 | + ui, |
871 | + views, |
872 | +) |
873 | + |
874 | + |
875 | +class TestShow(unittest.TestCase): |
876 | + |
877 | + @contextmanager |
878 | + def patch_setup_urwid_app(self, run_side_effect=None): |
879 | + """Patch the base.setup_urwid_app function. |
880 | + |
881 | + The context manager returns a tuple (mock_loop, mock_app) containing |
882 | + the two mock objects returned by the mock call. |
883 | + |
884 | + The run_side_effect argument can be provided to specify the side |
885 | + effects of the mock_loop.run call. |
886 | + """ |
887 | + mock_loop = mock.Mock() |
888 | + mock_loop.run = mock.Mock(side_effect=run_side_effect) |
889 | + mock_setup_urwid_app = mock.Mock(return_value=(mock_loop, mock.Mock())) |
890 | + setup_urwid_app_path = 'quickstart.cli.views.base.setup_urwid_app' |
891 | + with mock.patch(setup_urwid_app_path, mock_setup_urwid_app): |
892 | + yield mock_setup_urwid_app() |
893 | + |
894 | + def test_show_view(self): |
895 | + # The loop and app objects are properly used by the show function: |
896 | + # the loop is run and the app is passed to the view. |
897 | + view = mock.Mock() |
898 | + with self.patch_setup_urwid_app() as (mock_loop, mock_app): |
899 | + views.show(view) |
900 | + view.assert_called_once_with(mock_app) |
901 | + mock_loop.run.assert_called_once_with() |
902 | + |
903 | + def test_view_exit(self): |
904 | + # An ui.AppExit correctly quits the application. The return value |
905 | + # encapsulated on the exception is also returned by the show function. |
906 | + view = mock.Mock() |
907 | + run_side_effect = ui.AppExit('bad wolf') |
908 | + with self.patch_setup_urwid_app(run_side_effect=run_side_effect): |
909 | + return_value = views.show(view) |
910 | + self.assertEqual('bad wolf', return_value) |
911 | + |
912 | + def test_unhandled_input(self): |
913 | + # The unhandled_input callable is properly set up and registered to |
914 | + # the main loop. |
915 | + view = mock.Mock(return_value=42) |
916 | + with self.patch_setup_urwid_app() as (mock_loop, mock_app): |
917 | + views.show(view) |
918 | + unhandled_input = mock_loop.set_unhandled_input.call_args[0][0] |
919 | + with self.assertRaises(ui.AppExit) as context_manager: |
920 | + unhandled_input(ui.EXIT_KEY) |
921 | + self.assertEqual(42, context_manager.exception.return_value) |
922 | + |
923 | + def test_view_arguments(self): |
924 | + # The view is invoked passing the app and all the optional show |
925 | + # function arguments. |
926 | + view = mock.Mock() |
927 | + with self.patch_setup_urwid_app() as (mock_loop, mock_app): |
928 | + views.show(view, 'arg1', 'arg2') |
929 | + view.assert_called_once_with(mock_app, 'arg1', 'arg2') |
930 | |
931 | === modified file 'quickstart/tests/test_utils.py' |
932 | --- quickstart/tests/test_utils.py 2013-12-10 14:37:58 +0000 |
933 | +++ quickstart/tests/test_utils.py 2013-12-17 15:23:53 +0000 |
934 | @@ -349,6 +349,31 @@ |
935 | mock_call.assert_called_once_with('lsb_release', '-cs') |
936 | |
937 | |
938 | +class TestObjectDict(unittest.TestCase): |
939 | + |
940 | + def setUp(self): |
941 | + self.object_dict = utils.ObjectDict(mykey='myvalue') |
942 | + |
943 | + def test_get_attribute(self): |
944 | + # A value can be retrieved accessing the key as an attribute. |
945 | + self.assertEqual('myvalue', self.object_dict.mykey) |
946 | + |
947 | + def test_get_attribute_error(self): |
948 | + # An AttributeError is raised if the key is not in the dictionary. |
949 | + with self.assertRaises(AttributeError): |
950 | + self.object_dict.no_such_key |
951 | + |
952 | + def test_set_attribute(self): |
953 | + # A key can be added by setting the corresponding attribute. |
954 | + self.object_dict.another_key = 42 |
955 | + self.assertEqual(42, self.object_dict['another_key']) |
956 | + |
957 | + def test_update_attribute(self): |
958 | + # It is possible to update an attribute. |
959 | + self.object_dict.mykey = 47 |
960 | + self.assertEqual(47, self.object_dict.mykey) |
961 | + |
962 | + |
963 | class TestParseBundle( |
964 | helpers.BundleFileTestsMixin, helpers.ValueErrorTestsMixin, |
965 | unittest.TestCase): |
966 | |
967 | === modified file 'quickstart/utils.py' |
968 | --- quickstart/utils.py 2013-12-10 14:37:58 +0000 |
969 | +++ quickstart/utils.py 2013-12-17 15:23:53 +0000 |
970 | @@ -14,6 +14,22 @@ |
971 | # You should have received a copy of the GNU Affero General Public License |
972 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
973 | |
974 | +# The ObjectDict code: |
975 | +# |
976 | +# Copyright 2009 Facebook |
977 | +# |
978 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
979 | +# not use this file except in compliance with the License. You may obtain |
980 | +# a copy of the License at |
981 | +# |
982 | +# http://www.apache.org/licenses/LICENSE-2.0 |
983 | +# |
984 | +# Unless required by applicable law or agreed to in writing, software |
985 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
986 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
987 | +# License for the specific language governing permissions and limitations |
988 | +# under the License. |
989 | + |
990 | """Juju Quickstart utility functions and classes.""" |
991 | |
992 | from __future__ import ( |
993 | @@ -187,6 +203,23 @@ |
994 | return output.strip() |
995 | |
996 | |
997 | +class ObjectDict(dict): |
998 | + """Makes a dictionary behave like an object, with attribute-style access. |
999 | + |
1000 | + Original: |
1001 | + http://www.tornadoweb.org/en/stable/_modules/tornado/util.html#ObjectDict |
1002 | + """ |
1003 | + |
1004 | + def __getattr__(self, name): |
1005 | + try: |
1006 | + return self[name] |
1007 | + except KeyError: |
1008 | + raise AttributeError(name) |
1009 | + |
1010 | + def __setattr__(self, name, value): |
1011 | + self[name] = value |
1012 | + |
1013 | + |
1014 | def parse_bundle(bundle_yaml, bundle_name=None): |
1015 | """Parse the provided bundle YAML encoded contents. |
1016 | |
1017 | |
1018 | === modified file 'requirements.pip' |
1019 | --- requirements.pip 2013-11-14 10:37:29 +0000 |
1020 | +++ requirements.pip 2013-12-17 15:23:53 +0000 |
1021 | @@ -23,3 +23,4 @@ |
1022 | |
1023 | jujuclient==0.11 |
1024 | PyYAML==3.10 |
1025 | +urwid==1.1.1 |
Reviewers: mp+199254_ code.launchpad. net,
Message:
Please take a look.
Description:
Quickstart base structure for views.
This branch implements a way to start
Urwid interactive sessions in quickstart.
This infrastructure will be used in
future branches to implement the
quickstart environment management system.
My apologies for the long diff, but you
can safely ignore all the file headers,
and the code includes long module
docstrings which can help reviewing the
branch.
Tests: `make check`.
QA: app-demo. py`:
run `make` and then `./cli-
this will start a demo application using
the views infrastructure.
Thank you!
https:/ /code.launchpad .net/~frankban/ juju-quickstart /env-manage- base-views/ +merge/ 199254
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/42600044/
Affected files (+913, -1 lines): cli/__init_ _.py cli/base. py cli/ui. py cli/views. py models/ envs.py tests/cli/ __init_ _.py tests/cli/ test_base. py tests/cli/ test_ui. py tests/cli/ test_views. py tests/test_ utils.py
A [revision details]
A cli-app-demo.py
A quickstart/
A quickstart/
A quickstart/
A quickstart/
M quickstart/
A quickstart/
A quickstart/
A quickstart/
A quickstart/
M quickstart/
M quickstart/utils.py
M requirements.pip