Merge lp:~frankban/juju-quickstart/no-closure-for-you into lp:juju-quickstart
- no-closure-for-you
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+245868@code.launchpad.net |
Commit message
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/
them as default, canceling the changes, starting
one of them, etc...
Thanks a lot.
Francesco Banconi (frankban) wrote : | # |
Madison Scott-Clary (makyo) wrote : | # |
LGTM, no QA yet (if bac doesn't get to it, I can do it later in the day)
Brad Crittenden (bac) wrote : | # |
LGTM. WIll QA now.
Brad Crittenden (bac) wrote : | # |
QA was OK.
I changed defaults, created a new environment, and launched a local env.
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/
them as default, canceling the changes, starting
one of them, etc...
Thanks a lot.
R=matthew.scott, bac
CC=
https:/
Francesco Banconi (frankban) wrote : | # |
Thank you both for the reviews!
Preview Diff
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() |
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: editing/ destroying environments, setting
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/
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): cli/views. py
A [revision details]
M quickstart/