Merge lp:~frankban/juju-quickstart/improve-unexpected-fields into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 36
Proposed branch: lp:~frankban/juju-quickstart/improve-unexpected-fields
Merge into: lp:juju-quickstart
Diff against target: 194 lines (+140/-3)
4 files modified
quickstart/models/envs.py (+2/-3)
quickstart/models/fields.py (+41/-0)
quickstart/tests/models/test_envs.py (+25/-0)
quickstart/tests/models/test_fields.py (+72/-0)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/improve-unexpected-fields
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+199270@code.launchpad.net

Description of the change

Improve unexpected fields handling.

Implement the UnexpectedField type. This is used
to handle unexpected key/value pairs found in
the environments.yaml file. The user will be able
to get rid of those fields just deleting their
values from in the interactive session.

Tests: `make check`.
No QA.

https://codereview.appspot.com/43380043/

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

Reviewers: mp+199270_code.launchpad.net,

Message:
Please take a look.

Description:
Improve unexpected fields handling.

Implement the UnexpectedField type. This is used
to handle unexpected key/value pairs found in
the environments.yaml file. The user will be able
to get rid of those fields just deleting their
values from in the interactive session.

Tests: `make check`.
No QA.

https://code.launchpad.net/~frankban/juju-quickstart/improve-unexpected-fields/+merge/199270

(do not edit description out of merge proposal)

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

Affected files (+142, -3 lines):
   A [revision details]
   M quickstart/models/envs.py
   M quickstart/models/fields.py
   M quickstart/tests/models/test_envs.py
   M quickstart/tests/models/test_fields.py

39. By Francesco Banconi

Merged trunk.

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

*** Submitted:

Improve unexpected fields handling.

Implement the UnexpectedField type. This is used
to handle unexpected key/value pairs found in
the environments.yaml file. The user will be able
to get rid of those fields just deleting their
values from in the interactive session.

Tests: `make check`.
No QA.

R=matthew.scott
CC=
https://codereview.appspot.com/43380043

https://codereview.appspot.com/43380043/

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
=== modified file 'quickstart/models/envs.py'
--- quickstart/models/envs.py 2013-12-16 17:07:07 +0000
+++ quickstart/models/envs.py 2013-12-17 16:26:53 +0000
@@ -448,10 +448,9 @@
448 (field, data.pop(field.name, None))448 (field, data.pop(field.name, None))
449 for field in env_metadata['fields']449 for field in env_metadata['fields']
450 ]450 ]
451 # Add the remaining (unexpected) pairs using the base field type.451 # Add the remaining (unexpected) pairs using the unexpected field type.
452 help = 'this field is unrecognized and can be safely removed'
453 field_value_pairs.extend(452 field_value_pairs.extend(
454 (fields.Field(key, help=help, required=False), value)453 (fields.UnexpectedField(key), value)
455 for key, value in data.items()454 for key, value in data.items()
456 )455 )
457 return field_value_pairs456 return field_value_pairs
458457
=== modified file 'quickstart/models/fields.py'
--- quickstart/models/fields.py 2013-12-16 11:52:31 +0000
+++ quickstart/models/fields.py 2013-12-17 16:26:53 +0000
@@ -223,6 +223,47 @@
223 raise ValueError(msg.encode('utf-8'))223 raise ValueError(msg.encode('utf-8'))
224224
225225
226class UnexpectedField(Field):
227 """An unexpected field, used when the value type is unknown."""
228
229 def __init__(self, name, label=None, help=None):
230 if help is None:
231 help = 'this field is unrecognized and can be safely removed'
232 super(UnexpectedField, self).__init__(
233 name, label=label, help=help,
234 default=None, required=False, readonly=False)
235
236 def normalize(self, value):
237 """Try to guess the value type."""
238 if isinstance(value, (bool, int, type(None))):
239 # For booleans, numbers, and None values, return the value as is.
240 return value
241 if isinstance(value, unicode):
242 # If the value is a string (e.g. because it is returned as a
243 # string by view code), try to guess the underlying type.
244 value = value.strip()
245 # Check if the value is not set and can be discarded.
246 if not value:
247 return None
248 # Check if the value is a number.
249 if value.isdigit():
250 return int(value)
251 # Check if the value is a boolean.
252 lower_value = value.lower()
253 if lower_value == 'true':
254 return True
255 if lower_value == 'false':
256 return False
257 # The value is a string, return it.
258 return value
259 # If the above did not work, always turn the value into a string.
260 return unicode(value)
261
262 def validate(self, value):
263 """Unexpected values are always valid."""
264 pass
265
266
226class ChoiceField(StringField):267class ChoiceField(StringField):
227 """A string field whose value must be included in the given choices."""268 """A string field whose value must be included in the given choices."""
228269
229270
=== modified file 'quickstart/tests/models/test_envs.py'
--- quickstart/tests/models/test_envs.py 2013-12-16 17:07:07 +0000
+++ quickstart/tests/models/test_envs.py 2013-12-17 16:26:53 +0000
@@ -645,6 +645,7 @@
645 self.assertEqual(len(unexpected_dict), len(remaining_pairs))645 self.assertEqual(len(unexpected_dict), len(remaining_pairs))
646 help = 'this field is unrecognized and can be safely removed'646 help = 'this field is unrecognized and can be safely removed'
647 for field, value in remaining_pairs:647 for field, value in remaining_pairs:
648 self.assertIsInstance(field, fields.UnexpectedField)
648 self.assertEqual(unexpected_dict[field.name], value, field.name)649 self.assertEqual(unexpected_dict[field.name], value, field.name)
649 self.assertFalse(field.required, field.name)650 self.assertFalse(field.required, field.name)
650 self.assertEqual(help, field.help, field.name)651 self.assertEqual(help, field.help, field.name)
@@ -803,6 +804,30 @@
803 normalized_data = envs.normalize(self.env_metadata, env_data)804 normalized_data = envs.normalize(self.env_metadata, env_data)
804 self.assertEqual(expected, normalized_data)805 self.assertEqual(expected, normalized_data)
805806
807 def test_exclude_unexpected_fields(self):
808 # The normalization process excludes unexpected fields if the
809 # corresponding values are not set.
810 env_data = {
811 # Since this field is required, it is included even if not set.
812 'string-required': 'a string',
813 # Since the value is set, this value is preserved even if the
814 # field is not included in metadata.
815 'unexpected1': 'boo!',
816 # False is also considered a valid value for an unexpected field.
817 'unexpected2': False,
818 # Unexpected fields whose value is empty or None are excluded.
819 'unexpected3': '',
820 'unexpected4': '\t\n ',
821 'unexpected5': None,
822 }
823 expected = {
824 'string-required': 'a string',
825 'unexpected1': 'boo!',
826 'unexpected2': False,
827 }
828 normalized_data = envs.normalize(self.env_metadata, env_data)
829 self.assertEqual(expected, normalized_data)
830
806 def test_original_not_mutated(self):831 def test_original_not_mutated(self):
807 # The original env_data is not modified in the process.832 # The original env_data is not modified in the process.
808 env_data = {833 env_data = {
809834
=== modified file 'quickstart/tests/models/test_fields.py'
--- quickstart/tests/models/test_fields.py 2013-12-16 12:11:15 +0000
+++ quickstart/tests/models/test_fields.py 2013-12-17 16:26:53 +0000
@@ -335,6 +335,78 @@
335 field.validate(None)335 field.validate(None)
336336
337337
338class TestUnexpectedField(FieldTestsMixin, unittest.TestCase):
339
340 field_class = fields.UnexpectedField
341
342 def assert_normalized(self, normalized_value, input_value):
343 """Ensure the expected normalized value is returned."""
344 field = self.field_class('last-name')
345 self.assertEqual(normalized_value, field.normalize(input_value))
346
347 def test_attributes(self):
348 # The field attributes are properly stored in the field instance.
349 field = self.field_class(
350 'first-name', label='first name', help='your first name')
351 self.assertEqual('first-name', field.name)
352 self.assertEqual('first name', field.label)
353 self.assertEqual('your first name', field.help)
354
355 def test_default_attributes(self):
356 # Only the name identifier is required when instantiating a field.
357 field = self.field_class('last-name')
358 self.assertEqual('last-name', field.name)
359 self.assertEqual('last-name', field.label)
360 self.assertEqual(
361 'this field is unrecognized and can be safely removed', field.help)
362 self.assertIsNone(field.default)
363 self.assertFalse(field.required)
364 self.assertFalse(field.readonly)
365
366 def test_normalize_boolean(self):
367 # The normalized boolean value is the value itself.
368 self.assert_normalized(True, True)
369 self.assert_normalized(False, False)
370
371 def test_normalize_integer(self):
372 # The normalized numeric value is the number itself.
373 self.assert_normalized(42, 42)
374
375 def test_normalize_none(self):
376 # None normalization returns None.
377 self.assert_normalized(None, None)
378
379 def test_normalize_string_guess(self):
380 # A string value is converted to the underlying type if possible.
381 self.assert_normalized(42, '42')
382 self.assert_normalized(47, ' 47 ')
383 self.assert_normalized(True, 'true')
384 self.assert_normalized(False, '\tFALSE\n')
385
386 def test_normalize_string(self):
387 # The normalization process returns the stripped value if the value is
388 # a string.
389 self.assert_normalized('a string', 'a string')
390 self.assert_normalized('stripped', '\n stripped\t')
391
392 def test_normalize_empty_string(self):
393 # An empty string is normalized to None.
394 self.assert_normalized(None, '')
395 self.assert_normalized(None, ' ')
396
397 def test_normalize_unknown(self):
398 # The normalization process converts to string unrecognized values.
399 self.assert_normalized('42.47', 42.47)
400 self.assert_normalized('{}', {})
401
402 def test_validate(self):
403 # Unexpected values are always valid.
404 field = self.field_class('last-name')
405 for value in (None, 42, 42.47, True, False, 'a string'):
406 with self.assert_not_raises(ValueError, value):
407 field.validate(value)
408
409
338class TestAutoGeneratedStringField(TestStringField):410class TestAutoGeneratedStringField(TestStringField):
339411
340 field_class = fields.AutoGeneratedStringField412 field_class = fields.AutoGeneratedStringField

Subscribers

People subscribed via source and target branches