1#!/usr/bin/env python 2 3class autoprop(object): 4 5 __version__ = '1.0.1' 6 property = type('property', (property,), {}) 7 8 class _Cache: 9 __slots__ = ('value', 'is_stale') 10 11 def __init__(self): 12 self.value = None 13 self.is_stale = True 14 15 def __repr__(self): 16 import sys 17 return '{}(value={!r}, is_stale={!r})'.format( 18 self.__class__.__qualname__ if sys.version_info[0] >= 3 else self.__class__.__name__, 19 self.value, 20 self.is_stale, 21 ) 22 23 def __new__(autoprop, cls): 24 # These imports have to be inside autoprop(), otherwise the sys.modules 25 # hack below somehow makes them unavailable when the decorator is 26 # applied. 27 import inspect, re 28 from collections import defaultdict 29 from functools import wraps 30 31 if not hasattr(cls, '__class__'): 32 raise TypeError('@autoprop can only be used with new-style classes') 33 34 accessors = defaultdict(dict) 35 expected_num_args = {'get': 0, 'set': 1, 'del': 0} 36 37 # The accessors we're searching for are considered methods in python2 38 # and functions in python3. They behave the same either way. 39 ismethod = lambda x: inspect.ismethod(x) or inspect.isfunction(x) 40 41 for method_name, method in inspect.getmembers(cls, ismethod): 42 accessor_match = re.match('(get|set|del)_(.+)', method_name) 43 if not accessor_match: 44 continue 45 46 # Suppress a warning by using getfullargspec() if it's available 47 # and getargspec() if it's not. 48 try: from inspect import getfullargspec as getargspec 49 except ImportError: from inspect import getargspec 50 51 prefix, name = accessor_match.groups() 52 arg_spec = getargspec(method) 53 num_args = len(arg_spec.args) - len(arg_spec.defaults or ()) 54 num_args_minus_self = num_args - 1 55 56 if num_args_minus_self != expected_num_args[prefix]: 57 continue 58 59 accessors[name][prefix] = method 60 61 def get_cache(self, getter): 62 if not hasattr(self, '_autoprop_cache'): 63 self._autoprop_cache = {} 64 return self._autoprop_cache.setdefault(getter, autoprop._Cache()) 65 66 def use_cache(getter): 67 68 @wraps(getter) 69 def wrapper(self, *args, **kwargs): 70 cache = get_cache(self, getter) 71 72 if cache.is_stale: 73 cache.value = getter(self, *args, **kwargs) 74 cache.is_stale = False 75 76 return cache.value 77 78 return wrapper 79 80 def clear_cache(f, getter): 81 if f is None: 82 return None 83 84 @wraps(f) 85 def wrapper(self, *args, **kwargs): 86 get_cache(self, getter).is_stale = True 87 return f(self, *args, **kwargs) 88 89 return wrapper 90 91 for name in accessors: 92 try: 93 attr = getattr(cls, name) 94 ok_to_overwrite = isinstance(attr, autoprop.property) 95 except AttributeError: 96 ok_to_overwrite = True 97 98 getter = accessors[name].get('get') 99 setter = accessors[name].get('set') 100 deleter = accessors[name].get('del') 101 102 # Cache the return value of the getter, if requested. For some 103 # reason setting attributes on method objects 104 if getter and getattr(getter, '_autoprop_mark_cache', False): 105 setter = clear_cache(setter, getter) 106 deleter = clear_cache(deleter, getter) 107 getter = use_cache(getter) 108 109 if ok_to_overwrite: 110 property = autoprop.property(getter, setter, deleter) 111 setattr(cls, name, property) 112 113 return cls 114 115 @staticmethod 116 def cache(f): 117 if not f.__name__.startswith('get_'): 118 import sys 119 raise ValueError("{}() cannot be cached; it's not a getter".format( 120 f.__qualname__ if sys.version_info[0] >= 3 else f.__name__ 121 )) 122 123 f._autoprop_mark_cache = True 124 return f 125 126# Abuse the import system so that the module itself can be used as a decorator. 127# This is a simple module intended only to cut-down on boilerplate, so I think 128# the trade-off between magicalness and ease-of-use is justified in this case. 129import sys 130sys.modules[__name__] = autoprop 131