root/trunk/flexget/validator.py @ 719

Revision 719, 12.2 KB (checked in by paranoidi, 15 months ago)

Minor tweaks

  • Property svn:eol-style set to native
Line 
1import re
2
3# TODO: rename all validator.valid -> validator.accepts / accepted / accept ?
4
5class Errors:
6
7    """Create and hold validator error messages."""
8
9    def __init__(self):
10        self.messages = []
11        self.path = []
12        self.path_level = None
13       
14    def count(self):
15        """Return number of errors."""
16        return len(self.messages)
17   
18    def add(self, msg):
19        """Add new error message to current path."""
20        path = [str(p) for p in self.path]
21        msg = '[/%s] %s' % ('/'.join(path), msg)
22        self.messages.append(msg)
23
24    def path_add_level(self, value='?'):
25        """Adds level into error message path"""
26        self.path_level = len(self.path)
27        self.path.append(value)
28
29    def path_remove_level(self):
30        """Removes level from path by depth number"""
31        if self.path_level is None:
32            raise Exception('no path level')
33        del(self.path[self.path_level])
34        self.path_level -= 1
35
36    def path_update_value(self, value):
37        """Updates path level value"""
38        if self.path_level is None:
39            raise Exception('no path level')
40        self.path[self.path_level] = value
41
42def factory(name='root'):
43    """Factory method, return validator instance."""
44    v = Validator()
45    return v.get_validator(name)
46
47class Validator(object):
48
49    name = 'validator'
50
51    def __init__(self, parent=None):
52        self.valid = []
53        if parent is None:
54            self.errors = Errors()
55            self.validators = {}
56           
57            # register default validators
58            register = [RootValidator, ListValidator, DictValidator, TextValidator, FileValidator,
59                        PathValidator, AnyValidator, NumberValidator, BooleanValidator, 
60                        DecimalValidator, UrlValidator, RegexpValidator, ChoiceValidator]
61            for v in register:
62                self.register(v)
63        else:
64            self.errors = parent.errors
65            self.validators = parent.validators
66
67    def register(self, validator):
68        if not hasattr(validator, 'name'):
69            raise Exception('Validator %s is missing class-attribute name' % validator.__class__.__name__)
70        self.validators[validator.name] = validator
71
72    def get_validator(self, name):
73        if not self.validators.get(name):
74            raise Exception('Asked unknown validator \'%s\'' % name)
75        #print 'returning %s' % name
76        return self.validators[name](self)
77
78    def accept(self, name, **kwargs):
79        raise Exception('Validator %s should override accept method' % self.__class__.__name__)
80   
81    def validateable(self, data):
82        """Return true if validator can be used to validate given data, False otherwise."""
83        raise Exception('Validator %s should override validateable method' % self.__class__.__name__)
84       
85    def validate(self, data):
86        """Validate given data and log errors, return True if passed and False if not."""
87        raise Exception('Validator %s should override validate method' % self.__class__.__name__)
88       
89    def validate_item(self, item, rules):
90        """
91            Helper method. Validate item against list of rules (validators).
92            Return True if item passed some rule. False if none of the rules pass item.
93        """
94        for rule in rules:
95            #print 'validating %s' % rule.name
96            if rule.validateable(item):
97                if rule.validate(item):
98                    return True
99        return False
100
101    def __str__(self):
102        return '<%s>' % self.name
103       
104class RootValidator(Validator):
105    name = 'root'
106
107    def accept(self, name, **kwargs):
108        v = self.get_validator(name)
109        self.valid.append(v)
110        return v
111   
112    def validateable(self, data):
113        return True
114   
115    def validate(self, data):
116        count = self.errors.count()
117        for v in self.valid:
118            if v.validateable(data):
119                if v.validate(data):
120                    return True
121        # containers should only add errors if inner validators did not
122        if count == self.errors.count():
123            acceptable = [v.name for v in self.valid]
124            self.errors.add('failed to pass as %s' % ', '.join(acceptable))
125        return False
126
127# borked               
128class ChoiceValidator(Validator):
129    name = 'choice'
130
131    def accept(self, name, **kwargs):
132        v = self.get_validator(kwargs['value'])
133        self.valid.append(v)
134        return v
135
136    def validateable(self, data):
137        raise Exception('borked')
138
139    def validate(self, data):
140        if not self.validate_item(data, self.valid):
141            l = [r.name for r in self.valid]
142            self.errors.add('must be one of values %s' % (', '.join(l)))
143        return True
144
145class AnyValidator(Validator):
146    name = 'any'
147
148    def accept(self, value, **kwargs):
149        self.valid = value
150
151    def validateable(self, data):
152        return True
153   
154    def validate(self, data):
155        return True
156
157class EqualsValidator(Validator):
158    name = 'equals'
159
160    def accept(self, value, **kwargs):
161        self.valid = value
162
163    def validateable(self, data):
164        return True
165   
166    def validate(self, data):
167        return self.valid == data
168
169class NumberValidator(Validator):
170    name = 'number'
171
172    def accept(self, name, **kwargs):
173        pass
174
175    def validateable(self, data):
176        return isinstance(data, int)
177
178    def validate(self, data):
179        valid = isinstance(data, int)
180        if not valid:
181            self.errors.add('value %s is not valid number' % data)
182        return valid
183
184class BooleanValidator(Validator):
185    name = 'boolean'
186
187    def accept(self, name, **kwargs):
188        pass
189
190    def validateable(self, data):
191        return isinstance(data, bool)
192
193    def validate(self, data):
194        valid = isinstance(data, bool)
195        if not valid:
196            self.errors.add('value %s is not valid boolean' % data)
197        return valid
198
199class DecimalValidator(Validator):
200    name = 'decimal'
201
202    def accept(self, name, **kwargs):
203        pass
204
205    def validateable(self, data):
206        return isinstance(data, float)
207
208    def validate(self, data):
209        valid = isinstance(data, float)
210        if not valid:
211            self.errors.add('value %s is not valid decimal number' % data)
212        return valid
213
214class TextValidator(Validator):
215    name = 'text'
216   
217    def accept(self, name, **kwargs):
218        pass
219
220    def validateable(self, data):
221        return isinstance(data, basestring)
222
223    def validate(self, data):
224        valid = isinstance(data, basestring)
225        if not valid:
226            self.errors.add('value %s is not valid text' % data)
227        return valid
228
229class RegexpValidator(Validator):
230    name = 'regexp'
231   
232    def accept(self, name, **kwargs):
233        pass
234
235    def validateable(self, data):
236        return isinstance(data, basestring)
237
238    def validate(self, data):
239        if not isinstance(data, basestring):
240            self.errors.add('Value should be text')
241            return False
242        try:
243            re.compile(data)
244        except:
245            self.errors.add('%s is not a valid regular expression' % data)
246            return False
247        return True
248
249class FileValidator(TextValidator):
250    name = 'file'
251   
252    def validate(self, data):
253        import os
254        if not os.path.isfile(os.path.expanduser(data)):
255            self.errors.add('File %s does not exist' % data)
256            return False
257        return True
258
259class PathValidator(TextValidator):
260    name = 'path'
261   
262    def validate(self, data):
263        import os
264        if not os.path.isdir(os.path.expanduser(data)):
265            self.errors.add('Path %s does not exist' % data)
266            return False
267        return True
268
269class UrlValidator(TextValidator):
270    name = 'url'
271   
272    def validate(self, data):
273        regexp = '(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?'
274        if not isinstance(data, basestring):
275            self.errors.add('expecting text')
276            return False
277        valid = re.match(regexp, data) != None
278        if not valid:
279            self.errors.add('value %s is not valid url' % data)
280        return valid
281       
282class ListValidator(Validator):
283    name = 'list'
284
285    def accept(self, name, **kwargs):
286        v = self.get_validator(name)
287        self.valid.append(v)
288        return v
289
290    def validateable(self, data):
291        return isinstance(data, list)
292
293    def validate(self, data):
294        if not isinstance(data, list):
295            self.errors.add('value should be a list')
296            return False
297        self.errors.path_add_level()
298        count = self.errors.count()
299        for item in data:
300            self.errors.path_update_value('list:%i' % data.index(item))
301            if not self.validate_item(item, self.valid):
302                # containers should only add errors if inner validators did not
303                if count == self.errors.count():
304                    l = [r.name for r in self.valid]
305                    self.errors.add('is not valid %s' % (', '.join(l)))
306        self.errors.path_remove_level()
307        return count == self.errors.count()
308
309class DictValidator(Validator):
310    name = 'dict'
311
312    def __init__(self, parent=None):
313        self.reject = []
314        self.any_key = []
315        self.required_keys = []
316        Validator.__init__(self, parent)
317        # TODO: not dictionary?
318        self.valid = {}
319
320    def accept(self, name, **kwargs):
321        """Accepts key with name type"""
322        if not 'key' in kwargs:
323            raise Exception('%s.accept() must specify key' % self.name)
324
325        key = kwargs['key']
326        v = self.get_validator(name)
327        self.valid.setdefault(key, []).append(v)
328        # complain from old format
329        if 'require' in kwargs:
330            print 'TODO: REQUIRE USED'
331        if kwargs.get('required', False):
332            self.require_key(key)
333        return v
334
335    def reject_key(self, key):
336        """Rejects key"""
337        self.reject.append(key)
338
339    def reject_keys(self, keys):
340        """Reject list of keys"""
341        self.reject.extend(keys)
342
343    def require_key(self, key):
344        """Flag key as mandatory"""
345        if not key in self.required_keys:
346            self.required_keys.append(key)
347
348    def accept_any_key(self, name, **kwargs):
349        """Accepts any key with this given type."""
350        v = self.get_validator(name)
351        #v.accept(name, **kwargs)
352        self.any_key.append(v)
353        return v
354
355    def validateable(self, data):
356        return isinstance(data, dict)
357   
358    def validate(self, data):
359        if not isinstance(data, dict):
360            self.errors.add('value should be a dictionary / map')
361            return False
362       
363        count = self.errors.count()
364        self.errors.path_add_level()
365        for key, value in data.iteritems():
366            self.errors.path_update_value('dict:%s' % key)
367            if not key in self.valid and not self.any_key:
368                self.errors.add('key \'%s\' is not recognized' % key)
369                continue
370            if key in self.reject:
371                self.errors.add('key \'%s\' is forbidden here' % key)
372                continue
373            # rules contain rules specified for this key AND
374            # rules specified for any key
375            rules = self.valid.get(key, [])
376            rules.extend(self.any_key)
377            if not self.validate_item(value, rules):
378                if count == self.errors.count():
379                    # containers should only add errors if inner validators did not
380                    l = [r.name for r in rules]
381                    self.errors.add('value \'%s\' is not valid %s' % (value, ', '.join(l)))
382        for required in self.required_keys:
383            if not required in data:
384                self.errors.add('key \'%s\' required' % required)
385        self.errors.path_remove_level()
386        return count == self.errors.count()
387
388if __name__=='__main__':
389   
390    root = factory()
391    container = root.accept('list')
392    #container.accept('text')
393    #container.accept('number')
394    bundle = container.accept('dict')
395    advanced = bundle.accept_any_key('dict')
396    advanced.accept('text', key='path')
397   
398    root.validate([{'some series':{'path':'~/asfd/', 'fail':True}}])
399    #root.validate(['asdfasdf', 'asdfasdfasdf', {'key':'value'}])
400   
401    print root.errors.messages
402
Note: See TracBrowser for help on using the browser.