doc: update :class: to :py:class: for modern Sphinx.
[be.git] / libbe / storage / util / properties.py
1 # Bugs Everywhere - a distributed bugtracker
2 # Copyright (C) 2008-2012 Chris Ball <cjb@laptop.org>
3 #                         Gianluca Montecchi <gian@grys.it>
4 #                         W. Trevor King <wking@tremily.us>
5 #
6 # This file is part of Bugs Everywhere.
7 #
8 # Bugs Everywhere is free software: you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by the Free
10 # Software Foundation, either version 2 of the License, or (at your option) any
11 # later version.
12 #
13 # Bugs Everywhere is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
16 # more details.
17 #
18 # You should have received a copy of the GNU General Public License along with
19 # Bugs Everywhere.  If not, see <http://www.gnu.org/licenses/>.
20
21 """Provides a series of useful decorators for defining various types
22 of properties.
23
24 For example usage, consider the unittests at the end of the module.
25
26 Notes
27 -----
28
29 See `PEP 318` and Michele Simionato's `decorator documentation` for
30 more information on decorators.
31
32 .. _PEP 318: http://www.python.org/dev/peps/pep-0318/
33 .. _decorator documentation: http://www.phyast.pitt.edu/~micheles/python/documentation.html
34
35 See Also
36 --------
37 :py:mod:`libbe.storage.util.settings_object` : bundle properties into a convenient package
38
39 """
40
41 import copy
42 import types
43
44 import libbe
45 if libbe.TESTING == True:
46     import unittest
47
48
49 class ValueCheckError (ValueError):
50     def __init__(self, name, value, allowed):
51         action = "in" # some list of allowed values
52         if type(allowed) == types.FunctionType:
53             action = "allowed by" # some allowed-value check function
54         msg = "%s not %s %s for %s" % (value, action, allowed, name)
55         ValueError.__init__(self, msg)
56         self.name = name
57         self.value = value
58         self.allowed = allowed
59
60 def Property(funcs):
61     """
62     End a chain of property decorators, returning a property.
63     """
64     args = {}
65     args["fget"] = funcs.get("fget", None)
66     args["fset"] = funcs.get("fset", None)
67     args["fdel"] = funcs.get("fdel", None)
68     args["doc"] = funcs.get("doc", None)
69
70     #print "Creating a property with"
71     #for key, val in args.items(): print key, value
72     return property(**args)
73
74 def doc_property(doc=None):
75     """
76     Add a docstring to a chain of property decorators.
77     """
78     def decorator(funcs=None):
79         """
80         Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...}
81         or a function fn() returning such a dict.
82         """
83         if hasattr(funcs, "__call__"):
84             funcs = funcs() # convert from function-arg to dict
85         funcs["doc"] = doc
86         return funcs
87     return decorator
88
89 def local_property(name, null=None, mutable_null=False):
90     """
91     Define get/set access to per-parent-instance local storage.  Uses
92     ._<name>_value to store the value for a particular owner instance.
93     If the ._<name>_value attribute does not exist, returns null.
94
95     If mutable_null == True, we only release deepcopies of the null to
96     the outside world.
97     """
98     def decorator(funcs):
99         if hasattr(funcs, "__call__"):
100             funcs = funcs()
101         fget = funcs.get("fget", None)
102         fset = funcs.get("fset", None)
103         def _fget(self):
104             if fget is not None:
105                 fget(self)
106             if mutable_null == True:
107                 ret_null = copy.deepcopy(null)
108             else:
109                 ret_null = null
110             value = getattr(self, "_%s_value" % name, ret_null)
111             return value
112         def _fset(self, value):
113             setattr(self, "_%s_value" % name, value)
114             if fset is not None:
115                 fset(self, value)
116         funcs["fget"] = _fget
117         funcs["fset"] = _fset
118         funcs["name"] = name
119         return funcs
120     return decorator
121
122 def settings_property(name, null=None):
123     """
124     Similar to local_property, except where local_property stores the
125     value in instance._<name>_value, settings_property stores the
126     value in instance.settings[name].
127     """
128     def decorator(funcs):
129         if hasattr(funcs, "__call__"):
130             funcs = funcs()
131         fget = funcs.get("fget", None)
132         fset = funcs.get("fset", None)
133         def _fget(self):
134             if fget is not None:
135                 fget(self)
136             value = self.settings.get(name, null)
137             return value
138         def _fset(self, value):
139             self.settings[name] = value
140             if fset is not None:
141                 fset(self, value)
142         funcs["fget"] = _fget
143         funcs["fset"] = _fset
144         funcs["name"] = name
145         return funcs
146     return decorator
147
148
149 # Allow comparison and caching with _original_ values for mutables,
150 # since
151 #
152 # >>> a = []
153 # >>> b = a
154 # >>> b.append(1)
155 # >>> a
156 # [1]
157 # >>> a==b
158 # True
159 def _hash_mutable_value(value):
160     return repr(value)
161 def _init_mutable_property_cache(self):
162     if not hasattr(self, "_mutable_property_cache_hash"):
163         # first call to _fget for any mutable property
164         self._mutable_property_cache_hash = {}
165         self._mutable_property_cache_copy = {}
166 def _set_cached_mutable_property(self, cacher_name, property_name, value):
167     _init_mutable_property_cache(self)
168     self._mutable_property_cache_hash[(cacher_name, property_name)] = \
169         _hash_mutable_value(value)
170     self._mutable_property_cache_copy[(cacher_name, property_name)] = \
171         copy.deepcopy(value)
172 def _get_cached_mutable_property(self, cacher_name, property_name, default=None):
173     _init_mutable_property_cache(self)
174     if (cacher_name, property_name) not in self._mutable_property_cache_copy:
175         return default
176     return self._mutable_property_cache_copy[(cacher_name, property_name)]
177 def _cmp_cached_mutable_property(self, cacher_name, property_name, value, default=None):
178     _init_mutable_property_cache(self)
179     if (cacher_name, property_name) not in self._mutable_property_cache_hash:
180         _set_cached_mutable_property(self, cacher_name, property_name, default)
181     old_hash = self._mutable_property_cache_hash[(cacher_name, property_name)]
182     return cmp(_hash_mutable_value(value), old_hash)
183
184
185 def defaulting_property(default=None, null=None,
186                         mutable_default=False):
187     """
188     Define a default value for get access to a property.
189     If the stored value is null, then default is returned.
190
191     If mutable_default == True, we only release deepcopies of the
192     default to the outside world.
193
194     null should never escape to the outside world, so don't worry
195     about it being a mutable.
196     """
197     def decorator(funcs):
198         if hasattr(funcs, "__call__"):
199             funcs = funcs()
200         fget = funcs.get("fget")
201         fset = funcs.get("fset")
202         name = funcs.get("name", "<unknown>")
203         def _fget(self):
204             value = fget(self)
205             if value == null:
206                 if mutable_default == True:
207                     return copy.deepcopy(default)
208                 else:
209                     return default
210             return value
211         def _fset(self, value):
212             if value == default:
213                 value = null
214             fset(self, value)
215         funcs["fget"] = _fget
216         funcs["fset"] = _fset
217         return funcs
218     return decorator
219
220 def fn_checked_property(value_allowed_fn):
221     """
222     Define allowed values for get/set access to a property.
223     """
224     def decorator(funcs):
225         if hasattr(funcs, "__call__"):
226             funcs = funcs()
227         fget = funcs.get("fget")
228         fset = funcs.get("fset")
229         name = funcs.get("name", "<unknown>")
230         def _fget(self):
231             value = fget(self)
232             if value_allowed_fn(value) != True:
233                 raise ValueCheckError(name, value, value_allowed_fn)
234             return value
235         def _fset(self, value):
236             if value_allowed_fn(value) != True:
237                 raise ValueCheckError(name, value, value_allowed_fn)
238             fset(self, value)
239         funcs["fget"] = _fget
240         funcs["fset"] = _fset
241         return funcs
242     return decorator
243
244 def checked_property(allowed=[]):
245     """
246     Define allowed values for get/set access to a property.
247     """
248     def decorator(funcs):
249         if hasattr(funcs, "__call__"):
250             funcs = funcs()
251         fget = funcs.get("fget")
252         fset = funcs.get("fset")
253         name = funcs.get("name", "<unknown>")
254         def _fget(self):
255             value = fget(self)
256             if value not in allowed:
257                 raise ValueCheckError(name, value, allowed)
258             return value
259         def _fset(self, value):
260             if value not in allowed:
261                 raise ValueCheckError(name, value, allowed)
262             fset(self, value)
263         funcs["fget"] = _fget
264         funcs["fset"] = _fset
265         return funcs
266     return decorator
267
268 def cached_property(generator, initVal=None, mutable=False):
269     """
270     Allow caching of values generated by generator(instance), where
271     instance is the instance to which this property belongs.  Uses
272     ._<name>_cache to store a cache flag for a particular owner
273     instance.
274
275     When the cache flag is True or missing and the stored value is
276     initVal, the first fget call triggers the generator function,
277     whose output is stored in _<name>_cached_value.  That and
278     subsequent calls to fget will return this cached value.
279
280     If the input value is no longer initVal (e.g. a value has been
281     loaded from disk or set with fset), that value overrides any
282     cached value, and this property has no effect.
283
284     When the cache flag is False and the stored value is initVal, the
285     generator is not cached, but is called on every fget.
286
287     The cache flag is missing on initialization.  Particular instances
288     may override by setting their own flag.
289
290     In the case that mutable == True, all caching is disabled and the
291     generator is called whenever the cached value would otherwise be
292     used.
293     """
294     def decorator(funcs):
295         if hasattr(funcs, "__call__"):
296             funcs = funcs()
297         fget = funcs.get("fget")
298         name = funcs.get("name", "<unknown>")
299         def _fget(self):
300             cache = getattr(self, "_%s_cache" % name, True)
301             value = fget(self)
302             if value == initVal:
303                 if cache == True and mutable == False:
304                     if hasattr(self, "_%s_cached_value" % name):
305                         value = getattr(self, "_%s_cached_value" % name)
306                     else:
307                         value = generator(self)
308                         setattr(self, "_%s_cached_value" % name, value)
309                 else:
310                     value = generator(self)
311             return value
312         funcs["fget"] = _fget
313         return funcs
314     return decorator
315
316 def primed_property(primer, initVal=None, unprimeableVal=None):
317     """
318     Just like a cached_property, except that instead of returning a
319     new value and running fset to cache it, the primer attempts some
320     background manipulation (e.g. loads data into instance.settings)
321     such that a _second_ pass through fget succeeds.  If the second
322     pass doesn't succeed (e.g. no readable storage), we give up and
323     return unprimeableVal.
324
325     The 'cache' flag becomes a 'prime' flag, with priming taking place
326     whenever ._<name>_prime is True, or is False or missing and
327     value == initVal.
328     """
329     def decorator(funcs):
330         if hasattr(funcs, "__call__"):
331             funcs = funcs()
332         fget = funcs.get("fget")
333         name = funcs.get("name", "<unknown>")
334         def _fget(self):
335             prime = getattr(self, "_%s_prime" % name, False)
336             if prime == False:
337                 value = fget(self)
338             if prime == True or (prime == False and value == initVal):
339                 primer(self)
340                 value = fget(self)
341                 if prime == False and value == initVal:
342                     return unprimeableVal
343             return value
344         funcs["fget"] = _fget
345         return funcs
346     return decorator
347
348 def change_hook_property(hook, mutable=False, default=None):
349     """Call the function `hook` whenever a value different from the
350     current value is set.
351
352     This is useful for saving changes to disk, etc.  This function is
353     called *after* the new value has been stored, allowing you to
354     change the stored value if you want.
355
356     In the case of mutables, things are slightly trickier.  Because
357     the property-owning class has no way of knowing when the value
358     changes.  We work around this by caching a private deepcopy of the
359     mutable value, and checking for changes whenever the property is
360     set (obviously) or retrieved (to check for external changes).  So
361     long as you're conscientious about accessing the property after
362     making external modifications, mutability won't be a problem::
363
364       t.x.append(5) # external modification
365       t.x           # dummy access notices change and triggers hook
366
367     See :py:class:`testChangeHookMutableProperty` for an example of the
368     expected behavior.
369
370     Parameters
371     ----------
372     hook : fn
373       `hook(instance, old_value, new_value)`, where `instance` is a
374       reference to the class instance to which this property belongs.
375     """
376     def decorator(funcs):
377         if hasattr(funcs, "__call__"):
378             funcs = funcs()
379         fget = funcs.get("fget")
380         fset = funcs.get("fset")
381         name = funcs.get("name", "<unknown>")
382         def _fget(self, new_value=None, from_fset=False): # only used if mutable == True
383             if from_fset == True:
384                 value = new_value # compare new value with cached
385             else:
386                 value = fget(self) # compare current value with cached
387             if _cmp_cached_mutable_property(self, "change hook property", name, value, default) != 0:
388                 # there has been a change, cache new value
389                 old_value = _get_cached_mutable_property(self, "change hook property", name, default)
390                 _set_cached_mutable_property(self, "change hook property", name, value)
391                 if from_fset == True: # return previously cached value
392                     value = old_value
393                 else: # the value changed while we weren't looking
394                     hook(self, old_value, value)
395             return value
396         def _fset(self, value):
397             if mutable == True: # get cached previous value
398                 old_value = _fget(self, new_value=value, from_fset=True)
399             else:
400                 old_value = fget(self)
401             fset(self, value)
402             if value != old_value:
403                 hook(self, old_value, value)
404         if mutable == True:
405             funcs["fget"] = _fget
406         funcs["fset"] = _fset
407         return funcs
408     return decorator
409
410 if libbe.TESTING == True:
411     class DecoratorTests(unittest.TestCase):
412         def testLocalDoc(self):
413             class Test(object):
414                 @Property
415                 @doc_property("A fancy property")
416                 def x():
417                     return {}
418             self.failUnless(Test.x.__doc__ == "A fancy property",
419                             Test.x.__doc__)
420         def testLocalProperty(self):
421             class Test(object):
422                 @Property
423                 @local_property(name="LOCAL")
424                 def x():
425                     return {}
426             t = Test()
427             self.failUnless(t.x == None, str(t.x))
428             t.x = 'z' # the first set initializes ._LOCAL_value
429             self.failUnless(t.x == 'z', str(t.x))
430             self.failUnless("_LOCAL_value" in dir(t), dir(t))
431             self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value)
432         def testSettingsProperty(self):
433             class Test(object):
434                 @Property
435                 @settings_property(name="attr")
436                 def x():
437                     return {}
438                 def __init__(self):
439                     self.settings = {}
440             t = Test()
441             self.failUnless(t.x == None, str(t.x))
442             t.x = 'z' # the first set initializes ._LOCAL_value
443             self.failUnless(t.x == 'z', str(t.x))
444             self.failUnless("attr" in t.settings, t.settings)
445             self.failUnless(t.settings["attr"] == 'z', t.settings["attr"])
446         def testDefaultingLocalProperty(self):
447             class Test(object):
448                 @Property
449                 @defaulting_property(default='y', null='x')
450                 @local_property(name="DEFAULT", null=5)
451                 def x(): return {}
452             t = Test()
453             self.failUnless(t.x == 5, str(t.x))
454             t.x = 'x'
455             self.failUnless(t.x == 'y', str(t.x))
456             t.x = 'y'
457             self.failUnless(t.x == 'y', str(t.x))
458             t.x = 'z'
459             self.failUnless(t.x == 'z', str(t.x))
460             t.x = 5
461             self.failUnless(t.x == 5, str(t.x))
462         def testCheckedLocalProperty(self):
463             class Test(object):
464                 @Property
465                 @checked_property(allowed=['x', 'y', 'z'])
466                 @local_property(name="CHECKED")
467                 def x(): return {}
468                 def __init__(self):
469                     self._CHECKED_value = 'x'
470             t = Test()
471             self.failUnless(t.x == 'x', str(t.x))
472             try:
473                 t.x = None
474                 e = None
475             except ValueCheckError, e:
476                 pass
477             self.failUnless(type(e) == ValueCheckError, type(e))
478         def testTwoCheckedLocalProperties(self):
479             class Test(object):
480                 @Property
481                 @checked_property(allowed=['x', 'y', 'z'])
482                 @local_property(name="X")
483                 def x(): return {}
484
485                 @Property
486                 @checked_property(allowed=['a', 'b', 'c'])
487                 @local_property(name="A")
488                 def a(): return {}
489                 def __init__(self):
490                     self._A_value = 'a'
491                     self._X_value = 'x'
492             t = Test()
493             try:
494                 t.x = 'a'
495                 e = None
496             except ValueCheckError, e:
497                 pass
498             self.failUnless(type(e) == ValueCheckError, type(e))
499             t.x = 'x'
500             t.x = 'y'
501             t.x = 'z'
502             try:
503                 t.a = 'x'
504                 e = None
505             except ValueCheckError, e:
506                 pass
507             self.failUnless(type(e) == ValueCheckError, type(e))
508             t.a = 'a'
509             t.a = 'b'
510             t.a = 'c'
511         def testFnCheckedLocalProperty(self):
512             class Test(object):
513                 @Property
514                 @fn_checked_property(lambda v : v in ['x', 'y', 'z'])
515                 @local_property(name="CHECKED")
516                 def x(): return {}
517                 def __init__(self):
518                     self._CHECKED_value = 'x'
519             t = Test()
520             self.failUnless(t.x == 'x', str(t.x))
521             try:
522                 t.x = None
523                 e = None
524             except ValueCheckError, e:
525                 pass
526             self.failUnless(type(e) == ValueCheckError, type(e))
527         def testCachedLocalProperty(self):
528             class Gen(object):
529                 def __init__(self):
530                     self.i = 0
531                 def __call__(self, owner):
532                     self.i += 1
533                     return self.i
534             class Test(object):
535                 @Property
536                 @cached_property(generator=Gen(), initVal=None)
537                 @local_property(name="CACHED")
538                 def x(): return {}
539             t = Test()
540             self.failIf("_CACHED_cache" in dir(t),
541                         getattr(t, "_CACHED_cache", None))
542             self.failUnless(t.x == 1, t.x)
543             self.failUnless(t.x == 1, t.x)
544             self.failUnless(t.x == 1, t.x)
545             t.x = 8
546             self.failUnless(t.x == 8, t.x)
547             self.failUnless(t.x == 8, t.x)
548             t._CACHED_cache = False        # Caching is off, but the stored value
549             val = t.x                      # is 8, not the initVal (None), so we
550             self.failUnless(val == 8, val) # get 8.
551             t._CACHED_value = None         # Now we've set the stored value to None
552             val = t.x                      # so future calls to fget (like this)
553             self.failUnless(val == 2, val) # will call the generator every time...
554             val = t.x
555             self.failUnless(val == 3, val)
556             val = t.x
557             self.failUnless(val == 4, val)
558             t._CACHED_cache = True              # We turn caching back on, and get
559             self.failUnless(t.x == 1, str(t.x)) # the original cached value.
560             del t._CACHED_cached_value          # Removing that value forces a
561             self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call
562             self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which
563             self.failUnless(t.x == 5, str(t.x)) # we get the new cached value.
564         def testPrimedLocalProperty(self):
565             class Test(object):
566                 def prime(self):
567                     self.settings["PRIMED"] = self.primeVal
568                 @Property
569                 @primed_property(primer=prime, initVal=None, unprimeableVal=2)
570                 @settings_property(name="PRIMED")
571                 def x(): return {}
572                 def __init__(self):
573                     self.settings={}
574                     self.primeVal = "initialized"
575             t = Test()
576             self.failIf("_PRIMED_prime" in dir(t),
577                         getattr(t, "_PRIMED_prime", None))
578             self.failUnless(t.x == "initialized", t.x)
579             t.x = 1
580             self.failUnless(t.x == 1, t.x)
581             t.x = None
582             self.failUnless(t.x == "initialized", t.x)
583             t._PRIMED_prime = True
584             t.x = 3
585             self.failUnless(t.x == "initialized", t.x)
586             t._PRIMED_prime = False
587             t.x = 3
588             self.failUnless(t.x == 3, t.x)
589             # test unprimableVal
590             t.x = None
591             t.primeVal = None
592             self.failUnless(t.x == 2, t.x)
593         def testChangeHookLocalProperty(self):
594             class Test(object):
595                 def _hook(self, old, new):
596                     self.old = old
597                     self.new = new
598
599                 @Property
600                 @change_hook_property(_hook)
601                 @local_property(name="HOOKED")
602                 def x(): return {}
603             t = Test()
604             t.x = 1
605             self.failUnless(t.old == None, t.old)
606             self.failUnless(t.new == 1, t.new)
607             t.x = 1
608             self.failUnless(t.old == None, t.old)
609             self.failUnless(t.new == 1, t.new)
610             t.x = 2
611             self.failUnless(t.old == 1, t.old)
612             self.failUnless(t.new == 2, t.new)
613         def testChangeHookMutableProperty(self):
614             class Test(object):
615                 def _hook(self, old, new):
616                     self.old = old
617                     self.new = new
618                     self.hook_calls += 1
619
620                 @Property
621                 @change_hook_property(_hook, mutable=True)
622                 @local_property(name="HOOKED")
623                 def x(): return {}
624             t = Test()
625             t.hook_calls = 0
626             t.x = []
627             self.failUnless(t.old == None, t.old)
628             self.failUnless(t.new == [], t.new)
629             self.failUnless(t.hook_calls == 1, t.hook_calls)
630             a = t.x
631             a.append(5)
632             t.x = a
633             self.failUnless(t.old == [], t.old)
634             self.failUnless(t.new == [5], t.new)
635             self.failUnless(t.hook_calls == 2, t.hook_calls)
636             t.x = []
637             self.failUnless(t.old == [5], t.old)
638             self.failUnless(t.new == [], t.new)
639             self.failUnless(t.hook_calls == 3, t.hook_calls)
640             # now append without reassigning.  this doesn't trigger the
641             # change, since we don't ever set t.x, only get it and mess
642             # with it.  It does, however, update our t.new, since t.new =
643             # t.x and is not a static copy.
644             t.x.append(5)
645             self.failUnless(t.old == [5], t.old)
646             self.failUnless(t.new == [5], t.new)
647             self.failUnless(t.hook_calls == 3, t.hook_calls)
648             # however, the next t.x get _will_ notice the change...
649             a = t.x
650             self.failUnless(t.old == [], t.old)
651             self.failUnless(t.new == [5], t.new)
652             self.failUnless(t.hook_calls == 4, t.hook_calls)
653             t.x.append(6) # this append(6) is not noticed yet
654             self.failUnless(t.old == [], t.old)
655             self.failUnless(t.new == [5,6], t.new)
656             self.failUnless(t.hook_calls == 4, t.hook_calls)
657             # this append(7) is not noticed, but the t.x get causes the
658             # append(6) to be noticed
659             t.x.append(7)
660             self.failUnless(t.old == [5], t.old)
661             self.failUnless(t.new == [5,6,7], t.new)
662             self.failUnless(t.hook_calls == 5, t.hook_calls)
663             a = t.x # now the append(7) is noticed
664             self.failUnless(t.old == [5,6], t.old)
665             self.failUnless(t.new == [5,6,7], t.new)
666             self.failUnless(t.hook_calls == 6, t.hook_calls)
667
668     suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests)