*** empty log message ***
[pli.git] / pli / interface.py
blob96dfa9769d23a0da0b782a5ed3a5231c16a737cf
1 #=======================================================================
3 __version__ = '''0.2.01'''
4 __sub_version__ = '''20040829143248'''
5 __copyright__ = '''(c) Alex A. Naanou 2003'''
8 #-----------------------------------------------------------------------
10 __doc__ = '''\
11 This module defines an interface definition and enforcement mechanism and
12 classes.
13 '''
16 #-----------------------------------------------------------------------
18 import pli.pattern.mixin.mapping as mapping
19 import pli.logictypes as logictypes
22 #-----------------------------------------------------------------------
23 #------------------------------------------------------InterfaceError---
24 class InterfaceError(Exception):
25 '''
26 '''
27 pass
31 #-----------------------------------------------------------------------
32 #----------------------------------------------------------_Interface---
33 class _Interface(type, mapping.Mapping):
34 '''
35 '''
36 # this will define the interface format...
37 __format__ = None
38 # this if False will prevent the modification of the interface
39 # after it's definition... (default: False)
40 __interface_writable__ = False
41 # this if True will enable element deletion from base interfaces if
42 # it was not defined locally... (default: False)
43 __contagious_delete__ = False
45 # WARNING: do not change this unless you know what you are doing!
46 __attribute_properties__ = (
47 'type',
48 'default',
49 'predicate',
50 'essential',
51 'doc',
52 'handler',
53 'readable',
54 'writable',
55 'deleteable',
56 # special options...
57 'LIKE',
60 def __init__(cls, name, bases, ns):
61 '''
62 '''
63 # sanity checks....
64 if not hasattr(cls, '__format__'):
65 raise InterfaceError, 'interface %s does not have a format defined.' % cls
66 # check consistency...
67 errs = []
68 if not cls.__isconsistent__(errs):
69 errs[:] = dict.fromkeys([ e.__class__.__name__ + ': '+ str(e) for e in errs ]).keys()
70 raise InterfaceError, 'inconsistent interface definition for %s in:\n %s.' % (cls, '\n '.join(errs))
71 super(_Interface, cls).__init__(name, bases, ns)
72 # mapping specific:
73 def __getitem__(cls, name):
74 '''
75 '''
76 # sanity checks....
77 if not hasattr(cls, '__format__'):
78 raise InterfaceError, 'interface %s does not have a format defined.' % cls
79 format = cls.__format__
80 ## if format != None and name in format:
81 ## return format[name]
82 try:
83 ## for c in cls.__mro__[1:]:
84 for c in cls.__mro__:
85 if hasattr(c, '__format__') \
86 and c.__format__ != None \
87 and name in c.__format__:
88 return c.__format__[name]
89 except TypeError:
90 # c was not a dict-like....
91 pass
92 try:
93 return super(_Interface, cls).__getitem__(name)
94 except AttributeError:
95 raise KeyError, str(name)
96 def __setitem__(cls, name, value):
97 '''
99 NOTE: this will only modify the current class (no base interface will change).
101 if hasattr(cls, '__interface_writable__') and not cls.__interface_writable__:
102 raise InterfaceError, 'the interface %s is not modifiable.' % cls
103 if '__format__' in cls.__dict__ and cls.__format__ != None:
104 cls.__format__[name] = value
105 return
106 cls.__format__ = {name: value}
107 def __delitem__(cls, name):
110 if hasattr(cls, '__interface_writable__') and not cls.__interface_writable__:
111 raise InterfaceError, 'the interface %s is not modifiable.' % cls
112 if name in cls:
113 # if name is not local to this interface...
114 if ('__format__' not in cls.__dict__ or name not in cls.__dict__['__format__']) \
115 and (not hasattr(cls, '__contagious_delete__') or not cls.__contagious_delete__):
116 raise InterfaceError, 'the interface %s is not modifiable.' % cls
117 # delete...
118 # this is safe as-is as we get here in two cases:
119 # 1. the name is local
120 # 2. we can delete the name form its container...
121 try:
122 del cls.__format__[name]
123 except KeyError:
124 for c in cls.__mro__[1:]:
125 if hasattr(c, '__format__') \
126 and c.__format__ != None \
127 and name in c.__format__:
128 del c.__format__[name]
129 return
130 else:
131 raise KeyError, str(name)
132 ## def __contains__(cls, name):
133 ## '''
134 ## '''
135 def __iter__(cls):
138 visited = []
139 for c in cls.__mro__:
140 if hasattr(c, '__format__') \
141 and c.__format__ != None:
142 for k in c.__format__.iterkeys():
143 if k in visited:
144 continue
145 visited += [k]
146 yield k
148 def __isconsistent__(cls, errors=None):
151 for name in cls:
152 try:
153 props = cls.getattrproperty(name)
154 except Exception, e:
155 if errors != None:
156 errors += [e]
157 if errors in ([], None):
158 return True
159 return False
160 # interface specific (1st generation):
161 def getattrproperty(cls, name, prop=None):
164 returns:
165 None : if prop is given but not found.
166 val : if prop is given and found (might also be None).
167 dict : if name is found and prop not given.
169 NOTE: if a property is not defined for the attr None will be returned (this is the same as if its value was none).
171 allowed_props = cls.__attribute_properties__ + (None,)
172 if prop not in allowed_props:
173 raise InterfaceError, 'unknown option "%s".' % prop
174 if name not in cls:
175 if '*' in cls:
176 res = {}
177 else:
178 raise KeyError, str(name)
179 else:
180 res = cls[name].copy()
181 # resolve the 'LIKE' prop...
182 visited = [res]
183 while 'LIKE' in res:
184 if type(res['LIKE']) is str:
185 ext_format = cls[res['LIKE']].copy()
186 elif type(res['LIKE']) is dict:
187 ext_format = res['LIKE'].copy()
188 else:
189 raise TypeError, 'the argument of "LIKE" attribute option must '\
190 'either be of type str or dict (got: %s).' % type(res['LIKE'])
191 if res['LIKE'] == name or ext_format in visited:
192 # check for conflicts in the chain.... (a conflict is
193 # when a name is present more than once with different
194 # values).
195 v = visited[0]
196 # XXX is there a better way to do this??? (use sets???)
197 for d in visited[1:]:
198 for k in d:
199 if k != 'LIKE' and k in v and d[k] != v[k]:
200 raise InterfaceError, 'LIKE loop conflict in %s for attribute "%s".' % (cls, name)
201 if k not in allowed_props:
202 raise InterfaceError, 'unknown option "%s".' % k
203 v[k] = d[k]
204 del res['LIKE']
205 break
206 visited += [ext_format.copy()]
207 del res['LIKE']
208 ext_format.update(res)
209 # revise...
210 ## res = ext_format
211 res.update(ext_format)
212 if prop != None:
213 if prop in res:
214 return res[prop]
215 else:
216 ## raise InterfaceError, 'property "%s" is not defined for attr "%s".' % (prop, name)
217 return None
218 return res
219 # interface methods (2nd generation):
220 # TODO exception safe??????
221 def isessential(cls, name):
224 return cls.getattrproperty(name, 'essential') == True
225 def isreadable(cls, name):
228 return cls.getattrproperty(name, 'readable') in (True, None)
229 def iswritable(cls, name):
232 return cls.getattrproperty(name, 'writable') in (True, None)
233 def isdeletable(cls, name):
236 return cls.getattrproperty(name, 'deleteable') in (True, None) \
237 and cls.getattrproperty(name, 'essential') != True
238 ##!!!
242 #-----------------------------------------------------------Interface---
243 class Interface(object):
245 XXX write more docs...
247 the attribute definition format is as follows:
249 <attr-name> :
251 <opt-name>: <opt-value>
252 [...]
254 [...]
258 supported options:
259 type - value type or superclass.
260 default - this is the default value of the option.
261 predicate - this will get the option value as argument and
262 test its compliance (if the will return False
263 InterfaceError will be raised).
264 essential - this if true will guarantee the options'
265 existence in the created object.
267 doc - this is the attr documentation
269 handler - this is the alternative attribute handler.
270 this will take the option value and its old value
271 as arguments and its' return will replace the
272 original value.
274 readable - this if False will prevent the attr from being
275 read.
276 writable - this if False will prevent the attr from being
277 written.
278 deleteable - this if False will prevent the attr from being
279 removed.
281 special options:
282 LIKE - this states the attribute template. if this is
283 given all options not declared for this attribute
284 will be taken from the template.
285 the value is either the name of the attribute in
286 the current interface or a dict of the interface
287 attr definition format.
290 special attribute names:
291 '*' - this matches any attribute not explicitly given.
295 __metaclass__ = _Interface
297 __format__ = None
301 #-----------------------------------------------------------------------
302 # utility functions
303 #-------------------------------------------------------getinterfaces---
304 def getinterfaces(obj):
306 this will return a tuuple containing the supported by the object interfaces.
308 if not hasattr(obj, '__implemments__') or obj.__implemments__ is None:
309 return ()
310 # get objects interface...
311 if type(obj.__implemments__) in (list, tuple):
312 return tuple(obj.__implemments__)
313 else:
314 return (obj.__implemments__,)
317 #----------------------------------------------------------checkvalue---
318 def checkvalue(obj, name, value, interface=None):
321 if interface != None:
322 format = (interface,)
323 else:
324 format = getinterfaces(obj)
325 # get the effective inteface...
326 star = None
327 for i in format:
328 # find explicit name definition...
329 if name in i:
330 format = i.getattrproperty(name)
331 break
332 # find first '*'...
333 if star != None and '*' in i:
334 star = i.getattrproperty('*')
335 # if no explicit definition is found...
336 if type(format) is not dict:
337 # attr must be defined in interface (explicit or implicit)...
338 if star == None:
339 raise InterfaceError, 'no definition of attribute "%s" in %s.' % (name, obj)
340 format = star
341 # attr type...
342 if 'type' in format and not issubclass(type(value), format['type']):
343 raise InterfaceError, 'attribute type mismatch. "%s" attribute ' \
344 'must be of type %s (got: %s).' % (name, type(value), format['type'])
345 # attr predicate...
346 if 'predicate' in format and not format['predicate'](value):
347 raise InterfaceError, 'predicate failed for "%s" attribute.' % name
348 return True
351 #-----------------------------------------------------------checkattr---
352 # XXX use a different getattr mechanism as this might be blocked by the
353 # interface....
354 def checkattr(obj, name, interface=None):
357 return checkvalue(obj, name, getattr(obj, name), interface)
361 #-----------------------------------------------------------------------
362 # attribute level functions...
363 # Q: should there be additional logic to determine for example
364 # attr readability for an object not supporting interfaces...
365 #---------------------------------------------------------isessential---
366 def isessential(obj, name, interface=None):
369 NOTE: if the object does not support interfaces and no explicit
370 interface was given this will return False.
372 if interface != None:
373 format = (interface,)
374 else:
375 format = getinterfaces(obj)
376 return True in [ i.isessential(name) for i in fromat ]
379 #----------------------------------------------------------isreadable---
380 def isreadable(obj, name, interface=None):
383 NOTE: if the object does not support interfaces and no explicit
384 interface was given this will return True.
386 if interface != None:
387 format = (interface,)
388 else:
389 format = getinterfaces(obj)
390 return False not in [ i.isreadable(name) for i in fromat ]
393 #----------------------------------------------------------iswritable---
394 def iswritable(obj, name, interface=None):
397 NOTE: if the object does not support interfaces and no explicit
398 interface was given this will return True.
400 if interface != None:
401 format = (interface,)
402 else:
403 format = getinterfaces(obj)
404 return False not in [ i.iswritable(name) for i in fromat ]
407 #---------------------------------------------------------isdeletable---
408 def isdeletable(obj, name, interface=None):
411 NOTE: if the object does not support interfaces and no explicit
412 interface was given this will return True.
414 if interface != None:
415 format = (interface,)
416 else:
417 format = getinterfaces(obj)
418 return False not in [ i.isdeletable(name) for i in fromat ]
421 ###----------------------------------------------------istypecompatible---
422 ####!!! ?????
423 ##def istypecompatible(obj, name, interface=None):
424 ## '''
425 ## '''
426 ## ##!!!
429 ###-----------------------------------------------------------isdefault---
430 ##def isdefault(obj, name, interface=None):
431 ## '''
432 ## '''
433 ## ##!!!
436 #---------------------------------------------------isvaluecompatible---
437 def isvaluecompatible(obj, name, value, interface=None):
440 try:
441 return checkvalue(obj, name, value, interface)
442 except:
443 return False
446 #--------------------------------------------------------iscompatible---
447 # this will check all the applicable predicates...
448 # XXX use a different getattr mechanism as this might be blocked by the
449 # interface....
450 def iscompatible(obj, name, interface=None):
453 try:
454 return checkattr(obj, name, interface)
455 except:
456 return False
460 #-----------------------------------------------------------------------
461 # object level functions...
462 #-----------------------------------------------------checkessentials---
463 def checkessentials(obj, interface=None):
466 if interface != None:
467 format = interface
468 else:
469 format = logictypes.DictUnion(*getinterfaces(obj)[::-1])
470 res = {}
471 for n in format:
472 if n not in res:
473 v = format[n].get('essential', False)
474 if v not in (None, False):
475 res[n] = v
476 for n in res:
477 if not hasattr(obj, n):
478 raise InterfaceError, 'essential attribute "%s" missing from %s' % (n, obj)
479 checkattr(obj, n)
480 return True
483 #---------------------------------------------------------checkobject---
484 def checkobject(obj, interface=None):
487 if interface != None:
488 format = interface
489 else:
490 format = logictypes.DictUnion(*getinterfaces(obj)[::-1])
491 o_attrs = vars(obj).copy()
492 for n in format:
493 # Q: which one of the folowing is faster??
494 if n not in o_attrs:
495 ## if isessential(obj, n):
496 if format[n].get('essential', False):
497 raise InterfaceError, 'essential attribute "%s" missing from %s' % (n, obj)
498 else:
499 chackattr(obj, n)
500 del o_attrs[n]
501 if len(o_attrs) > 0:
502 if '*' not in format:
503 raise InterfaceError, 'excess attributes %s in object %s.' % (o_attrs.keys(), obj)
504 for n in o_attrs:
505 chackattr(obj, n)
506 return True
510 #-----------------------------------------------------------------------
511 #--------------------------------------------------------------getdoc---
512 ##!! revise
513 def getdoc(obj, name=None, interface=None):
515 this will return a dict containing the attr name and the coresponding
516 doc defined in the interface.
517 if name is not present this will return all the docs for each attr defined...
519 if interface != None:
520 format = interface
521 else:
522 format = logictypes.DictUnion(*getinterfaces(obj)[::-1])
523 # if name is present...
524 if name != None:
525 if name in format:
526 return {name: format[n].get('essential', None)}
527 raise InterfaceError, 'attribute "%s" is not defined in the interface for %s.' % (name, obj)
528 # if name is not present...
529 res = {}
530 # this is bad as getattrproperty is called for each attr in each
531 # interface... (see below for a more eficient variant...)
532 #for i in format[::-1]:
533 # res.update(dict([ (n, i.getattrproperty(n, 'doc')) for n in i ]))
534 # NOTE: this is faster in the above variant... (complexity is less
535 # for cases where there are overlaping interfaces...)
536 # though this might be slightly slower due to the nested
537 # loops...
538 for i in format:
539 for n in i:
540 if n not in res:
541 res[n] = i.getattrproperty(n, 'doc')
542 return res
546 #-----------------------------------------------------------------------
547 #-------------------------------------------------ObjectWithInterface---
548 class ObjectWithInterface(object):
551 # this defines the objects' interface.
552 # NOTE: if this is None interface support will be disabled.
553 __implemments__ = None
555 ##!!!
556 def __new__(cls, *p, **n):
559 obj = object.__new__(cls, *p, **n)
560 interface = obj.__implemments__
561 if interface != None:
562 obj.__dict__.update(dict([ (n, v['default']) \
563 for n, v \
564 in type(interface) is tuple \
565 and logictypes.DictUnion(*[ i.__format__ for i in interface ]).iteritems() \
566 or interface.__format__.iteritems() \
567 if 'default' in v and n != '*' ]))
568 return obj
569 def __getattribute__(self, name):
572 if name == '__implemments__' \
573 or object.__getattribute__(self, '__implemments__') == None \
574 or isreadable(self, name):
575 return super(ObjectWithInterface, self).__getattribute__(name)
576 raise InterfaceError, 'can\'t read attribute "%s".' % name
577 def __setattr__(self, name, value):
580 if object.__getattribute__(self, '__implemments__') == None \
581 or (iswritable(self, name) and isvaluecompatible(self, name, value)):
582 return super(ObjectWithInterface, self).__setattr__(name, value)
583 raise InterfaceError, 'can\'t write value "%s" to attribute "%s".' % (value, name)
584 def __delattr__(self, name):
587 if object.__getattribute__(self, '__implemments__') != None \
588 and not isdeletable(self, name):
589 raise InterfaceError, 'can\'t delete an essential attribute "%s".' % name
590 super(ObjectWithInterface, self).__delattr__(name)
593 #------------------------------------------------------InterfaceProxy---
594 # TODO move this to pli.pattern.proxy (???)
595 class InterfaceProxy(object):
598 __implemments__ = None
600 __source__ = None
602 def __init__(self, source):
605 self.__source__ = source
606 def __getattr__(self, name):
609 if self.__implemments__ == None \
610 or name == '__implemments__' or isreadable(self, name):
611 return getattr(self.__source__, name)
612 raise InterfaceError, 'can\'t read attribute "%s".' % name
613 def __setattr__(self, name, value):
616 if name in ('__source__', '__implemments__'):
617 return super(InterfaceProxy, self).__setattr__(name, value)
618 if self.__implemments__ == None \
619 or (iswritable(self, name) and isvaluecompatible(self, name, value)):
620 return setattr(self.__source__, name, value)
621 raise InterfaceError, 'can\'t write value "%s" to attribute "%s".' % (value, name)
622 def __delattr__(self, name):
625 if self.__implemments__ != None and isessential(self, name):
626 raise InterfaceError, 'can\'t delete an essential attribute "%s".' % name
627 delattr(self.__source__, name)
631 #=======================================================================
632 if __name__ == '__main__':
634 class ITest(Interface):
635 __format__ = {\
636 'aaa': {},
637 'ess': {'essential': True},
638 'xxx': {'doc':'ITest'},
641 class ITestTest(Interface):
642 __format__ = {\
643 'bbb': {'type': int, 'LIKE': 'ccc', 'default': 0},
644 'ccc': {'LIKE': 'xxx', 'default': 0, 'readable': True},
645 ## 'ccc': {'default': 5, 'readable': True},
646 'xxx': {'doc': 'ITestTest', 'LIKE': 'bbb', 'default': 0},
649 class IDTest0(ITest, ITestTest):
650 pass
652 class IDTest1(ITestTest, ITest):
653 __format__ = {}
656 print ITest['aaa']
657 print ITestTest['bbb']
658 print IDTest0['xxx']
659 print IDTest1['xxx']
661 print 'yyy' in IDTest1
663 print IDTest0.getattrproperty('xxx')
665 print IDTest1.getattrproperty('xxx', 'doc')
666 print IDTest1.getattrproperty('xxx')
667 print IDTest1['xxx']
669 print list(IDTest1)
670 print list(IDTest0)
673 class A(object):
674 __implemments__ = IDTest1
675 ccc = 123
677 a = A()
679 a.ess = ''
681 print checkattr(a, 'ccc')
682 print checkvalue(a, 'ccc', 0)
684 print getdoc(a, 'ccc')
686 print checkessentials(a)
689 #=======================================================================
690 # vim:set ts=4 sw=4 nowrap :