Merge lp:~frankban/juju-quickstart/no-closure-for-you into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 112
Proposed branch: lp:~frankban/juju-quickstart/no-closure-for-you
Merge into: lp:juju-quickstart
Diff against target: 334 lines (+138/-101)
1 file modified
quickstart/cli/views.py (+138/-101)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/no-closure-for-you
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+245868@code.launchpad.net

Description of the change

Reorganize view code.

Replace most of the view closures with external
private functions.
This has several good side effects in my opinion:
- view functions are shorter;
- we know which objects are used for each callback
  function, because we need to explicitly pass them
  as arguments;
- external functions can be reused by multiple views
  if required (and we have the _use example here);
- if an external callback become less straightforward,
  it is now possible to directly unit test it.

Tests: `make check`.

QA:
If unit tests pass we have some confidence that
the interactive session is not broken.
However, some time spent on it would be great,
creating/editing/destroying environments, setting
them as default, canceling the changes, starting
one of them, etc...
Thanks a lot.

https://codereview.appspot.com/194080043/

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

Reviewers: mp+245868_code.launchpad.net,

Message:
Please take a look.

Description:
Reorganize view code.

Replace most of the view closures with external
private functions.
This has several good side effects in my opinion:
- view functions are shorter;
- we know which objects are used for each callback
   function, because we need to explicitly pass them
   as arguments;
- external functions can be reused by multiple views
   if required (and we have the _use example here);
- if an external callback become less straightforward,
   it is now possible to directly unit test it.

Tests: `make check`.

QA:
If unit tests pass we have some confidence that
the interactive session is not broken.
However, some time spent on it would be great,
creating/editing/destroying environments, setting
them as default, canceling the changes, starting
one of them, etc...
Thanks a lot.

https://code.launchpad.net/~frankban/juju-quickstart/no-closure-for-you/+merge/245868

(do not edit description out of merge proposal)

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

Affected files (+140, -101 lines):
   A [revision details]
   M quickstart/cli/views.py

Revision history for this message
Madison Scott-Clary (makyo) wrote :

LGTM, no QA yet (if bac doesn't get to it, I can do it later in the day)

https://codereview.appspot.com/194080043/

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

QA was OK.

I changed defaults, created a new environment, and launched a local env.

https://codereview.appspot.com/194080043/

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

*** Submitted:

Reorganize view code.

Replace most of the view closures with external
private functions.
This has several good side effects in my opinion:
- view functions are shorter;
- we know which objects are used for each callback
   function, because we need to explicitly pass them
   as arguments;
- external functions can be reused by multiple views
   if required (and we have the _use example here);
- if an external callback become less straightforward,
   it is now possible to directly unit test it.

Tests: `make check`.

QA:
If unit tests pass we have some confidence that
the interactive session is not broken.
However, some time spent on it would be great,
creating/editing/destroying environments, setting
them as default, canceling the changes, starting
one of them, etc...
Thanks a lot.

R=matthew.scott, bac
CC=
https://codereview.appspot.com/194080043

https://codereview.appspot.com/194080043/

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

Thank you both for the reviews!

https://codereview.appspot.com/194080043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'quickstart/cli/views.py'
2--- quickstart/cli/views.py 2015-01-07 16:46:28 +0000
3+++ quickstart/cli/views.py 2015-01-08 14:53:36 +0000
4@@ -184,24 +184,6 @@
5 for env_name in params.env_db['environments']
6 ], key=operator.itemgetter('name'))
7
8- def create_and_start_local_env():
9- # Automatically create and use a local environment named "local".
10- # This closure can only be called when there are no environments in the
11- # database. For this reason, the new environment is set as default.
12- # Exit the interactive session selecting the newly created environment.
13- env_data = envs.create_local_env_data(
14- params.env_type_db, 'local', is_default=True)
15- _save_and_exit(params.env_db, env_data, params.save_callable)
16-
17- def create_and_start_maas_env(name, server, api_key):
18- # Automatically create and use a MAAS environment.
19- # This closure can only be called when there are no environments in the
20- # database. For this reason, the new environment is set as default.
21- # Exit the interactive session selecting the newly created environment.
22- env_data = envs.create_maas_env_data(
23- params.env_type_db, name, server, api_key, is_default=True)
24- _save_and_exit(params.env_db, env_data, params.save_callable)
25-
26 platform = platform_support.get_platform()
27 supports_local = platform_support.supports_local(platform)
28
29@@ -233,7 +215,8 @@
30 if maas_api_data is not None:
31 maas_name, maas_server, maas_api_key = maas_api_data
32 maas_callback = ui.thunk(
33- create_and_start_maas_env,
34+ _create_and_start_maas_env,
35+ params.env_type_db, params.env_db, params.save_callable,
36 maas_name, maas_server, maas_api_key)
37 widgets.extend([
38 urwid.Text([
39@@ -254,6 +237,9 @@
40 # If the current platform supports local Juju environments, add an
41 # option to automatically create and bootstrap one.
42 if supports_local:
43+ local_callback = ui.thunk(
44+ _create_and_start_local_env,
45+ params.env_type_db, params.env_db, params.save_callable)
46 widgets.extend([
47 urwid.Text([
48 '\nAt the bottom of the page you can find links to '
49@@ -264,7 +250,7 @@
50 urwid.Divider(),
51 ui.MenuButton(
52 '\N{BULLET} automatically create and bootstrap a local '
53- 'environment', ui.thunk(create_and_start_local_env)),
54+ 'environment', local_callback),
55 ])
56
57 app.set_title(title)
58@@ -369,6 +355,31 @@
59 app.set_contents(contents)
60
61
62+def _create_and_start_local_env(env_type_db, env_db, save_callable):
63+ """Automatically create and use a local environment named "local".
64+
65+ This function can only be called when there are no environments in the
66+ database. For this reason, the new environment is set as default.
67+ Exit the interactive session selecting the newly created environment.
68+ """
69+ env_data = envs.create_local_env_data(
70+ env_type_db, 'local', is_default=True)
71+ _save_and_exit(env_db, env_data, save_callable)
72+
73+
74+def _create_and_start_maas_env(
75+ env_type_db, env_db, save_callable, name, server, api_key):
76+ """Automatically create and use a MAAS environment.
77+
78+ This function can only be called when there are no environments in the
79+ database. For this reason, the new environment is set as default.
80+ Exit the interactive session selecting the newly created environment.
81+ """
82+ env_data = envs.create_maas_env_data(
83+ env_type_db, name, server, api_key, is_default=True)
84+ _save_and_exit(env_db, env_data, save_callable)
85+
86+
87 def env_detail(app, params, env_data):
88 """Show details on a Juju environment.
89
90@@ -391,40 +402,6 @@
91 index_view = functools.partial(env_index, app, params)
92 edit_view = functools.partial(env_edit, app, params, env_data)
93
94- def use(env_data):
95- # Quit the interactive session returning the (possibly modified)
96- # environment database and the environment data corresponding to the
97- # selected environment.
98- raise ui.AppExit((params.env_db, env_data))
99-
100- def set_default(env_data):
101- # Set this environment as the default one, save the env_db and return
102- # to the index view.
103- env_name = env_data['name']
104- params.env_db['default'] = env_name
105- params.save_callable(params.env_db)
106- app.set_message('{} successfully set as default'.format(env_name))
107- index_view()
108-
109- def remove(env_data):
110- # The environment deletion is confirmed: remove the environment from
111- # the database, save the new env_db and return to the index view.
112- env_name = env_data['name']
113- envs.remove_env(params.env_db, env_name)
114- params.save_callable(params.env_db)
115- app.set_message('{} successfully removed'.format(env_name))
116- index_view()
117-
118- def confirm_removal(env_data):
119- # Ask confirmation before removing an environment.
120- ui.show_dialog(
121- app, 'Remove the {} environment'.format(env_data['name']),
122- 'This action cannot be undone!',
123- actions=[
124- (('control alert', 'confirm'), ui.thunk(remove, env_data)),
125- ],
126- )
127-
128 env_metadata = envs.get_env_metadata(params.env_type_db, env_data)
129 app.set_title(envs.get_env_short_description(env_data))
130 # Validate the environment.
131@@ -447,21 +424,76 @@
132 ])
133 else:
134 # Without errors, it is possible to use/start this environment.
135- controls.append(ui.MenuButton('use', ui.thunk(use, env_data)))
136+ use_callback = ui.thunk(_use, params.env_db, env_data)
137+ controls.append(ui.MenuButton('use', use_callback))
138 app.set_status(status)
139 if not env_data['is-default']:
140- controls.append(
141- ui.MenuButton('set default', ui.thunk(set_default, env_data)))
142+ # If the environment is not the default one, it is possible to set it
143+ # as default.
144+ set_default_callback = ui.thunk(
145+ _set_default, params.env_db, env_data, params.save_callable,
146+ app.set_message, index_view)
147+ controls.append(ui.MenuButton('set default', set_default_callback))
148+ # Add the controls to edit and remove the environment.
149+ remove_callback = ui.thunk(
150+ _remove, params.env_db, env_data, params.save_callable,
151+ app.set_message, index_view)
152+ confirm_removal_callback = ui.thunk(
153+ _confirm_removal, app, env_data, remove_callback)
154 controls.extend([
155 ui.MenuButton('edit', ui.thunk(edit_view)),
156- ui.MenuButton(
157- ('control alert', 'remove'), ui.thunk(confirm_removal, env_data)),
158+ ui.MenuButton(('control alert', 'remove'), confirm_removal_callback),
159 ])
160 widgets.append(ui.create_controls(*controls))
161 listbox = urwid.ListBox(urwid.SimpleFocusListWalker(widgets))
162 app.set_contents(listbox)
163
164
165+def _use(env_db, env_data):
166+ """Use the selected environment.
167+
168+ Quit the interactive session returning the (possibly modified)
169+ environment database and the environment data corresponding to the
170+ selected environment.
171+ """
172+ raise ui.AppExit((env_db, env_data))
173+
174+
175+def _set_default(env_db, env_data, save_callable, set_message, redirect_view):
176+ """Set this environment as the default one.
177+
178+ Save the env_db and return to the given view.
179+ Also output a notification using the given set_message callable.
180+ """
181+ env_name = env_data['name']
182+ env_db['default'] = env_name
183+ save_callable(env_db)
184+ set_message('{} successfully set as default'.format(env_name))
185+ redirect_view()
186+
187+
188+def _confirm_removal(app, env_data, remove_callable):
189+ """Ask confirmation before removing an environment."""
190+ ui.show_dialog(
191+ app, 'Remove the {} environment'.format(env_data['name']),
192+ 'This action cannot be undone!',
193+ actions=[(('control alert', 'confirm'), remove_callable)],
194+ )
195+
196+
197+def _remove(env_db, env_data, save_callable, set_message, redirect_view):
198+ """Remove the environment represented by env_data from the database.
199+
200+ Save the new env_db and return to the given view.
201+ Also output a notification using the given set_message callable.
202+ """
203+ env_name = env_data['name']
204+ envs.remove_env(env_db, env_name)
205+ save_callable(env_db)
206+ set_message('{} successfully removed'.format(env_name))
207+ redirect_view()
208+
209+
210 def jenv_detail(app, params, env_data):
211 """Show details on a Juju imported environment.
212
213@@ -484,12 +516,6 @@
214 app.set_return_value_on_exit((params.env_db, None))
215 index_view = functools.partial(env_index, app, params)
216
217- def use(env_data):
218- # Quit the interactive session returning the (possibly modified)
219- # environment database and the environment data corresponding to the
220- # selected environment.
221- raise ui.AppExit((params.env_db, env_data))
222-
223 app.set_title(jenv.get_env_short_description(env_data))
224 widgets = []
225 for key, value in jenv.get_env_details(env_data):
226@@ -508,7 +534,7 @@
227
228 controls = [
229 ui.MenuButton('back', ui.thunk(index_view)),
230- ui.MenuButton('use', ui.thunk(use, env_data)),
231+ ui.MenuButton('use', ui.thunk(_use, params.env_db, env_data)),
232 ]
233 widgets.append(ui.create_controls(*controls))
234 listbox = urwid.ListBox(urwid.SimpleFocusListWalker(widgets))
235@@ -566,39 +592,6 @@
236 ' field errors ',
237 ])
238
239- def save(env_data, get_new_env_data):
240- # Create a new environment or save changes for an existing one.
241- # The new values are saved only if the new env_data is valid, in which
242- # case also redirect to the environment detail view.
243- new_env_data = get_new_env_data()
244- # Validate the new env_data.
245- errors = envs.validate(env_metadata, new_env_data)
246- new_name = new_env_data['name']
247- initial_name = env_data.get('name')
248- env_db = params.env_db
249- if (new_name != initial_name) and new_name in env_db['environments']:
250- errors['name'] = 'an environment with this name already exists'
251- # If errors are found, re-render the form passing the errors. This way
252- # the errors are displayed as part of the form and the user is given
253- # the opportunity to fix the invalid values.
254- if errors:
255- return render_form(new_env_data, errors)
256- # Without errors, normalize the new values, update the env_db and save
257- # the resulting environments database.
258- env_data = envs.normalize(env_metadata, new_env_data)
259- # If this is the only environment in the db, set it as the default one.
260- if not env_db['environments']:
261- env_data['is-default'] = True
262- envs.set_env_data(env_db, initial_name, env_data)
263- params.save_callable(env_db)
264- verb = 'modified' if exists else 'created'
265- app.set_message('{} successfully {}'.format(new_name, verb))
266- return detail_view(env_data)
267-
268- def cancel(env_data):
269- # Dismiss any changes and return to the index or detail view.
270- return detail_view(env_data) if exists else index_view()
271-
272 def render_form(data, errors):
273 # Render the environment edit form.
274 widgets = [
275@@ -612,9 +605,15 @@
276 form_widgets, get_new_env_data = forms.create_form(
277 field_value_pairs, errors)
278 widgets.extend(form_widgets)
279+ save_callback = ui.thunk(
280+ _save, params.env_db, env_data, env_metadata, params.save_callable,
281+ exists, get_new_env_data, render_form, app.set_message,
282+ detail_view)
283+ cancel_callback = ui.thunk(
284+ _cancel, env_data, exists, index_view, detail_view)
285 actions = (
286- ('save', ui.thunk(save, env_data, get_new_env_data)),
287- ('cancel', ui.thunk(cancel, env_data)),
288+ ('save', save_callback),
289+ ('cancel', cancel_callback),
290 ('restore', ui.thunk(render_form, env_data, initial_errors)),
291 )
292 widgets.append(forms.create_actions(actions))
293@@ -624,3 +623,41 @@
294
295 # Render the initial form.
296 render_form(env_data, initial_errors)
297+
298+
299+def _save(
300+ env_db, env_data, env_metadata, save_callable, exists,
301+ get_new_env_data, render_form, set_message, redirect_view):
302+ """Create a new environment or save changes for an existing one.
303+
304+ The new values are saved only if the new env_data is valid, in which
305+ case also redirect to the given view.
306+ """
307+ new_env_data = get_new_env_data()
308+ # Validate the new env_data.
309+ errors = envs.validate(env_metadata, new_env_data)
310+ new_name = new_env_data['name']
311+ initial_name = env_data.get('name')
312+ if (new_name != initial_name) and new_name in env_db['environments']:
313+ errors['name'] = 'an environment with this name already exists'
314+ # If errors are found, re-render the form passing the errors. This way
315+ # the errors are displayed as part of the form and the user is given
316+ # the opportunity to fix the invalid values.
317+ if errors:
318+ return render_form(new_env_data, errors)
319+ # Without errors, normalize the new values, update the env_db and save
320+ # the resulting environments database.
321+ env_data = envs.normalize(env_metadata, new_env_data)
322+ # If this is the only environment in the db, set it as the default one.
323+ if not env_db['environments']:
324+ env_data['is-default'] = True
325+ envs.set_env_data(env_db, initial_name, env_data)
326+ save_callable(env_db)
327+ verb = 'modified' if exists else 'created'
328+ set_message('{} successfully {}'.format(new_name, verb))
329+ return redirect_view(env_data)
330+
331+
332+def _cancel(env_data, exists, index_view, detail_view):
333+ """Dismiss any changes and return to the index or detail view."""
334+ return detail_view(env_data) if exists else index_view()

Subscribers

People subscribed via source and target branches