1 # vim: set et sts=4 sw=4 encoding=utf-8 :
3 r"""Validated classes constructed with declarative style.
5 This is a black magic module to ease the creation of classes that get
6 their attributes validated using formencode validators at creation time
7 (providing a constructor) and when updating (via a provided update
10 The important classes of this module are Field and ValidatedClass. When
11 you'd like a class to validate their attributes, just inherit from
12 ValidatedClass and declare all the attributes it will have as class
13 attributes using Field instances. For example:
16 >>> class Test(ValidatedClass):
17 >>> name = Field(formencode.validators.String(not_empty=True))
18 >>> age = Field(formencode.validators.Int(max=110, if_empty=None,
20 >>> # Some global validation after individual fields validation
21 >>> # (use pre_validator to validate *before* the individual fields
23 >>> def chained_validator(self, fields, state):
24 >>> if 'Jr' in fields['name'] and fields['age'] > 25:
25 >>> raise formencode.Invalid(u"Junior can't be older than 25 years",
26 >>> fields, state, error_dict=dict(
27 >>> age='Should not be more than 25'))
29 >>> # Will fail because 'name' is mandatory
31 >>> assert 'It should raised' is False
32 >>> except formencode.Invalid, e:
33 >>> print unicode(e), e.error_dict
34 >>> t = Test(name='Graham') # Keyword arguments are valid
35 >>> assert t.name == 'Graham'
36 >>> t = Test('Graham') # But can be used without keywords too!
37 >>> # Use the order of fields declaration
38 >>> assert t.name == 'Graham'
39 >>> t = Test('Graham', 20)
40 >>> assert t.name == 'Graham' and t.age == 20
41 >>> t.update('Graham Jr.') # An update method is provided
42 >>> assert t.name == 'Graham Jr.'
43 >>> t.update(age=18) # And accepts keyword arguments too
44 >>> assert t.age == 18
45 >>> # Updates are validated
47 >>> # Will fail because Junior can't be older than 25 years
49 >>> assert 'It should raised' is False
50 >>> except formencode.Invalid, e:
51 >>> print unicode(e), e.error_dict
52 >>> # Other operations are not
54 >>> assert t.age == 50
55 >>> # But you can use an empty update to validate
57 >>> # Will fail because Junior can't be older than 25 years
59 >>> assert 'It should raised' is False
60 >>> except formencode.Invalid, e:
61 >>> print unicode(e), e.error_dict
62 >>> # You can use the alias validate() too
64 >>> # Will fail because Junior can't be older than 25 years
66 >>> assert 'It should raised' is False
67 >>> except formencode.Invalid, e:
68 >>> print unicode(e), e.error_dict
73 __all__ = ('Field', 'ValidatedClass')
75 from formencode import Invalid
76 from formencode.schema import Schema
77 from formencode.validators import FancyValidator, OneOf, CIDR, Int
79 # FIXME not thread safe (use threadlocal?)
80 # This is a counter to preserve the order of declaration of fields (class
81 # attributes). When a new Field is instantiated, the Field stores internally
82 # the current counter value and increments the counter, so then, when doing
83 # the metaclass magic, you can know the order in which the fields were declared
87 r"""Field(validator[, doc]) -> Field object
89 This is a object used to declare class attributes that will be validated.
90 The only purpose of this class is declaration. After a Field is declared,
91 the metaclass process it and remove it, leaving the attributes as regular
94 validator - A field validator. You can use any formencode validator.
95 doc - Document string (not used yet)
97 See module documentation for examples of usage.
99 def __init__(self, validator, doc=None):
100 r"Initialize the object, see the class documentation for details."
101 self.validator = validator
103 # Set and update the declarative counter
104 global declarative_count
105 self._declarative_count = declarative_count
106 declarative_count += 1
108 class ValidatedMetaclass(type):
109 r"""ValidatedMetaclass(classname, bases, class_dict) -> type
111 This metaclass does the magic behind the scenes. It inspects the class
112 for Field instances, using them to build a validator schema and replacing
113 them with regular objects (None by default). It looks for pre_validator
114 and chained_validator attributes (assuming they are methods), and builds
115 a simple FancyValidator to add them as pre and chained validators to the
118 This metaclass add this attributes to the class:
119 class_validator - Schema validator for the class
120 validated_fields - Tuple of declared class fields (preserving order)
122 And remove this attributes (if present):
123 pre_validator - Provided pre validator, added to the class_validator
124 chained_validator - Provided chained validator, added too
126 This metaclass should be used indirectly inheriting from ValidatedClass.
128 def __new__(meta, classname, bases, class_dict):
129 # Reset the declarative_count so we can order again the fields
130 # (this is not extrictly necessary, it's just to avoid the counter
132 global declarative_count
133 declarative_count = 0
134 # List of attributes that are Fields
135 fields = [(k, v) for k, v in class_dict.items() if isinstance(v, Field)]
136 # Sort them according to the declarative counter
137 fields.sort(key=lambda i: i[1]._declarative_count)
138 # Validated fields to preserve field order for constructor
139 validated_fields = list()
140 # Create a new validator schema for the new class
142 for name, field in fields:
143 validated_fields.append(name)
144 # We don't want the class attribute to be a Field
145 class_dict[name] = None
146 # But we want its validator to go into the schema
147 schema.add_field(name, field.validator)
148 # Check if the class has a pre and/or chained validators to check if
149 # the class is valid as a whole before/after (respectively) validating
150 # each individual field
151 for key, add_to_schema in (('pre_validator', schema.add_pre_validator),
152 ('chained_validator', schema.add_chained_validator)):
153 if key in class_dict:
154 # Create a simple fancy validator
155 class Validator(FancyValidator):
156 validate_python = class_dict[key]
157 # And add it to the schema's special validators
158 add_to_schema(Validator)
159 # We don't need the validator in the class anymore
161 # Now we add the special new attributes to the new class
162 class_dict['validated_fields'] = tuple(validated_fields)
163 class_dict['class_validator'] = schema
164 return type.__new__(meta, classname, bases, class_dict)
166 def join_args(args, names, kwargs):
167 r"""join_args(args, names, kwargs) -> dict
169 This is a helper function to join positional arguments ('args') to keyword
170 arguments ('kwargs'). This is done using the 'names' list, which maps
171 positional arguments indexes to keywords. It *modifies* kwargs to add the
172 positional arguments using the mapped keywords (and checking for
173 duplicates). The number of argument passed is checked too (it should't be
174 greater than len(names). Extra keywords are not checked though, because it
175 assumes the validator schema takes care of that.
177 args - Positional arguments.
178 names - list of keywords.
179 kwargs - Keywords arguments.
181 if len(args) > len(names):
182 raise Invalid('Too many arguments', args, None)
183 for i in range(len(args)):
184 if names[i] in kwargs:
185 raise Invalid("Duplicated value for argument '%s'" % names[i],
186 kwargs[names[i]], None)
187 kwargs[names[i]] = args[i]
190 class ValidatedClass(object):
191 r"""ValidatedClass(*args, **kw) -> ValidatedClass object
193 You should inherit your classes from this one, declaring the class
194 attributes using the Field class to specify a validator for each
197 Please see the module documentation for details and examples of usage.
200 __metaclass__ = ValidatedMetaclass
202 def __init__(self, *args, **kw):
203 r"Initialize and validate the object, see the class documentation."
204 for name in self.validated_fields:
205 # Create all the attributes
206 setattr(self, name, None)
207 # Update the attributes with the arguments passed
208 self.update(**join_args(args, self.validated_fields, kw))
210 def update(self, *args, **kw):
211 r"update(*args, **kw) - Update objects attributes validating them."
212 # Get class attributes as a dict
213 attrs = dict([(k, getattr(self, k)) for k in self.validated_fields])
214 # Update the dict with the arguments passed
215 attrs.update(join_args(args, self.validated_fields, kw))
216 # Validate the resulting dict
217 attrs = self.class_validator.to_python(attrs)
218 # If we are here, there were no errors, so update the real attributes
219 for k, v in attrs.items():
223 r"validate() - Validate the object's attributes."
227 if __name__ == '__main__':
231 class Test(ValidatedClass):
232 name = Field(formencode.validators.String(not_empty=True))
233 age = Field(formencode.validators.Int(max=110, if_empty=None,
235 # Some global validation after individual fields validation
236 def chained_validator(self, fields, state):
237 if 'Jr' in fields['name'] and fields['age'] > 25:
238 raise formencode.Invalid(u"Junior can't be older than 25 years",
239 fields, state, error_dict=dict(
240 age='Should not be more than 25'))
243 # Will fail because 'name' is mandatory
245 assert 'It should raised' is False
246 except formencode.Invalid, e:
247 print unicode(e), e.error_dict
249 t = Test(name='Graham') # Keyword arguments are valid
250 assert t.name == 'Graham'
252 t = Test('Graham') # But can be used without keywords too!
253 # Use the order of fields declaration
254 assert t.name == 'Graham'
256 t = Test('Graham', 20)
257 assert t.name == 'Graham' and t.age == 20
259 t.update('Graham Jr.') # An update method is provided
260 assert t.name == 'Graham Jr.'
262 t.update(age=18) # And accepts keyword arguments too
265 # Updates are validated
267 # Will fail because Junior can't be older than 25 years
269 assert 'It should raised' is False
270 except formencode.Invalid, e:
271 print unicode(e), e.error_dict
273 # Other operations are not
277 # But you can use an empty update to validate
279 # Will fail because Junior can't be older than 25 years
281 assert 'It should raised' is False
282 except formencode.Invalid, e:
283 print unicode(e), e.error_dict
285 # You can use the alias validate() too
287 # Will fail because Junior can't be older than 25 years
289 assert 'It should raised' is False
290 except formencode.Invalid, e:
291 print unicode(e), e.error_dict