Merge lp:~frankban/juju-quickstart/env-manage-fields into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 33
Proposed branch: lp:~frankban/juju-quickstart/env-manage-fields
Merge into: lp:juju-quickstart
Diff against target: 657 lines (+625/-1)
4 files modified
quickstart/models/envs.py (+1/-1)
quickstart/models/fields.py (+280/-0)
quickstart/settings.py (+3/-0)
quickstart/tests/models/test_fields.py (+341/-0)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/env-manage-fields
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+198537@code.launchpad.net

Description of the change

Env management: fields.

See the module docstring for an
explanation of how those fields
will be used.

Tests: `make check`.
No QA required.

https://codereview.appspot.com/37820046/

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

Reviewers: mp+198537_code.launchpad.net,

Message:
Please take a look.

Description:
Env management: fields.

See the module docstring for an
explanation of how those fields
will be used.

Tests: `make check`.
No QA required.

https://code.launchpad.net/~frankban/juju-quickstart/env-manage-fields/+merge/198537

(do not edit description out of merge proposal)

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

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

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

The code LGTM with a few changes.

I am a bit confused as to where we're going with this work. It feels a
little like you're reinventing Zope. :) Let's not do that.

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py
File quickstart/models/fields.py (right):

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode63
quickstart/models/fields.py:63: - editable: True if the associated value
must be considered immutable.
This definition seems backwards. s/immutable/mutable/ ?

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode77
quickstart/models/fields.py:77: been previously validated.
Should there be a trap for that situation? A _validated flag?

https://codereview.appspot.com/37820046/

Revision history for this message
Gary Poster (gary) wrote :
Download full text (5.7 KiB)

LGTM and nice work, with consideration of various fairly trivial
comments.

I'm interested in bac's comments. Calling something Zope-like these
days is unfortunately not a complement. Perhaps he's referring to using
classes for fields? Certainly these fields could be data as well: your
classes really simply have some flags and some functions, and the
validation functions could be composed (more?) attractively
functionally, to my eyes. That said, while I am intrigued by the idea
of simplifying the fields into data, and think it might be a win, I
don't think it will be a big enough win to justify refactoring at this
time; and I think people with an OOP bent might not like the refactoring
at all.

Moreover, maybe that's not anywhere close to what bac means?

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py
File quickstart/models/fields.py (right):

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode24
quickstart/models/fields.py:24: each provider type is associated to a
sequence of fields. Those fields describe
...associated with a sequence of fields.

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode49
quickstart/models/fields.py:49: - autogenerated: indicates this is a
string field that can be
It strikes me that this functionality is along a different logical axis
than field type. I'd be tempted to have the code look for a generate
method, for instance, as a flag: if it exists, it can autogenerate (a
string, a number, whatever). I don't think this is very important: just
a suggestion.

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode53
quickstart/models/fields.py:53: edited using the usual input edit
widget.
Which effectively means multi-line string, right? Might as well be
explicit about it.

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode56
quickstart/models/fields.py:56: - name: the name identifying a specific
piece of information
Is this...

the key identifying a specific piece of information in the environments
data

...or not? Might be good to clarify, either way.

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode83
quickstart/models/fields.py:83: self, name, label=None, help='',
required=True, editable=True):
I generally prefer flags to default to false, but I think this is
probably the right compromise between clarity and convenience for
required and editable...unless we can choose different names?

optional=False, readonly=False

readonly conveys the idea well. Less sure about "optional"

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode84
quickstart/models/fields.py:84: """Initialize a field, only the name
identifier is required."""
Run-on sentence. Any of the following are better

Initialize a field. Only...
Initialize a field; only...

I prefer the period in this case.

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode100
quickstart/models/fields.py:100: if isinstance(value, str):
When would this happen? Or is this just insurance?

https://codereview.appspot.com/3782...

Read more...

Revision history for this message
Gary Poster (gary) wrote :

On 2013/12/11 14:15:21, gary.poster wrote:
> LGTM and nice work, with consideration of various fairly trivial
comments.

> I'm interested in bac's comments. Calling something Zope-like these
days is
> unfortunately not a complement.

s/complement/compliment/ :-P

https://codereview.appspot.com/37820046/

38. By Francesco Banconi

Changes as per review.

39. By Francesco Banconi

Gettng rid of the autogenerated field_type.

40. By Francesco Banconi

Some other changes as per review.

41. By Francesco Banconi

Changes as per review.

Revision history for this message
Francesco Banconi (frankban) wrote :
Download full text (5.7 KiB)

*** Submitted:

Env management: fields.

See the module docstring for an
explanation of how those fields
will be used.

Tests: `make check`.
No QA required.

R=bac, gary.poster
CC=
https://codereview.appspot.com/37820046

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py
File quickstart/models/fields.py (right):

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode24
quickstart/models/fields.py:24: each provider type is associated to a
sequence of fields. Those fields describe
On 2013/12/11 14:15:21, gary.poster wrote:
> ...associated with a sequence of fields.

Done.

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode49
quickstart/models/fields.py:49: - autogenerated: indicates this is a
string field that can be
On 2013/12/11 14:15:21, gary.poster wrote:
> It strikes me that this functionality is along a different logical
axis than
> field type. I'd be tempted to have the code look for a generate
method, for
> instance, as a flag: if it exists, it can autogenerate (a string, a
number,
> whatever). I don't think this is very important: just a suggestion.

Good idea, done.

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode53
quickstart/models/fields.py:53: edited using the usual input edit
widget.
On 2013/12/11 14:15:21, gary.poster wrote:
> Which effectively means multi-line string, right? Might as well be
explicit
> about it.

Right, fixed.

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode56
quickstart/models/fields.py:56: - name: the name identifying a specific
piece of information
On 2013/12/11 14:15:21, gary.poster wrote:
> Is this...

> the key identifying a specific piece of information in the
environments data

> ...or not? Might be good to clarify, either way.

Done.

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode63
quickstart/models/fields.py:63: - editable: True if the associated value
must be considered immutable.
On 2013/12/11 13:41:16, bac wrote:
> This definition seems backwards. s/immutable/mutable/ ?

Done.

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode77
quickstart/models/fields.py:77: been previously validated.
On 2013/12/11 13:41:16, bac wrote:
> Should there be a trap for that situation? A _validated flag?

Not sure. We don't have state in those fields, e.g.:
field = MyField('foo')
field.validate(42)
field.normalize(None)

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode83
quickstart/models/fields.py:83: self, name, label=None, help='',
required=True, editable=True):
On 2013/12/11 14:15:21, gary.poster wrote:
> I generally prefer flags to default to false, but I think this is
probably the
> right compromise between clarity and convenience for required and
> editable...unless we can choose different names?

> optional=False, readonly=False

> readonly conveys the idea well. Less sure about "optional"

As agreedd, we now use required=False, readonly=False.

https://codereview.appspot.com/37820046/diff/1/quickstart/models/fields.py#newcode84
quickstart/models/f...

Read more...

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

Thank you both for the great reviews!

https://codereview.appspot.com/37820046/

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-10 15:43:33 +0000
+++ quickstart/models/envs.py 2013-12-11 17:45:26 +0000
@@ -96,7 +96,7 @@
9696
97from quickstart import (97from quickstart import (
98 serializers,98 serializers,
99 utils99 utils,
100)100)
101101
102102
103103
=== added file 'quickstart/models/fields.py'
--- quickstart/models/fields.py 1970-01-01 00:00:00 +0000
+++ quickstart/models/fields.py 2013-12-11 17:45:26 +0000
@@ -0,0 +1,280 @@
1# This file is part of the Juju Quickstart Plugin, which lets users set up a
2# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
3# Copyright (C) 2013 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it under
6# the terms of the GNU Affero General Public License version 3, as published by
7# the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Juju Quickstart field definitions.
18
19A field is a simple object describing a value, e.g. a label or a help text to
20be associated to that value. A field also provides the logic to display,
21validate and normalize the given data.
22
23This module is useful as part of the environments metadata definition, in which
24each provider type is associated with a sequence of fields. Those fields
25describe how an environment of that type should look like, and provide a way to
26validate the whole environment on a per-field basis.
27
28See quickstart.models.envs.get_env_type_db for an example of how this works.
29XXX frankban 13-12-11:
30 the function above will be implemented in the next branch.
31"""
32
33from __future__ import unicode_literals
34
35import uuid
36
37
38class Field(object):
39 """Describe a piece of information.
40
41 Also provide the logic to display, normalize and validate input data.
42
43 Field subclasses can define a "field_type" class attribute that can be
44 used by view code to choose an appropriate widget to use for that type of
45 field instances, e.g. a "bool" field type indicates that a checkbox is
46 appropriate when editing that field value. The following field types are
47 defined by fields in this module:
48 - bool: as mentioned, values are expected to be boolean values;
49 If field_type is not specified, view code assumes the values can be
50 edited using the usual input edit widget which handles multi-line strings.
51
52 Field instances have the following attributes:
53 - name: the key identifying a specific piece of information. In the
54 environments context this can be, for instance, "admin-secret" or
55 "default-series";
56 - label: a human friendly string identifying this field
57 (e.g. "Admin Secret");
58 - help: help text associated with this field
59 (e.g. "the password you use for authenticating");
60 - required: True if this is a required field, False otherwise;
61 - readonly: True if the associated value must be considered immutable.
62
63 Field instances also expose the following methods:
64 - display(value): how the value should be displayed by views
65 (usually just the value itself as a unicode string is returned);
66 - normalize(value): return the normalized value, e.g. a string field
67 might return a stripped version of the input value;
68 - validate(value): validate the given value, raising a ValueError if
69 the input value is not valid, returning None otherwise;
70 - generate(): this optional method indicates the value associated with
71 this field can be optionally automatically generated by view code.
72 When implemented, this method must return a suitable generated value.
73
74 Note that it is not safe to call normalize on a value if that value has not
75 been previously validated.
76 """
77
78 # Since this is the default field the type is not specified.
79 field_type = None
80
81 def __init__(
82 self, name, label=None, help='', required=False, readonly=False):
83 """Initialize a field. Only the name identifier is required."""
84 self.name = name
85 self.label = name if label is None else label
86 self.help = help
87 self.required = required
88 self.readonly = readonly
89
90 def __repr__(self):
91 name = self.name.encode('utf-8')
92 return b'<{}: {}>'.format(self.__class__.__name__, name)
93
94 def display(self, value):
95 """Return a value to display.
96
97 Override this method to change how the value is displayed in view code.
98 """
99 return unicode(value)
100
101 def normalize(self, value):
102 """Return a normalized version of the given value."""
103 return value
104
105 def validate(self, value):
106 """Validate the given value.
107
108 Return a possibly normalized version of the value.
109 Raise a ValueError if the given value is not valid.
110 """
111 if self.required and not value:
112 msg = 'a value is required for the {} field'.format(self.label)
113 raise ValueError(msg.encode('utf-8'))
114
115
116class StringField(Field):
117 """Values associated with this field must be strings."""
118
119 def normalize(self, value):
120 """Strip the string."""
121 if value is None:
122 return ''
123 return value.strip()
124
125 def validate(self, value):
126 """Check that the value is a string."""
127 if not isinstance(value, (unicode, type(None))):
128 # Assume view code always works with unicode strings.
129 msg = 'the {} field requires a string value'.format(self.label)
130 raise ValueError(msg.encode('utf-8'))
131 value = self.normalize(value)
132 # The parent field ensures the value is set if required.
133 super(StringField, self).validate(value)
134
135
136class IntField(Field):
137 """Values associated with this field must be integers."""
138
139 def __init__(self, name, min_value=None, max_value=None, **kwargs):
140 """Initialize an integer field.
141
142 The "min_value" and "max_value" keyword arguments, if provided, are
143 used in the validation process.
144 """
145 super(IntField, self).__init__(name, **kwargs)
146 self.min_value = min_value
147 self.max_value = max_value
148
149 def normalize(self, value):
150 """Return the value as an integer.
151
152 Return None if the value is an empty string or None. In these cases,
153 the field value is considered not set.
154 """
155 if isinstance(value, unicode):
156 value = value.strip()
157 if value in ('', None):
158 return None
159 return int(value)
160
161 def validate(self, value):
162 """Validate the integer value.
163
164 Raise a ValueError if:
165 - the normalized value is None but the value is required;
166 - the normalized field is set but it is not an integer number;
167 - the normalized field is a number but not in the range defined
168 by self.min_value and self.max_value.
169 """
170 label = self.label
171 # Ensure the value, if set, is an integer.
172 msg = 'the {} field requires an integer value'.format(label)
173 # Avoid implicit boolean to integer conversion.
174 if isinstance(value, bool):
175 raise ValueError(msg.encode('utf-8'))
176 try:
177 value = self.normalize(value)
178 except (TypeError, ValueError):
179 raise ValueError(msg.encode('utf-8'))
180 # Ensure the value is set if required.
181 if value is None:
182 if self.required:
183 msg = 'a value is required for the {} field'.format(self.label)
184 raise ValueError(msg.encode('utf-8'))
185 return value
186 # Ensure the value is in the given range.
187 min_value = self.min_value
188 max_value = self.max_value
189 if (min_value is not None) and (max_value is not None):
190 if not (min_value <= value <= max_value):
191 msg = 'the {} value must be in the {}-{} range'.format(
192 label, min_value, max_value)
193 raise ValueError(msg.encode('utf-8'))
194 elif min_value is not None:
195 if value < min_value:
196 msg = 'the {} value must be >= {}'.format(label, min_value)
197 raise ValueError(msg.encode('utf-8'))
198 elif max_value is not None:
199 if value > max_value:
200 msg = 'the {} value must be <= {}'.format(label, max_value)
201 raise ValueError(msg.encode('utf-8'))
202
203
204class BoolField(Field):
205 """Values associated with this field must be booleans."""
206
207 field_type = 'bool'
208
209 def __init__(self, name, default=False, **kwargs):
210 """Initialize the boolean field.
211
212 It is possible to pass a "default" keyword argument in order to specify
213 the default value (True or False) to use if the value is unset (None).
214 """
215 super(BoolField, self).__init__(name, **kwargs)
216 self.default = default
217
218 def normalize(self, value):
219 """Return the default value if the given one is None (unset)."""
220 if value is None:
221 value = self.default
222 return value
223
224 def validate(self, value):
225 """Check that the value is a boolean."""
226 value = self.normalize(value)
227 if not isinstance(value, bool):
228 msg = 'the {} field requires a boolean value'.format(self.label)
229 raise ValueError(msg.encode('utf-8'))
230
231
232class AutoGeneratedStringField(StringField):
233 """Can automatically generate string values if they are not provided.
234
235 Subclasses can override the generate method to return customized values.
236 """
237
238 def generate(self):
239 """Generate a uuid valid value."""
240 return '{}-{}'.format(self.name[:3], uuid.uuid4().hex)
241
242
243class ChoiceField(StringField):
244 """A string field whose value must be included in the given choices."""
245
246 def __init__(self, name, choices=(), **kwargs):
247 """Initialize the choices field with the given choices."""
248 super(ChoiceField, self).__init__(name, **kwargs)
249 self.choices = tuple(choices)
250
251 def validate(self, value):
252 """Check the field is set if required.
253
254 If the field is set, also check it is included in self.choices.
255 """
256 # The parent field ensures the value is set if required.
257 super(ChoiceField, self).validate(value)
258 value = self.normalize(value)
259 choices = list(self.choices)
260 if not self.required:
261 choices.append('')
262 if value not in choices:
263 msg = 'the {} requires the value to be one of the following: {}'
264 raise ValueError(
265 msg.format(self.label, ', '.join(self.choices)).encode('utf-8')
266 )
267
268
269class PasswordField(StringField):
270 """Assume values associated with this field represent sensible data."""
271
272 def display(self, value):
273 """Obfuscate the value."""
274 if value:
275 return '*****'
276 return 'None'
277
278
279class AutoGeneratedPasswordField(AutoGeneratedStringField, PasswordField):
280 """Values are passwords which can be automatically generated."""
0281
=== modified file 'quickstart/settings.py'
--- quickstart/settings.py 2013-12-06 10:07:40 +0000
+++ quickstart/settings.py 2013-12-11 17:45:26 +0000
@@ -31,6 +31,9 @@
31# The quickstart app short description.31# The quickstart app short description.
32DESCRIPTION = 'set up a Juju environment (including the GUI) in very few steps'32DESCRIPTION = 'set up a Juju environment (including the GUI) in very few steps'
3333
34# The possible values for the environments.yaml default-series field.
35JUJU_DEFAULT_SERIES = ('precise', 'quantal', 'raring', 'saucy')
36
34# Retrieve the current juju-core home.37# Retrieve the current juju-core home.
35JUJU_HOME = os.getenv('JUJU_HOME', '~/.juju')38JUJU_HOME = os.getenv('JUJU_HOME', '~/.juju')
3639
3740
=== added file 'quickstart/tests/models/test_fields.py'
--- quickstart/tests/models/test_fields.py 1970-01-01 00:00:00 +0000
+++ quickstart/tests/models/test_fields.py 2013-12-11 17:45:26 +0000
@@ -0,0 +1,341 @@
1# This file is part of the Juju Quickstart Plugin, which lets users set up a
2# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
3# Copyright (C) 2013 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it under
6# the terms of the GNU Affero General Public License version 3, as published by
7# the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Tests for the Juju Quickstart field definitions."""
18
19from __future__ import unicode_literals
20
21from contextlib import contextmanager
22import unittest
23
24from quickstart.models import fields
25from quickstart.tests import helpers
26
27
28class FieldTestsMixin(object):
29 """Define a collection of tests shared by all fields.
30
31 Subclasses must define a field_class attribute.
32 """
33
34 @contextmanager
35 def assert_not_raises(self, exception, message=None):
36 """Ensure the given exception is not raised in the code block."""
37 try:
38 yield
39 except exception as err:
40 msg = b'unexpected {}: {}'.format(err.__class__.__name__, err)
41 if message:
42 msg += b' ({!r})'.format(message)
43 self.fail(msg)
44
45 def test_attributes(self):
46 # The field attributes are properly stored in the field instance.
47 field = self.field_class(
48 'first-name', label='first name', help='your first name',
49 required=True, readonly=False)
50 self.assertEqual('first-name', field.name)
51 self.assertEqual('first name', field.label)
52 self.assertEqual('your first name', field.help)
53 self.assertTrue(field.required)
54 self.assertFalse(field.readonly)
55
56 def test_default_attributes(self):
57 # Only the name identifier is required when instantiating a field.
58 field = self.field_class('last-name')
59 self.assertEqual('last-name', field.name)
60 self.assertEqual('last-name', field.label)
61 self.assertEqual('', field.help)
62 self.assertFalse(field.required)
63 self.assertFalse(field.readonly)
64
65 def test_field_representation(self):
66 # A field object is properly represented.
67 field = self.field_class('email')
68 expected = b'<{}: email>'.format(self.field_class.__name__)
69 self.assertEqual(expected, repr(field))
70
71 def test_display(self):
72 # A field is able to display values.
73 field = self.field_class('phone-number')
74 for value in (None, 42, True, 'a unicode string'):
75 self.assertEqual(unicode(value), field.display(value), value)
76
77
78class TestField(
79 FieldTestsMixin, helpers.ValueErrorTestsMixin, unittest.TestCase):
80
81 field_class = fields.Field
82
83 def test_normalization(self):
84 # The base field normalization is a no-op.
85 field = self.field_class('email')
86 for value in (None, 42, True, 'a unicode string'):
87 self.assertEqual(value, field.normalize(value), value)
88
89 def test_validation_success(self):
90 # The validation succeeds if the value is set.
91 field = self.field_class('email')
92 for value in (42, True, 'a unicode string', ' '):
93 self.assertIsNone(field.validate(value), value)
94
95 def test_validation_not_required(self):
96 # If the field is not required no errors are raised.
97 field = self.field_class('email', required=False)
98 for value in ('', False, None):
99 with self.assert_not_raises(ValueError, value):
100 field.validate(value)
101
102 def test_validation_error_required(self):
103 # A ValueError is raised by required fields if the value is not set.
104 field = self.field_class('email', label='email address', required=True)
105 expected = 'a value is required for the email address field'
106 for value in ('', False, None):
107 with self.assert_value_error(expected):
108 field.validate(value)
109
110
111class TestStringField(
112 FieldTestsMixin, helpers.ValueErrorTestsMixin, unittest.TestCase):
113
114 field_class = fields.StringField
115
116 def test_normalization(self):
117 # The string field normalization returns the stripped string value.
118 field = self.field_class('email')
119 for value in ('a value', '\t tabs and spaces ', ' ', 'newlines\n\n'):
120 self.assertEqual(value.strip(), field.normalize(value), value)
121
122 def test_none_normalization(self):
123 # The string field normalization turns None values into empty strings.
124 field = self.field_class('email')
125 self.assertEqual('', field.normalize(None))
126
127 def test_validation_success(self):
128 # The validation succeeds if the value is set.
129 field = self.field_class('email')
130 for value in ('a value', '\t tabs and spaces ', 'newlines\n\n'):
131 self.assertIsNone(field.validate(value), value)
132
133 def test_validation_not_required(self):
134 # If the field is not required no errors are raised.
135 field = self.field_class('email', required=False)
136 for value in ('', None, ' ', '\t\n'):
137 with self.assert_not_raises(ValueError, value):
138 field.validate(value)
139
140 def test_validation_error_required(self):
141 # A ValueError is raised by required fields if the value is not set.
142 field = self.field_class('email', label='email address', required=True)
143 expected = 'a value is required for the email address field'
144 for value in ('', None, ' ', '\t\n'):
145 with self.assert_value_error(expected):
146 field.validate(value)
147
148 def test_validation_error_not_a_string(self):
149 # A ValueError is raised by string fields if the value is not a string.
150 field = self.field_class('email', label='email address')
151 expected = 'the email address field requires a string value'
152 for value in (42, False, []):
153 with self.assert_value_error(expected):
154 field.validate(value)
155
156
157class TestIntField(
158 FieldTestsMixin, helpers.ValueErrorTestsMixin, unittest.TestCase):
159
160 field_class = fields.IntField
161
162 def test_normalization(self):
163 # The int field normalization returns the values as integers.
164 field = self.field_class('tcp-port')
165 for value in (42, 42.0, '42', '\t42 ', '42\n\n'):
166 self.assertEqual(42, field.normalize(value), value)
167
168 def test_none_normalization(self):
169 # The int field normalization returns None if the value is not set.
170 field = self.field_class('tcp-port')
171 for value in (None, '', ' ', '\t\n'):
172 self.assertIsNone(field.normalize(value), value)
173
174 def test_validation_success(self):
175 # The value as an integer number is returned if the value is valid.
176 field = self.field_class('tcp-port')
177 for value in (42, 42.0, '42', '\t42 ', '42\n\n'):
178 with self.assert_not_raises(ValueError, value):
179 field.validate(value)
180
181 def test_validation_success_zero(self):
182 # The zero value is not considered "unset".
183 field = self.field_class('tcp-port')
184 with self.assert_not_raises(ValueError):
185 field.validate(0)
186
187 def test_validation_success_in_range(self):
188 # The value as an integer number is returned if the value is valid and
189 # is in the specified range of min/max values.
190 field = self.field_class('tcp-port', min_value=42, max_value=47)
191 for value in (42, 42.0, '42', '\t42 ', '42\n\n'):
192 with self.assert_not_raises(ValueError, value):
193 field.validate(value)
194
195 def test_validation_not_required(self):
196 # If the field is not required no errors are raised.
197 field = self.field_class('tcp-port', required=False)
198 for value in ('', None, ' ', '\t\n'):
199 with self.assert_not_raises(ValueError, value):
200 self.assertIsNone(field.validate(value), value)
201
202 def test_validation_error_required(self):
203 # A ValueError is raised by required fields if the value is not set.
204 field = self.field_class('tcp-port', label='TCP port', required=True)
205 expected = 'a value is required for the TCP port field'
206 for value in ('', None, ' ', '\t\n'):
207 with self.assert_value_error(expected):
208 field.validate(value)
209
210 def test_validation_error_not_a_number(self):
211 # A ValueError is raised by int fields if the value is not a number.
212 field = self.field_class('tcp-port', label='TCP port')
213 expected = 'the TCP port field requires an integer value'
214 for value in ('a string', False, {}, []):
215 with self.assert_value_error(expected):
216 field.validate(value)
217
218 def test_validation_error_min_value(self):
219 # A ValueError is raised if value < min_value.
220 field = self.field_class('tcp-port', min_value=42, label='TCP port')
221 with self.assert_value_error('the TCP port value must be >= 42'):
222 field.validate(27)
223
224 def test_validation_error_max_value(self):
225 # A ValueError is raised if value > max_value.
226 field = self.field_class('tcp-port', max_value=42, label='TCP port')
227 with self.assert_value_error('the TCP port value must be <= 42'):
228 field.validate(47)
229
230 def test_validation_error_range(self):
231 # A ValueError is raised if not min_value <= value <= max_value.
232 field = self.field_class(
233 'tcp-port', min_value=42, max_value=47, label='TCP port')
234 expected = 'the TCP port value must be in the 42-47 range'
235 with self.assert_value_error(expected):
236 field.validate(27)
237
238
239class TestBoolField(
240 FieldTestsMixin, helpers.ValueErrorTestsMixin, unittest.TestCase):
241
242 field_class = fields.BoolField
243
244 def test_normalization(self):
245 # The bool field normalization returns the value itself.
246 field = self.field_class('is-public')
247 self.assertTrue(field.normalize(True))
248 self.assertFalse(field.normalize(False))
249
250 def test_none_normalization(self):
251 # The string field normalization turns None values into the default.
252 field_true = self.field_class('is-public', default=True)
253 field_false = self.field_class('is-private', default=False)
254 self.assertTrue(field_true.normalize(None))
255 self.assertFalse(field_false.normalize(None))
256
257 def test_validation_success(self):
258 # The validation succeeds if the value is boolean.
259 field = self.field_class('is-public')
260 with self.assert_not_raises(ValueError):
261 field.validate(True)
262 field.validate(False)
263
264 def test_validation_error_not_a_boolean(self):
265 # A ValueError is raised by string fields if the value is not a bool.
266 field = self.field_class('is-public', label='is public')
267 expected = 'the is public field requires a boolean value'
268 for value in (42, 'a string', []):
269 with self.assert_value_error(expected):
270 field.validate(value)
271
272
273class TestAutoGeneratedStringField(TestStringField):
274
275 field_class = fields.AutoGeneratedStringField
276
277 def test_generate(self):
278 # The autogenerated field can generate random values.
279 field = self.field_class('auto')
280 value1 = field.generate()
281 value2 = field.generate()
282 # The generated values are unicode strings.
283 self.assertIsInstance(value1, unicode)
284 self.assertIsInstance(value2, unicode)
285 # The generated values are not empty.
286 self.assertNotEqual(0, len(value1))
287 self.assertNotEqual(0, len(value2))
288 # The generated values are different to each other.
289 self.assertNotEqual(value1, value2)
290
291
292class TestChoiceField(TestStringField):
293
294 field_class = fields.ChoiceField
295 choices = ('these', 'are', 'the', 'voyages')
296
297 def test_validation_success(self):
298 # No errors are raised if the value is included in the choices.
299 field = self.field_class('word', choices=self.choices)
300 for value in self.choices:
301 with self.assert_not_raises(ValueError, value):
302 field.validate(value)
303
304 def test_validation_error_not_in_choices(self):
305 # A ValueError is raised by choice fields if the value is not included
306 # in the specified choices/
307 field = self.field_class(
308 'word', choices=self.choices, label='selected word')
309 expected = ('the selected word requires the value to be one of the '
310 'following: these, are, the, voyages')
311 with self.assert_value_error(expected):
312 field.validate('resistance is futile')
313
314
315class TestPasswordField(TestStringField):
316
317 field_class = fields.PasswordField
318
319 def test_display(self):
320 # A placeholder value is displayed.
321 field = self.field_class('passwd')
322 for value in (42, True, 'a unicode string'):
323 self.assertEqual('*****', field.display(value), value)
324
325 def test_display_bytes(self):
326 # A placeholder value is still displayed.
327 snowman = b'Here is a snowman\xc2\xa1: \xe2\x98\x83'
328 field = self.field_class('passwd')
329 self.assertEqual('*****', field.display(snowman))
330
331 def test_display_no_values(self):
332 # Do not display the placeholder if the value is not set.
333 field = self.field_class('passwd')
334 for value in (None, False, ''):
335 self.assertEqual('None', field.display(value), value)
336
337
338class TestAutoGeneratedPasswordField(
339 TestAutoGeneratedStringField, TestPasswordField):
340
341 field_class = fields.AutoGeneratedPasswordField

Subscribers

People subscribed via source and target branches