Merge lp:~frankban/juju-quickstart/env-manage-base-views into lp:juju-quickstart

Proposed by Francesco Banconi
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
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+199254@code.launchpad.net

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-app-demo.py`:
this will start a demo application using
the views infrastructure.

Thank you!

https://codereview.appspot.com/42600044/

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

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:
run `make` and then `./cli-app-demo.py`:
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):
   A [revision details]
   A cli-app-demo.py
   A quickstart/cli/__init__.py
   A quickstart/cli/base.py
   A quickstart/cli/ui.py
   A quickstart/cli/views.py
   M quickstart/models/envs.py
   A quickstart/tests/cli/__init__.py
   A quickstart/tests/cli/test_base.py
   A quickstart/tests/cli/test_ui.py
   A quickstart/tests/cli/test_views.py
   M quickstart/tests/test_utils.py
   M quickstart/utils.py
   M requirements.pip

Revision history for this message
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://codereview.appspot.com/42600044/diff/1/quickstart/cli/ui.py
File quickstart/cli/ui.py (right):

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/ui.py#newcode28
quickstart/cli/ui.py:28: # Define the color palette used by the Urwid
application.
Might be nice to describe the columns. I assume they are class name (or
None for default), text style, background style.

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/ui.py#newcode70
quickstart/cli/ui.py:70: def bind(function, *args):
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_thunk(function, *args, **kwargs):
     def thunk(*ignored, **ignored_kwargs):
         return function(*args, **kwargs)
     return thunk

<shrug> :-)

https://codereview.appspot.com/42600044/

Revision history for this message
Brad Crittenden (bac) wrote :

LGTM and QA-OK. Some changes suggested below but nothing substantial.
Thanks for the interesting work.

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/base.py
File quickstart/cli/base.py (right):

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/base.py#newcode19
quickstart/cli/base.py:19: A collection of objects which help building a
Quickstart CLI application
change 'building' -> 'in building' or 'build'

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/base.py#newcode55
quickstart/cli/base.py:55: Improve the level of event loop introspection
so that code and tests
Improves

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/base.py#newcode111
quickstart/cli/base.py:111: def set_contents(listbox):
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://codereview.appspot.com/42600044/diff/1/quickstart/cli/base.py#newcode112
quickstart/cli/base.py:112: contents.original_widget = listbox
original_widget seems like an odd name since it is really just the
current widget.

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/views.py
File quickstart/cli/views.py (right):

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/views.py#newcode51
quickstart/cli/views.py:51: pressing a keyboard shortcut;
by pressing

https://codereview.appspot.com/42600044/diff/1/quickstart/utils.py
File quickstart/utils.py (right):

https://codereview.appspot.com/42600044/diff/1/quickstart/utils.py#newcode194
quickstart/utils.py:194:
http://www.tornadoweb.org/en/stable/_modules/tornado/util.html#ObjectDict
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://www.apache.org/licenses/LICENSE-2.0
#
# 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.

https://codereview.appspot.com/42600044/

55. By Francesco Banconi

Changes as per review.

Revision history for this message
Francesco Banconi (frankban) wrote :
Download full text (4.5 KiB)

*** 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-app-demo.py`:
this will start a demo application using
the views infrastructure.

Thank you!

R=gary.poster, bac
CC=
https://codereview.appspot.com/42600044

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/base.py
File quickstart/cli/base.py (right):

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/base.py#newcode19
quickstart/cli/base.py:19: A collection of objects which help building a
Quickstart CLI application
On 2013/12/17 14:30:47, bac wrote:
> change 'building' -> 'in building' or 'build'

Done.

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/base.py#newcode55
quickstart/cli/base.py:55: Improve the level of event loop introspection
so that code and tests
On 2013/12/17 14:30:47, bac wrote:
> Improves

Done.

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/base.py#newcode111
quickstart/cli/base.py:111: def set_contents(listbox):
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://codereview.appspot.com/42600044/diff/1/quickstart/cli/base.py#newcode112
quickstart/cli/base.py:112: contents.original_widget = listbox
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://codereview.appspot.com/42600044/diff/1/quickstart/cli/ui.py
File quickstart/cli/ui.py (right):

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/ui.py#newcode28
quickstart/cli/ui.py:28: # Define the color palette used by the Urwid
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://codereview.appspot.com/42600044/diff/1/quickstart/cli/ui.py#newcode70
quickstart/cli/ui.py:70: def bind(function, *args):
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_thunk(function, *args, **kwargs):
> def thunk(*ignored, **ignored_kwargs):
> return function(*args, **kwargs)
> return thunk

> <shrug> :-)

Renamed to thunk.

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/views.py
File quickstart/cli/views.py (right):

https://codereview.appspot.com/42600044/diff/1/quickstart/cli/views.py#newcode51
quickstart/...

Read more...

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=== 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

Subscribers

People subscribed via source and target branches