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
1=== modified file 'quickstart/models/envs.py'
2--- quickstart/models/envs.py 2013-12-16 17:07:07 +0000
3+++ quickstart/models/envs.py 2013-12-17 16:26:53 +0000
4@@ -448,10 +448,9 @@
5 (field, data.pop(field.name, None))
6 for field in env_metadata['fields']
7 ]
8- # Add the remaining (unexpected) pairs using the base field type.
9- help = 'this field is unrecognized and can be safely removed'
10+ # Add the remaining (unexpected) pairs using the unexpected field type.
11 field_value_pairs.extend(
12- (fields.Field(key, help=help, required=False), value)
13+ (fields.UnexpectedField(key), value)
14 for key, value in data.items()
15 )
16 return field_value_pairs
17
18=== modified file 'quickstart/models/fields.py'
19--- quickstart/models/fields.py 2013-12-16 11:52:31 +0000
20+++ quickstart/models/fields.py 2013-12-17 16:26:53 +0000
21@@ -223,6 +223,47 @@
22 raise ValueError(msg.encode('utf-8'))
23
24
25+class UnexpectedField(Field):
26+ """An unexpected field, used when the value type is unknown."""
27+
28+ def __init__(self, name, label=None, help=None):
29+ if help is None:
30+ help = 'this field is unrecognized and can be safely removed'
31+ super(UnexpectedField, self).__init__(
32+ name, label=label, help=help,
33+ default=None, required=False, readonly=False)
34+
35+ def normalize(self, value):
36+ """Try to guess the value type."""
37+ if isinstance(value, (bool, int, type(None))):
38+ # For booleans, numbers, and None values, return the value as is.
39+ return value
40+ if isinstance(value, unicode):
41+ # If the value is a string (e.g. because it is returned as a
42+ # string by view code), try to guess the underlying type.
43+ value = value.strip()
44+ # Check if the value is not set and can be discarded.
45+ if not value:
46+ return None
47+ # Check if the value is a number.
48+ if value.isdigit():
49+ return int(value)
50+ # Check if the value is a boolean.
51+ lower_value = value.lower()
52+ if lower_value == 'true':
53+ return True
54+ if lower_value == 'false':
55+ return False
56+ # The value is a string, return it.
57+ return value
58+ # If the above did not work, always turn the value into a string.
59+ return unicode(value)
60+
61+ def validate(self, value):
62+ """Unexpected values are always valid."""
63+ pass
64+
65+
66 class ChoiceField(StringField):
67 """A string field whose value must be included in the given choices."""
68
69
70=== modified file 'quickstart/tests/models/test_envs.py'
71--- quickstart/tests/models/test_envs.py 2013-12-16 17:07:07 +0000
72+++ quickstart/tests/models/test_envs.py 2013-12-17 16:26:53 +0000
73@@ -645,6 +645,7 @@
74 self.assertEqual(len(unexpected_dict), len(remaining_pairs))
75 help = 'this field is unrecognized and can be safely removed'
76 for field, value in remaining_pairs:
77+ self.assertIsInstance(field, fields.UnexpectedField)
78 self.assertEqual(unexpected_dict[field.name], value, field.name)
79 self.assertFalse(field.required, field.name)
80 self.assertEqual(help, field.help, field.name)
81@@ -803,6 +804,30 @@
82 normalized_data = envs.normalize(self.env_metadata, env_data)
83 self.assertEqual(expected, normalized_data)
84
85+ def test_exclude_unexpected_fields(self):
86+ # The normalization process excludes unexpected fields if the
87+ # corresponding values are not set.
88+ env_data = {
89+ # Since this field is required, it is included even if not set.
90+ 'string-required': 'a string',
91+ # Since the value is set, this value is preserved even if the
92+ # field is not included in metadata.
93+ 'unexpected1': 'boo!',
94+ # False is also considered a valid value for an unexpected field.
95+ 'unexpected2': False,
96+ # Unexpected fields whose value is empty or None are excluded.
97+ 'unexpected3': '',
98+ 'unexpected4': '\t\n ',
99+ 'unexpected5': None,
100+ }
101+ expected = {
102+ 'string-required': 'a string',
103+ 'unexpected1': 'boo!',
104+ 'unexpected2': False,
105+ }
106+ normalized_data = envs.normalize(self.env_metadata, env_data)
107+ self.assertEqual(expected, normalized_data)
108+
109 def test_original_not_mutated(self):
110 # The original env_data is not modified in the process.
111 env_data = {
112
113=== modified file 'quickstart/tests/models/test_fields.py'
114--- quickstart/tests/models/test_fields.py 2013-12-16 12:11:15 +0000
115+++ quickstart/tests/models/test_fields.py 2013-12-17 16:26:53 +0000
116@@ -335,6 +335,78 @@
117 field.validate(None)
118
119
120+class TestUnexpectedField(FieldTestsMixin, unittest.TestCase):
121+
122+ field_class = fields.UnexpectedField
123+
124+ def assert_normalized(self, normalized_value, input_value):
125+ """Ensure the expected normalized value is returned."""
126+ field = self.field_class('last-name')
127+ self.assertEqual(normalized_value, field.normalize(input_value))
128+
129+ def test_attributes(self):
130+ # The field attributes are properly stored in the field instance.
131+ field = self.field_class(
132+ 'first-name', label='first name', help='your first name')
133+ self.assertEqual('first-name', field.name)
134+ self.assertEqual('first name', field.label)
135+ self.assertEqual('your first name', field.help)
136+
137+ def test_default_attributes(self):
138+ # Only the name identifier is required when instantiating a field.
139+ field = self.field_class('last-name')
140+ self.assertEqual('last-name', field.name)
141+ self.assertEqual('last-name', field.label)
142+ self.assertEqual(
143+ 'this field is unrecognized and can be safely removed', field.help)
144+ self.assertIsNone(field.default)
145+ self.assertFalse(field.required)
146+ self.assertFalse(field.readonly)
147+
148+ def test_normalize_boolean(self):
149+ # The normalized boolean value is the value itself.
150+ self.assert_normalized(True, True)
151+ self.assert_normalized(False, False)
152+
153+ def test_normalize_integer(self):
154+ # The normalized numeric value is the number itself.
155+ self.assert_normalized(42, 42)
156+
157+ def test_normalize_none(self):
158+ # None normalization returns None.
159+ self.assert_normalized(None, None)
160+
161+ def test_normalize_string_guess(self):
162+ # A string value is converted to the underlying type if possible.
163+ self.assert_normalized(42, '42')
164+ self.assert_normalized(47, ' 47 ')
165+ self.assert_normalized(True, 'true')
166+ self.assert_normalized(False, '\tFALSE\n')
167+
168+ def test_normalize_string(self):
169+ # The normalization process returns the stripped value if the value is
170+ # a string.
171+ self.assert_normalized('a string', 'a string')
172+ self.assert_normalized('stripped', '\n stripped\t')
173+
174+ def test_normalize_empty_string(self):
175+ # An empty string is normalized to None.
176+ self.assert_normalized(None, '')
177+ self.assert_normalized(None, ' ')
178+
179+ def test_normalize_unknown(self):
180+ # The normalization process converts to string unrecognized values.
181+ self.assert_normalized('42.47', 42.47)
182+ self.assert_normalized('{}', {})
183+
184+ def test_validate(self):
185+ # Unexpected values are always valid.
186+ field = self.field_class('last-name')
187+ for value in (None, 42, 42.47, True, False, 'a string'):
188+ with self.assert_not_raises(ValueError, value):
189+ field.validate(value)
190+
191+
192 class TestAutoGeneratedStringField(TestStringField):
193
194 field_class = fields.AutoGeneratedStringField

Subscribers

People subscribed via source and target branches