]> git.llucax.com Git - software/pymin.git/blob - pymin/validatedclass.py
Split ppp handler in submodules (refs #2).
[software/pymin.git] / pymin / validatedclass.py
1 # vim: set et sts=4 sw=4 encoding=utf-8 :
2
3 r"""Validated classes constructed with declarative style.
4
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
8 method).
9
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:
14
15 >>> import formencode
16 >>> class Test(ValidatedClass):
17 >>>     name = Field(formencode.validators.String(not_empty=True))
18 >>>     age = Field(formencode.validators.Int(max=110, if_empty=None,
19 >>>                                                    if_missing=None))
20 >>>     # Some global validation after individual fields validation
21 >>>     # (use pre_validator to validate *before* the individual fields
22 >>>     # validation)
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'))
28 >>> try:
29 >>>     # Will fail because 'name' is mandatory
30 >>>     t = Test()
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
46 >>> try:
47 >>>     # Will fail because Junior can't be older than 25 years
48 >>>     t.update(age=40)
49 >>>     assert 'It should raised' is False
50 >>> except formencode.Invalid, e:
51 >>>     print unicode(e), e.error_dict
52 >>> # Other operations are not
53 >>> t.age = 50
54 >>> assert t.age == 50
55 >>> # But you can use an empty update to validate
56 >>> try:
57 >>>     # Will fail because Junior can't be older than 25 years
58 >>>     t.update()
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
63 >>> try:
64 >>>     # Will fail because Junior can't be older than 25 years
65 >>>     t.validate()
66 >>>     assert 'It should raised' is False
67 >>> except formencode.Invalid, e:
68 >>>     print unicode(e), e.error_dict
69
70 Nice, ugh?
71 """
72
73 __all__ = ('Field', 'ValidatedClass')
74
75 from formencode import Invalid
76 from formencode.schema import Schema
77 from formencode.validators import FancyValidator
78
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
84 declarative_count = 0
85
86 class Field(object):
87     r"""Field(validator[, doc]) -> Field object
88
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
92     objects.
93
94     validator - A field validator. You can use any formencode validator.
95     doc - Document string (not used yet)
96
97     See module documentation for examples of usage.
98     """
99     def __init__(self, validator, doc=None):
100         r"Initialize the object, see the class documentation for details."
101         self.validator = validator
102         self.doc = doc
103         # Set and update the declarative counter
104         global declarative_count
105         self._declarative_count = declarative_count
106         declarative_count += 1
107
108 class ValidatedMetaclass(type):
109     r"""ValidatedMetaclass(classname, bases, class_dict) -> type
110
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
116     schema.
117
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)
121
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
125
126     This metaclass should be used indirectly inheriting from ValidatedClass.
127     """
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
131         # to go too high)
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
141         schema = Schema()
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
160                 del class_dict[key]
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)
165
166 def join_args(args, names, kwargs):
167     r"""join_args(args, names, kwargs) -> dict
168
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.
176
177     args - Positional arguments.
178     names - list of keywords.
179     kwargs - Keywords arguments.
180     """
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]
188     return kwargs
189
190 class ValidatedClass(object):
191     r"""ValidatedClass(*args, **kw) -> ValidatedClass object
192
193     You should inherit your classes from this one, declaring the class
194     attributes using the Field class to specify a validator for each
195     attribute.
196
197     Please see the module documentation for details and examples of usage.
198     """
199
200     __metaclass__ = ValidatedMetaclass
201
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))
209
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():
220             setattr(self, k, v)
221
222     def validate(self):
223         r"validate() - Validate the object's attributes."
224         self.update()
225
226
227 if __name__ == '__main__':
228
229     import formencode
230
231     class Test(ValidatedClass):
232         name = Field(formencode.validators.String(not_empty=True))
233         age = Field(formencode.validators.Int(max=110, if_empty=None,
234                                                        if_missing=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'))
241
242     try:
243         # Will fail because 'name' is mandatory
244         t = Test()
245         assert 'It should raised' is False
246     except formencode.Invalid, e:
247         print unicode(e), e.error_dict
248
249     t = Test(name='Graham') # Keyword arguments are valid
250     assert t.name == 'Graham'
251
252     t = Test('Graham') # But can be used without keywords too!
253                        # Use the order of fields declaration
254     assert t.name == 'Graham'
255
256     t = Test('Graham', 20)
257     assert t.name == 'Graham' and t.age == 20
258
259     t.update('Graham Jr.') # An update method is provided
260     assert t.name == 'Graham Jr.'
261
262     t.update(age=18) # And accepts keyword arguments too
263     assert t.age == 18
264
265     # Updates are validated
266     try:
267         # Will fail because Junior can't be older than 25 years
268         t.update(age=40)
269         assert 'It should raised' is False
270     except formencode.Invalid, e:
271         print unicode(e), e.error_dict
272
273     # Other operations are not
274     t.age = 50
275     assert t.age == 50
276
277     # But you can use an empty update to validate
278     try:
279         # Will fail because Junior can't be older than 25 years
280         t.update()
281         assert 'It should raised' is False
282     except formencode.Invalid, e:
283         print unicode(e), e.error_dict
284
285     # You can use the alias validate() too
286     try:
287         # Will fail because Junior can't be older than 25 years
288         t.validate()
289         assert 'It should raised' is False
290     except formencode.Invalid, e:
291         print unicode(e), e.error_dict
292
293