Merge lp:~frankban/juju-quickstart/views-params into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 111
Proposed branch: lp:~frankban/juju-quickstart/views-params
Merge into: lp:juju-quickstart
Diff against target: 975 lines (+301/-172)
6 files modified
quickstart/cli/params.py (+51/-0)
quickstart/cli/views.py (+70/-74)
quickstart/manage.py (+11/-4)
quickstart/tests/cli/test_params.py (+87/-0)
quickstart/tests/cli/test_views.py (+71/-90)
quickstart/tests/test_manage.py (+11/-4)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/views-params
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+245753@code.launchpad.net

Description of the change

Refactor view parameters.

Implement a Params object used to store
common view parameters. This way it will
be easier to add parameters in the future
(e.g. one callable to remove stale jenv files).

New code include the params module and a fix
to the code handling the listing of jenv files
in the index view: now the header message is
only displayed if jenv files actually exist.

The rest of the diff is mechanical: i.e. replacing
the single view arguments with the params named
tuple.

Tests: `make check`.

QA:
Use the quickstart interactive session and
check everything works ok.

https://codereview.appspot.com/194030043/

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

Reviewers: mp+245753_code.launchpad.net,

Message:
Please take a look.

Description:
Refactor view parameters.

Implement a Params object used to store
common view parameters. This way it will
be easier to add parameters in the future
(e.g. one callable to remove stale jenv files).

New code include the params module and a fix
to the code handling the listing of jenv files
in the index view: now the header message is
only displayed if jenv files actually exist.

The rest of the diff is mechanical: i.e. replacing
the single view arguments with the params named
tuple.

Tests: `make check`.

QA:
Use the quickstart interactive session and
check everything works ok.

https://code.launchpad.net/~frankban/juju-quickstart/views-params/+merge/245753

(do not edit description out of merge proposal)

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

Affected files (+303, -172 lines):
   A [revision details]
   A quickstart/cli/params.py
   M quickstart/cli/views.py
   M quickstart/manage.py
   A quickstart/tests/cli/test_params.py
   M quickstart/tests/cli/test_views.py
   M quickstart/tests/test_manage.py

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

LGTM. No QA yet.

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

https://codereview.appspot.com/194030043/diff/1/quickstart/cli/views.py#newcode314
quickstart/cli/views.py:314: # and supposed to be working/active. The
user has the ability to select
Maybe 'assumed' would be clearer than 'supposed'?

https://codereview.appspot.com/194030043/diff/1/quickstart/tests/cli/test_params.py
File quickstart/tests/cli/test_params.py (right):

https://codereview.appspot.com/194030043/diff/1/quickstart/tests/cli/test_params.py#newcode87
quickstart/tests/cli/test_params.py:87: self.assertNotEqual(self.params,
params)
Such a nice set of tests! Thanks for the clarity.

https://codereview.appspot.com/194030043/

Revision history for this message
Martin Hilton (martin-hilton) wrote :
Revision history for this message
Brad Crittenden (bac) wrote :

QA-OK. I simply ran quickstart and launched a local provider.
Everything came up as expected.

https://codereview.appspot.com/194030043/

114. By Francesco Banconi

Improve view comment.

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

*** Submitted:

Refactor view parameters.

Implement a Params object used to store
common view parameters. This way it will
be easier to add parameters in the future
(e.g. one callable to remove stale jenv files).

New code include the params module and a fix
to the code handling the listing of jenv files
in the index view: now the header message is
only displayed if jenv files actually exist.

The rest of the diff is mechanical: i.e. replacing
the single view arguments with the params named
tuple.

Tests: `make check`.

QA:
Use the quickstart interactive session and
check everything works ok.

R=bac, martin.hilton
CC=
https://codereview.appspot.com/194030043

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

https://codereview.appspot.com/194030043/diff/1/quickstart/cli/views.py#newcode314
quickstart/cli/views.py:314: # and supposed to be working/active. The
user has the ability to select
On 2015/01/07 15:58:26, bac wrote:
> Maybe 'assumed' would be clearer than 'supposed'?

Done.

https://codereview.appspot.com/194030043/

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 'quickstart/cli/params.py'
2--- quickstart/cli/params.py 1970-01-01 00:00:00 +0000
3+++ quickstart/cli/params.py 2015-01-07 16:46:42 +0000
4@@ -0,0 +1,51 @@
5+# This file is part of the Juju Quickstart Plugin, which lets users set up a
6+# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
7+# Copyright (C) 2015 Canonical Ltd.
8+#
9+# This program is free software: you can redistribute it and/or modify it under
10+# the terms of the GNU Affero General Public License version 3, as published by
11+# the Free Software Foundation.
12+#
13+# This program is distributed in the hope that it will be useful, but WITHOUT
14+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
15+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+# Affero General Public License for more details.
17+#
18+# You should have received a copy of the GNU Affero General Public License
19+# along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
21+"""Juju Quickstart view parameters implementation.
22+
23+This module contains an implementation of a composite parameter object to be
24+used in views.
25+
26+The Params named tuple is intended to both help grouping view parameters
27+together and avoid undesired mutations, by allowing explicit copies of the
28+whole parameters object (see the Params.copy instance method).
29+"""
30+
31+from __future__ import unicode_literals
32+
33+from collections import namedtuple
34+import copy
35+
36+
37+_PARAMS = ('env_type_db', 'env_db', 'jenv_db', 'save_callable')
38+
39+
40+class Params(namedtuple('Params', _PARAMS)):
41+ """View parameters as a named tuple."""
42+
43+ # Define empty slots to keep memory requirements low by preventing the
44+ # creation of instance dictionaries.
45+ # See https://docs.python.org/2/library/collections.html
46+ __slots__ = ()
47+
48+ def copy(self):
49+ """Return a deep copy of the stored parameters."""
50+ return self.__class__(
51+ env_type_db=self.env_type_db,
52+ env_db=copy.deepcopy(self.env_db),
53+ jenv_db=copy.deepcopy(self.jenv_db),
54+ save_callable=self.save_callable,
55+ )
56
57=== modified file 'quickstart/cli/views.py'
58--- quickstart/cli/views.py 2014-12-17 11:34:06 +0000
59+++ quickstart/cli/views.py 2015-01-07 16:46:42 +0000
60@@ -155,36 +155,33 @@
61 raise ui.AppExit((env_db, env_data))
62
63
64-def env_index(app, env_type_db, env_db, jenv_db, save_callable):
65+def env_index(app, params):
66 """Show the Juju environments list.
67
68 The env_detail view is displayed when the user clicks on an environment.
69 From here it is also possible to switch to the edit view in order to create
70 a new environment.
71
72- Receives:
73+ Receives a params namedtuple-like object including the following fields:
74 - env_type_db: the environments meta information;
75 - env_db: the environments database;
76 - jenv_db: the jenv files database;
77 - save_callable: a function called to save a new environment database.
78+ See quickstart.cli.params.Params.
79 """
80 # XXX frankban 16/12/2014: this function is too long, subdivide it.
81- env_db = copy.deepcopy(env_db)
82- jenv_db = copy.deepcopy(jenv_db)
83+ params = params.copy()
84 # All the environment views return a tuple (new_env_db, env_data).
85 # Set the env_data to None in the case the user quits the application
86 # without selecting an environment to use.
87- app.set_return_value_on_exit((env_db, None))
88- detail_view = functools.partial(
89- env_detail, app, env_type_db, env_db, jenv_db, save_callable)
90- jenv_view = functools.partial(
91- jenv_detail, app, env_type_db, env_db, jenv_db, save_callable)
92- edit_view = functools.partial(
93- env_edit, app, env_type_db, env_db, jenv_db, save_callable)
94+ app.set_return_value_on_exit((params.env_db, None))
95+ detail_view = functools.partial(env_detail, app, params)
96+ jenv_view = functools.partial(jenv_detail, app, params)
97+ edit_view = functools.partial(env_edit, app, params)
98 # Alphabetically sort the existing environments.
99 environments = sorted([
100- envs.get_env_data(env_db, env_name)
101- for env_name in env_db['environments']
102+ envs.get_env_data(params.env_db, env_name)
103+ for env_name in params.env_db['environments']
104 ], key=operator.itemgetter('name'))
105
106 def create_and_start_local_env():
107@@ -193,8 +190,8 @@
108 # database. For this reason, the new environment is set as default.
109 # Exit the interactive session selecting the newly created environment.
110 env_data = envs.create_local_env_data(
111- env_type_db, 'local', is_default=True)
112- _save_and_exit(env_db, env_data, save_callable)
113+ params.env_type_db, 'local', is_default=True)
114+ _save_and_exit(params.env_db, env_data, params.save_callable)
115
116 def create_and_start_maas_env(name, server, api_key):
117 # Automatically create and use a MAAS environment.
118@@ -202,8 +199,8 @@
119 # database. For this reason, the new environment is set as default.
120 # Exit the interactive session selecting the newly created environment.
121 env_data = envs.create_maas_env_data(
122- env_type_db, name, server, api_key, is_default=True)
123- _save_and_exit(env_db, env_data, save_callable)
124+ params.env_type_db, name, server, api_key, is_default=True)
125+ _save_and_exit(params.env_db, env_data, params.save_callable)
126
127 platform = platform_support.get_platform()
128 supports_local = platform_support.supports_local(platform)
129@@ -279,7 +276,7 @@
130 focus_position = None
131 active_found = default_found = errors_found = False
132 existing_widgets_num = len(widgets)
133- remaining_jenv_db = copy.deepcopy(jenv_db)
134+ remaining_jenv_db = copy.deepcopy(params.jenv_db)
135 for position, env_data in enumerate(environments):
136 bullet = '\N{BULLET}'
137 # Is this environment the default one?
138@@ -294,7 +291,7 @@
139 bullet = ('active', bullet)
140 else:
141 # Check if this environment is valid.
142- env_metadata = envs.get_env_metadata(env_type_db, env_data)
143+ env_metadata = envs.get_env_metadata(params.env_type_db, env_data)
144 errors = envs.validate(env_metadata, env_data)
145 if errors:
146 errors_found = True
147@@ -306,25 +303,28 @@
148
149 # Alphabetically sort the remaining environments not included in the
150 # environments.yaml file.
151- environments = sorted([
152+ environments = list(sorted([
153 envs.get_env_data(remaining_jenv_db, env_name)
154 for env_name in remaining_jenv_db['environments']
155- ], key=operator.itemgetter('name'))
156+ ], key=operator.itemgetter('name')))
157
158- # List the remaining active environments. Those environments are not
159- # included in the environments.yaml file: they are probably imported and
160- # supposed to be working/active. The user has the ability to select them.
161- widgets.extend([
162- urwid.Divider(),
163- urwid.Text(('highlight', 'Other active environments')),
164- urwid.Text('(imported/not included in your environments.yaml file):'),
165- urwid.Divider(),
166- ])
167- bullet = ('active', '\N{BULLET}')
168- for env_data in environments:
169- env_short_description = jenv.get_env_short_description(env_data)
170- text = [bullet, ' {}'.format(env_short_description)]
171- widgets.append(ui.MenuButton(text, ui.thunk(jenv_view, env_data)))
172+ if environments:
173+ # List the remaining active environments. Those environments are not
174+ # included in the environments.yaml file: they are probably imported
175+ # and assumed to be working/active. The user has the ability to select
176+ # them.
177+ widgets.extend([
178+ urwid.Divider(),
179+ urwid.Text(('highlight', 'Other active environments')),
180+ urwid.Text(
181+ '(imported/not included in your environments.yaml file):'),
182+ urwid.Divider(),
183+ ])
184+ bullet = ('active', '\N{BULLET}')
185+ for env_data in environments:
186+ env_short_description = jenv.get_env_short_description(env_data)
187+ text = [bullet, ' {}'.format(env_short_description)]
188+ widgets.append(ui.MenuButton(text, ui.thunk(jenv_view, env_data)))
189
190 # Set up the "create a new environment" section.
191 widgets.extend([
192@@ -342,7 +342,7 @@
193 if not supports_local:
194 filter_function = lambda env_type, _: env_type != 'local'
195 supported_env_types = envs.get_supported_env_types(
196- env_type_db, filter_function=filter_function)
197+ params.env_type_db, filter_function=filter_function)
198 # Add the buttons used to create new environments.
199 widgets.extend([
200 ui.MenuButton(
201@@ -369,42 +369,40 @@
202 app.set_contents(contents)
203
204
205-def env_detail(app, env_type_db, env_db, jenv_db, save_callable, env_data):
206+def env_detail(app, params, env_data):
207 """Show details on a Juju environment.
208
209 From this view it is possible to start the environment, set it as default,
210 edit/remove the environment.
211
212- Receives:
213+ Receives a params namedtuple-like object including the following fields:
214 - env_type_db: the environments meta information;
215 - env_db: the environments database;
216 - jenv_db: the jenv files database;
217- - save_callable: a function called to save a new environment database;
218- - env_data: the environment data.
219+ - save_callable: a function called to save a new environment database.
220+ See quickstart.cli.params.Params.
221+ Also receives the current environment data env_data.
222 """
223- env_db = copy.deepcopy(env_db)
224- jenv_db = copy.deepcopy(jenv_db)
225+ params = params.copy()
226 # All the environment views return a tuple (new_env_db, env_data).
227 # Set the env_data to None in the case the user quits the application
228 # without selecting an environment to use.
229- app.set_return_value_on_exit((env_db, None))
230- index_view = functools.partial(
231- env_index, app, env_type_db, env_db, jenv_db, save_callable)
232- edit_view = functools.partial(
233- env_edit, app, env_type_db, env_db, jenv_db, save_callable, env_data)
234+ app.set_return_value_on_exit((params.env_db, None))
235+ index_view = functools.partial(env_index, app, params)
236+ edit_view = functools.partial(env_edit, app, params, env_data)
237
238 def use(env_data):
239 # Quit the interactive session returning the (possibly modified)
240 # environment database and the environment data corresponding to the
241 # selected environment.
242- raise ui.AppExit((env_db, env_data))
243+ raise ui.AppExit((params.env_db, env_data))
244
245 def set_default(env_data):
246 # Set this environment as the default one, save the env_db and return
247 # to the index view.
248 env_name = env_data['name']
249- env_db['default'] = env_name
250- save_callable(env_db)
251+ params.env_db['default'] = env_name
252+ params.save_callable(params.env_db)
253 app.set_message('{} successfully set as default'.format(env_name))
254 index_view()
255
256@@ -412,8 +410,8 @@
257 # The environment deletion is confirmed: remove the environment from
258 # the database, save the new env_db and return to the index view.
259 env_name = env_data['name']
260- envs.remove_env(env_db, env_name)
261- save_callable(env_db)
262+ envs.remove_env(params.env_db, env_name)
263+ params.save_callable(params.env_db)
264 app.set_message('{} successfully removed'.format(env_name))
265 index_view()
266
267@@ -427,7 +425,7 @@
268 ],
269 )
270
271- env_metadata = envs.get_env_metadata(env_type_db, env_data)
272+ env_metadata = envs.get_env_metadata(params.env_type_db, env_data)
273 app.set_title(envs.get_env_short_description(env_data))
274 # Validate the environment.
275 errors = envs.validate(env_metadata, env_data)
276@@ -464,34 +462,33 @@
277 app.set_contents(listbox)
278
279
280-def jenv_detail(app, env_type_db, env_db, jenv_db, save_callable, env_data):
281+def jenv_detail(app, params, env_data):
282 """Show details on a Juju imported environment.
283
284 The environment is not included in the environments.yaml file, but just
285 found in the jenv database.
286 From this view it is possible to start the environment.
287
288- Receives:
289+ Receives a params namedtuple-like object including the following fields:
290 - env_type_db: the environments meta information;
291 - env_db: the environments database;
292 - jenv_db: the jenv files database;
293 - save_callable: a function called to save a new environment database;
294- - env_data: the environment data.
295+ See quickstart.cli.params.Params.
296+ Also receives the current environment data env_data.
297 """
298- env_db = copy.deepcopy(env_db)
299- jenv_db = copy.deepcopy(jenv_db)
300+ params = params.copy()
301 # All the environment views return a tuple (new_env_db, env_data).
302 # Set the env_data to None in the case the user quits the application
303 # without selecting an environment to use.
304- app.set_return_value_on_exit((env_db, None))
305- index_view = functools.partial(
306- env_index, app, env_type_db, env_db, jenv_db, save_callable)
307+ app.set_return_value_on_exit((params.env_db, None))
308+ index_view = functools.partial(env_index, app, params)
309
310 def use(env_data):
311 # Quit the interactive session returning the (possibly modified)
312 # environment database and the environment data corresponding to the
313 # selected environment.
314- raise ui.AppExit((env_db, env_data))
315+ raise ui.AppExit((params.env_db, env_data))
316
317 app.set_title(jenv.get_env_short_description(env_data))
318 widgets = []
319@@ -519,36 +516,34 @@
320 app.set_status([' \N{RIGHTWARDS ARROW OVER LEFTWARDS ARROW} navigate '])
321
322
323-def env_edit(app, env_type_db, env_db, jenv_db, save_callable, env_data):
324+def env_edit(app, params, env_data):
325 """Create or modify a Juju environment.
326
327 This view displays an edit form allowing for environment
328 creation/modification. Saving the form redirects to the environment detail
329 view if the values are valid.
330
331- Receives:
332+ Receives a params namedtuple-like object including the following fields:
333 - env_type_db: the environments meta information;
334 - env_db: the environments database;
335 - jenv_db: the jenv files database;
336 - save_callable: a function called to save a new environment database;
337- - env_data: the environment data.
338+ See quickstart.cli.params.Params.
339+ Also receives the current environment data env_data.
340
341 The last value (env_data) indicates whether this view is used to create a
342 new environment or to change an existing one. In the former case, env_data
343 does not include the "name" key. If instead the environment already exists,
344 env_data includes the "name" key and all the other environment info.
345 """
346- env_db = copy.deepcopy(env_db)
347- jenv_db = copy.deepcopy(jenv_db)
348+ params = params.copy()
349 # All the environment views return a tuple (new_env_db, env_data).
350 # Set the env_data to None in the case the user quits the application
351 # without selecting an environment to use.
352- app.set_return_value_on_exit((env_db, None))
353- env_metadata = envs.get_env_metadata(env_type_db, env_data)
354- index_view = functools.partial(
355- env_index, app, env_type_db, env_db, jenv_db, save_callable)
356- detail_view = functools.partial(
357- env_detail, app, env_type_db, env_db, jenv_db, save_callable)
358+ app.set_return_value_on_exit((params.env_db, None))
359+ env_metadata = envs.get_env_metadata(params.env_type_db, env_data)
360+ index_view = functools.partial(env_index, app, params)
361+ detail_view = functools.partial(env_detail, app, params)
362 if 'name' in env_data:
363 exists = True
364 title = 'Edit the {} environment'
365@@ -580,6 +575,7 @@
366 errors = envs.validate(env_metadata, new_env_data)
367 new_name = new_env_data['name']
368 initial_name = env_data.get('name')
369+ env_db = params.env_db
370 if (new_name != initial_name) and new_name in env_db['environments']:
371 errors['name'] = 'an environment with this name already exists'
372 # If errors are found, re-render the form passing the errors. This way
373@@ -594,7 +590,7 @@
374 if not env_db['environments']:
375 env_data['is-default'] = True
376 envs.set_env_data(env_db, initial_name, env_data)
377- save_callable(env_db)
378+ params.save_callable(env_db)
379 verb = 'modified' if exists else 'created'
380 app.set_message('{} successfully {}'.format(new_name, verb))
381 return detail_view(env_data)
382
383=== modified file 'quickstart/manage.py'
384--- quickstart/manage.py 2014-12-16 16:52:49 +0000
385+++ quickstart/manage.py 2015-01-07 16:46:42 +0000
386@@ -38,7 +38,10 @@
387 settings,
388 utils,
389 )
390-from quickstart.cli import views
391+from quickstart.cli import (
392+ params,
393+ views,
394+)
395 from quickstart.models import (
396 charms,
397 envs,
398@@ -210,9 +213,13 @@
399 Exit the application if the user exits the interactive session without
400 selecting an environment to start.
401 """
402- save_callable = _create_save_callable(parser, env_file)
403- new_env_db, env_data = views.show(
404- views.env_index, env_type_db, env_db, jenv_db, save_callable)
405+ parameters = params.Params(
406+ env_type_db=env_type_db,
407+ env_db=env_db,
408+ jenv_db=jenv_db,
409+ save_callable=_create_save_callable(parser, env_file),
410+ )
411+ new_env_db, env_data = views.show(views.env_index, parameters)
412 if new_env_db != env_db:
413 print('changes to the environments file have been saved')
414 if env_data is None:
415
416=== added file 'quickstart/tests/cli/test_params.py'
417--- quickstart/tests/cli/test_params.py 1970-01-01 00:00:00 +0000
418+++ quickstart/tests/cli/test_params.py 2015-01-07 16:46:42 +0000
419@@ -0,0 +1,87 @@
420+# This file is part of the Juju Quickstart Plugin, which lets users set up a
421+# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
422+# Copyright (C) 2015 Canonical Ltd.
423+#
424+# This program is free software: you can redistribute it and/or modify it under
425+# the terms of the GNU Affero General Public License version 3, as published by
426+# the Free Software Foundation.
427+#
428+# This program is distributed in the hope that it will be useful, but WITHOUT
429+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
430+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
431+# Affero General Public License for more details.
432+#
433+# You should have received a copy of the GNU Affero General Public License
434+# along with this program. If not, see <http://www.gnu.org/licenses/>.
435+
436+"""Tests for the Juju Quickstart CLI parameters implementation."""
437+
438+from __future__ import unicode_literals
439+
440+import unittest
441+
442+from quickstart.cli import params
443+from quickstart.models import envs
444+from quickstart.tests import helpers
445+
446+
447+class TestParams(unittest.TestCase):
448+
449+ def setUp(self):
450+ # Store parameters.
451+ self.env_type_db = envs.get_env_type_db()
452+ self.env_db = helpers.make_env_db()
453+ self.jenv_db = helpers.make_jenv_db()
454+ self.save_callable = lambda env_db: None
455+ # Set up a params object used in tests.
456+ self.params = params.Params(
457+ env_type_db=self.env_type_db,
458+ env_db=self.env_db,
459+ jenv_db=self.jenv_db,
460+ save_callable=self.save_callable,
461+ )
462+
463+ def test_tuple(self):
464+ # The params object can be used as a tuple.
465+ env_type_db, env_db, jenv_db, save_callable = self.params
466+ self.assertIs(self.env_type_db, env_type_db)
467+ self.assertIs(self.env_db, env_db)
468+ self.assertIs(self.jenv_db, jenv_db)
469+ self.assertIs(self.save_callable, save_callable)
470+
471+ def test_attributes(self):
472+ # Parameters can be accessed as attributes.
473+ self.assertIs(self.env_type_db, self.params.env_type_db)
474+ self.assertIs(self.env_db, self.params.env_db)
475+ self.assertIs(self.jenv_db, self.params.jenv_db)
476+ self.assertIs(self.save_callable, self.params.save_callable)
477+
478+ def test_immutable(self):
479+ # It is not possible to replace a stored parameter.
480+ with self.assertRaises(AttributeError):
481+ self.params.env_db = {}
482+
483+ def test_copy(self):
484+ # Params can be copied.
485+ params = self.params.copy()
486+ # The original object is not mutated by the copy.
487+ self.assertIs(self.env_type_db, self.params.env_type_db)
488+ self.assertIs(self.env_db, self.params.env_db)
489+ self.assertIs(self.jenv_db, self.params.jenv_db)
490+ self.assertIs(self.save_callable, self.params.save_callable)
491+ # The new params object stores the same data.
492+ self.assertEqual(self.params, params)
493+ # But they do not refer to the same object.
494+ self.assertIsNot(self.params, params)
495+
496+ def test_copy_mutations(self):
497+ # Internal mutable objects can be still mutated.
498+ params = self.params.copy()
499+ # Mutate the copy.
500+ params.env_db['environments']['lxc'] = {}
501+ # The mutation took effect.
502+ self.assertNotEqual(self.env_db, params.env_db)
503+ # The original object is still preserved.
504+ self.assertIs(self.env_db, self.params.env_db)
505+ # The two now store different data.
506+ self.assertNotEqual(self.params, params)
507
508=== modified file 'quickstart/tests/cli/test_views.py'
509--- quickstart/tests/cli/test_views.py 2014-12-17 11:47:43 +0000
510+++ quickstart/tests/cli/test_views.py 2015-01-07 16:46:42 +0000
511@@ -31,6 +31,7 @@
512 from quickstart.cli import (
513 base,
514 forms,
515+ params,
516 ui,
517 views,
518 )
519@@ -166,6 +167,15 @@
520 """
521 return lambda arg: isinstance(arg, cls)
522
523+ def make_params(self, env_db, jenv_db):
524+ """Create and return view parameters using the given env databases."""
525+ return params.Params(
526+ env_type_db=self.env_type_db,
527+ env_db=env_db,
528+ jenv_db=jenv_db,
529+ save_callable=self.save_callable,
530+ )
531+
532
533 class TestEnvIndex(EnvViewTestsMixin, unittest.TestCase):
534
535@@ -175,6 +185,7 @@
536 create_maas_caption = (
537 '\N{BULLET} automatically create and bootstrap the {} MAAS '
538 'environment'.format(MAAS_NAME))
539+ active_environments_message = 'Other active environments'
540
541 def test_view_default_return_value_on_exit(self):
542 # The view configures the app so that the return value on user exit is
543@@ -182,8 +193,7 @@
544 # meaning no environment has been selected.
545 env_db = helpers.make_env_db()
546 jenv_db = helpers.make_jenv_db()
547- views.env_index(
548- self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
549+ views.env_index(self.app, self.make_params(env_db, jenv_db))
550 new_env_db, env_data = self.get_on_exit_return_value(self.loop)
551 self.assertEqual(env_db, new_env_db)
552 self.assertIsNot(env_db, new_env_db)
553@@ -193,8 +203,7 @@
554 # The application title is correctly set up.
555 env_db = helpers.make_env_db()
556 jenv_db = helpers.make_jenv_db()
557- views.env_index(
558- self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
559+ views.env_index(self.app, self.make_params(env_db, jenv_db))
560 self.assertEqual(
561 'Select an existing Juju environment or create a new one',
562 self.app.get_title())
563@@ -203,8 +212,7 @@
564 # The application title changes if the env_db has no environments.
565 env_db = {'environments': {}}
566 jenv_db = helpers.make_jenv_db()
567- views.env_index(
568- self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
569+ views.env_index(self.app, self.make_params(env_db, jenv_db))
570 self.assertEqual(
571 'No Juju environments already set up: please create one',
572 self.app.get_title())
573@@ -215,9 +223,7 @@
574 env_db = helpers.make_env_db()
575 jenv_db = {'environments': {}}
576 with local_envs_supported(True):
577- views.env_index(
578- self.app, self.env_type_db, env_db, jenv_db,
579- self.save_callable)
580+ views.env_index(self.app, self.make_params(env_db, jenv_db))
581 buttons = self.get_widgets_in_contents(
582 filter_function=self.is_a(ui.MenuButton))
583 # A button is created for each existing environment (see details) and
584@@ -232,9 +238,7 @@
585 env_db = {'environments': {}}
586 jenv_db = helpers.make_jenv_db()
587 with local_envs_supported(True):
588- views.env_index(
589- self.app, self.env_type_db, env_db, jenv_db,
590- self.save_callable)
591+ views.env_index(self.app, self.make_params(env_db, jenv_db))
592 buttons = self.get_widgets_in_contents(
593 filter_function=self.is_a(ui.MenuButton))
594 # A button is created for each existing environment (see details) and
595@@ -249,6 +253,20 @@
596 1
597 )
598 self.assertEqual(expected_buttons_number, len(buttons))
599+ # A text widget is displayed as header for the imported environments.
600+ widgets = self.get_widgets_in_contents(
601+ filter_function=self.is_a(urwid.Text))
602+ self.assertEqual(self.active_environments_message, widgets[2].text)
603+
604+ def test_view_contents_without_imported_envs(self):
605+ # If there are no active imported environments the corresponding
606+ # header text is not displayed.
607+ env_db = jenv_db = {'environments': {}}
608+ views.env_index(self.app, self.make_params(env_db, jenv_db))
609+ widgets = self.get_widgets_in_contents(
610+ filter_function=self.is_a(urwid.Text))
611+ texts = [widget.text for widget in widgets]
612+ self.assertNotIn(self.active_environments_message, texts)
613
614 def test_new_local_environment_disabled(self):
615 # The option to create a new local environment is not present if they
616@@ -256,9 +274,7 @@
617 env_db = helpers.make_env_db()
618 jenv_db = helpers.make_jenv_db()
619 with local_envs_supported(False):
620- views.env_index(
621- self.app, self.env_type_db, env_db, jenv_db,
622- self.save_callable)
623+ views.env_index(self.app, self.make_params(env_db, jenv_db))
624 buttons = self.get_widgets_in_contents(
625 filter_function=self.is_a(ui.MenuButton))
626 captions = map(cli_helpers.get_button_caption, buttons)
627@@ -273,8 +289,8 @@
628 # The environment detail view is called when clicking an environment.
629 env_db = helpers.make_env_db()
630 jenv_db = {'environments': {}}
631- views.env_index(
632- self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
633+ params = self.make_params(env_db, jenv_db)
634+ views.env_index(self.app, params)
635 buttons = self.get_widgets_in_contents(
636 filter_function=self.is_a(ui.MenuButton))
637 # The environments are listed in alphabetical order.
638@@ -288,9 +304,7 @@
639 # When the button is clicked, the detail view is called passing the
640 # corresponding environment data.
641 cli_helpers.emit(button)
642- mock_env_detail.assert_called_once_with(
643- self.app, self.env_type_db, env_db, jenv_db,
644- self.save_callable, env_data)
645+ mock_env_detail.assert_called_once_with(self.app, params, env_data)
646 # Reset the mock so that it does not include any calls on the next
647 # loop cycle.
648 mock_env_detail.reset_mock()
649@@ -300,10 +314,9 @@
650 # The jenv detail view is called when clicking an imported environment.
651 env_db = {'environments': {}}
652 jenv_db = helpers.make_jenv_db()
653+ params = self.make_params(env_db, jenv_db)
654 with local_envs_supported(False):
655- views.env_index(
656- self.app, self.env_type_db, env_db, jenv_db,
657- self.save_callable)
658+ views.env_index(self.app, params)
659 buttons = self.get_widgets_in_contents(
660 filter_function=self.is_a(ui.MenuButton))
661 # The environments are listed in alphabetical order.
662@@ -318,8 +331,7 @@
663 # passing the corresponding environment data.
664 cli_helpers.emit(button)
665 mock_jenv_detail.assert_called_once_with(
666- self.app, self.env_type_db, env_db, jenv_db,
667- self.save_callable, env_data)
668+ self.app, params, env_data)
669 # Reset the mock so that it does not include any calls on the next
670 # loop cycle.
671 mock_jenv_detail.reset_mock()
672@@ -330,10 +342,9 @@
673 # environment.
674 env_db = helpers.make_env_db()
675 jenv_db = {'environments': {}}
676+ params = self.make_params(env_db, jenv_db)
677 with local_envs_supported(True):
678- views.env_index(
679- self.app, self.env_type_db, env_db, jenv_db,
680- self.save_callable)
681+ views.env_index(self.app, params)
682 buttons = self.get_widgets_in_contents(
683 filter_function=self.is_a(ui.MenuButton))
684 env_types = envs.get_supported_env_types(self.env_type_db)
685@@ -347,8 +358,7 @@
686 # corresponding environment data.
687 cli_helpers.emit(button)
688 mock_env_edit.assert_called_once_with(
689- self.app, self.env_type_db, env_db, jenv_db,
690- self.save_callable, {
691+ self.app, params, {
692 'type': env_type,
693 'default-series': settings.JUJU_GUI_SUPPORTED_SERIES[-1],
694 })
695@@ -365,9 +375,7 @@
696 jenv_db = helpers.make_jenv_db()
697 with maas_env_detected(False):
698 with local_envs_supported(True):
699- views.env_index(
700- self.app, self.env_type_db, env_db, jenv_db,
701- self.save_callable)
702+ views.env_index(self.app, self.make_params(env_db, jenv_db))
703 buttons = self.get_widgets_in_contents(
704 filter_function=self.is_a(ui.MenuButton))
705 # The "create and bootstrap" button is the first one in the contents.
706@@ -390,9 +398,7 @@
707 env_db = envs.create_empty_env_db()
708 jenv_db = helpers.make_jenv_db()
709 with local_envs_supported(False):
710- views.env_index(
711- self.app, self.env_type_db, env_db, jenv_db,
712- self.save_callable)
713+ views.env_index(self.app, self.make_params(env_db, jenv_db))
714 buttons = self.get_widgets_in_contents(
715 filter_function=self.is_a(ui.MenuButton))
716 # No "create and bootstrap local" buttons are present.
717@@ -408,9 +414,7 @@
718 env_db = envs.create_empty_env_db()
719 jenv_db = helpers.make_jenv_db()
720 with maas_env_detected(True):
721- views.env_index(
722- self.app, self.env_type_db, env_db, jenv_db,
723- self.save_callable)
724+ views.env_index(self.app, self.make_params(env_db, jenv_db))
725 buttons = self.get_widgets_in_contents(
726 filter_function=self.is_a(ui.MenuButton))
727 # The "create and bootstrap" button is the first one in the contents.
728@@ -433,9 +437,7 @@
729 env_db = envs.create_empty_env_db()
730 jenv_db = helpers.make_jenv_db()
731 with maas_env_detected(False):
732- views.env_index(
733- self.app, self.env_type_db, env_db, jenv_db,
734- self.save_callable)
735+ views.env_index(self.app, self.make_params(env_db, jenv_db))
736 buttons = self.get_widgets_in_contents(
737 filter_function=self.is_a(ui.MenuButton))
738 # No "create and bootstrap MAAS" buttons are present.
739@@ -446,8 +448,7 @@
740 # The default environment is already selected in the list.
741 env_db = helpers.make_env_db(default='lxc')
742 jenv_db = helpers.make_jenv_db()
743- views.env_index(
744- self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
745+ views.env_index(self.app, self.make_params(env_db, jenv_db))
746 env_data = envs.get_env_data(env_db, 'lxc')
747 env_description = envs.get_env_short_description(env_data)
748 contents = self.app.get_contents()
749@@ -460,8 +461,7 @@
750 # The status message explains how errors are displayed.
751 env_db = helpers.make_env_db()
752 jenv_db = {'environments': {}}
753- views.env_index(
754- self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
755+ views.env_index(self.app, self.make_params(env_db, jenv_db))
756 status = self.app.get_status()
757 self.assertEqual(self.base_status + ' \N{BULLET} has errors ', status)
758
759@@ -469,8 +469,7 @@
760 # The status message explains how default environment is represented.
761 env_db = helpers.make_env_db(default='lxc', exclude_invalid=True)
762 jenv_db = {'environments': {}}
763- views.env_index(
764- self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
765+ views.env_index(self.app, self.make_params(env_db, jenv_db))
766 status = self.app.get_status()
767 self.assertEqual(self.base_status + ' \N{CHECK MARK} default ', status)
768
769@@ -478,8 +477,7 @@
770 # The status message explains how active environments are displayed.
771 env_db = helpers.make_env_db(exclude_invalid=True)
772 jenv_db = helpers.make_jenv_db()
773- views.env_index(
774- self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
775+ views.env_index(self.app, self.make_params(env_db, jenv_db))
776 status = self.app.get_status()
777 self.assertEqual(self.base_status + ' \N{BULLET} active ', status)
778
779@@ -487,8 +485,7 @@
780 # The status message includes default, active and errors explanations.
781 env_db = helpers.make_env_db(default='lxc')
782 jenv_db = helpers.make_jenv_db()
783- views.env_index(
784- self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
785+ views.env_index(self.app, self.make_params(env_db, jenv_db))
786 status = self.app.get_status()
787 self.assertEqual(
788 self.base_status +
789@@ -501,8 +498,7 @@
790 # The status only includes navigation info if there are no errors.
791 env_db = helpers.make_env_db(exclude_invalid=True)
792 jenv_db = {'environments': {}}
793- views.env_index(
794- self.app, self.env_type_db, env_db, jenv_db, self.save_callable)
795+ views.env_index(self.app, self.make_params(env_db, jenv_db))
796 status = self.app.get_status()
797 self.assertEqual(self.base_status, status)
798
799@@ -516,9 +512,8 @@
800 def call_view(self, env_name='lxc'):
801 """Call the view passing the env_data corresponding to env_name."""
802 self.env_data = envs.get_env_data(self.env_db, env_name)
803- return views.env_detail(
804- self.app, self.env_type_db, self.env_db, self.jenv_db,
805- self.save_callable, self.env_data)
806+ self.params = self.make_params(self.env_db, self.jenv_db)
807+ return views.env_detail(self.app, self.params, self.env_data)
808
809 def test_view_default_return_value_on_exit(self):
810 # The view configures the app so that the return value on user exit is
811@@ -584,9 +579,7 @@
812 # The "back" button is the first one.
813 back_button = self.get_control_buttons()[0]
814 cli_helpers.emit(back_button)
815- mock_env_index.assert_called_once_with(
816- self.app, self.env_type_db, self.env_db, self.jenv_db,
817- self.save_callable)
818+ mock_env_index.assert_called_once_with(self.app, self.params)
819
820 def test_use_button(self):
821 # The application exits if the "use" button is clicked.
822@@ -608,14 +601,13 @@
823 # The "set default" button is the third one.
824 set_default_button = self.get_control_buttons()[2]
825 cli_helpers.emit(set_default_button)
826- # The index view has been called passing the modified env_db as third
827- # argument.
828+ # The index view has been called passing the modified env_db in params.
829 self.assertTrue(mock_env_index.called)
830- new_env_db = mock_env_index.call_args[0][2]
831+ params = mock_env_index.call_args[0][1]
832 # The new env_db has a new default.
833- self.assertEqual(new_env_db['default'], 'ec2-west')
834+ self.assertEqual(params.env_db['default'], 'ec2-west')
835 # The new env_db has been saved.
836- self.save_callable.assert_called_once_with(new_env_db)
837+ self.save_callable.assert_called_once_with(params.env_db)
838
839 @mock.patch('quickstart.cli.views.env_edit')
840 def test_edit_button(self, mock_env_edit):
841@@ -625,8 +617,7 @@
842 edit_button = self.get_control_buttons()[3]
843 cli_helpers.emit(edit_button)
844 mock_env_edit.assert_called_once_with(
845- self.app, self.env_type_db, self.env_db, self.jenv_db,
846- self.save_callable, self.env_data)
847+ self.app, self.params, self.env_data)
848
849 def test_remove_button(self):
850 # A confirmation dialog is displayed if the "remove" button is clicked.
851@@ -677,14 +668,13 @@
852 # The "confirm" button is the second one in the dialog.
853 confirm_button = buttons[1]
854 cli_helpers.emit(confirm_button)
855- # The index view has been called passing the modified env_db as third
856- # argument.
857+ # The index view has been called passing the modified env_db in params.
858 self.assertTrue(mock_env_index.called)
859- new_env_db = mock_env_index.call_args[0][2]
860+ params = mock_env_index.call_args[0][1]
861 # The new env_db no longer includes the "ec2-west" environment.
862- self.assertNotIn('ec2-west', new_env_db['environments'])
863+ self.assertNotIn('ec2-west', params.env_db['environments'])
864 # The new env_db has been saved.
865- self.save_callable.assert_called_once_with(new_env_db)
866+ self.save_callable.assert_called_once_with(params.env_db)
867
868 def test_status_with_errors(self):
869 # The status message explains how field errors are displayed.
870@@ -709,9 +699,8 @@
871 def call_view(self, env_name='lxc'):
872 """Call the view passing the env_data corresponding to env_name."""
873 self.env_data = envs.get_env_data(self.jenv_db, env_name)
874- return views.jenv_detail(
875- self.app, self.env_type_db, self.env_db, self.jenv_db,
876- self.save_callable, self.env_data)
877+ self.params = self.make_params(self.env_db, self.jenv_db)
878+ return views.jenv_detail(self.app, self.params, self.env_data)
879
880 def test_view_default_return_value_on_exit(self):
881 # The view configures the app so that the return value on user exit is
882@@ -756,9 +745,7 @@
883 # The "back" button is the first one.
884 back_button = self.get_control_buttons()[0]
885 cli_helpers.emit(back_button)
886- mock_env_index.assert_called_once_with(
887- self.app, self.env_type_db, self.env_db, self.jenv_db,
888- self.save_callable)
889+ mock_env_index.assert_called_once_with(self.app, self.params)
890
891 def test_use_button(self):
892 # The application exits if the "use" button is clicked.
893@@ -788,9 +775,8 @@
894 self.env_data = envs.get_env_data(self.env_db, env_name)
895 else:
896 self.env_data = {'type': env_type}
897- return views.env_edit(
898- self.app, self.env_type_db, self.env_db, self.jenv_db,
899- self.save_callable, self.env_data)
900+ self.params = self.make_params(self.env_db, self.jenv_db)
901+ return views.env_edit(self.app, self.params, self.env_data)
902
903 def get_form_contents(self):
904 """Return the form contents included in the app page.
905@@ -925,8 +911,7 @@
906 'ec2-west successfully modified', self.app.get_message())
907 # The application displays the environment detail view.
908 mock_env_detail.assert_called_once_with(
909- self.app, self.env_type_db, self.env_db, self.jenv_db,
910- self.save_callable, new_env_data)
911+ self.app, self.params, new_env_data)
912
913 @mock.patch('quickstart.cli.views.env_detail')
914 def test_save_empty_db(self, mock_env_detail):
915@@ -945,8 +930,7 @@
916 expected_new_env_data.update({'type': 'local', 'is-default': True})
917 envs.set_env_data(self.env_db, None, expected_new_env_data)
918 mock_env_detail.assert_called_once_with(
919- self.app, self.env_type_db, self.env_db, self.jenv_db,
920- self.save_callable, expected_new_env_data)
921+ self.app, self.params, expected_new_env_data)
922
923 def test_save_invalid_form_data(self):
924 # Errors are displayed if the user tries to save invalid data.
925@@ -990,9 +974,7 @@
926 # The "cancel" button is the second one.
927 cancel_button = self.get_control_buttons()[1]
928 cli_helpers.emit(cancel_button)
929- mock_env_index.assert_called_once_with(
930- self.app, self.env_type_db, self.env_db, self.jenv_db,
931- self.save_callable)
932+ mock_env_index.assert_called_once_with(self.app, self.params)
933
934 @mock.patch('quickstart.cli.views.env_detail')
935 def test_modification_view_cancel_button(self, mock_env_detail):
936@@ -1003,5 +985,4 @@
937 cancel_button = self.get_control_buttons()[1]
938 cli_helpers.emit(cancel_button)
939 mock_env_detail.assert_called_once_with(
940- self.app, self.env_type_db, self.env_db, self.jenv_db,
941- self.save_callable, self.env_data)
942+ self.app, self.params, self.env_data)
943
944=== modified file 'quickstart/tests/test_manage.py'
945--- quickstart/tests/test_manage.py 2014-12-16 16:52:49 +0000
946+++ quickstart/tests/test_manage.py 2015-01-07 16:46:42 +0000
947@@ -35,7 +35,10 @@
948 manage,
949 settings,
950 )
951-from quickstart.cli import views
952+from quickstart.cli import (
953+ params,
954+ views,
955+)
956 from quickstart.models import (
957 envs,
958 jenv,
959@@ -391,9 +394,13 @@
960 with mock.patch('quickstart.manage.views.show', mock_show):
961 yield
962 mock_save_callable.assert_called_once_with(self.parser, self.env_file)
963- mock_show.assert_called_once_with(
964- views.env_index, self.env_type_db, env_db, jenv_db,
965- mock_save_callable())
966+ expected_params = params.Params(
967+ env_type_db=self.env_type_db,
968+ env_db=env_db,
969+ jenv_db=jenv_db,
970+ save_callable=mock_save_callable(),
971+ )
972+ mock_show.assert_called_once_with(views.env_index, expected_params)
973
974 def test_resulting_env_data(self):
975 # The interactive session can be used to select an environment, in

Subscribers

People subscribed via source and target branches