diff options
Diffstat (limited to 'bps/meta.py')
| -rw-r--r-- | bps/meta.py | 1190 |
1 files changed, 1190 insertions, 0 deletions
diff --git a/bps/meta.py b/bps/meta.py new file mode 100644 index 0000000..d2a1fbb --- /dev/null +++ b/bps/meta.py @@ -0,0 +1,1190 @@ +"""bps.meta - introspection utilities""" +#=================================================== +#imports +#=================================================== +from __future__ import absolute_import +#core +import os.path +import inspect +from functools import update_wrapper, partial +from logging import getLogger; log = getLogger(__name__) +import time +import sys +from warnings import warn +from weakref import WeakKeyDictionary +import types #python module, NOT bps.types +#needed imports +#legacy imports +from bps.undef import Undef +from bps.error.types import AbstractMethodError + +__all__ = [ + #interfaces + 'isseq', 'isnum', 'isstr', + + #introspection & monkeypatching + 'is_overridden', + 'find_attribute', +## 'get_module', +## 'get_module_exports', + 'monkeypatch', + 'monkeypatch_mixin', + 'instrument_super', + + #other decorators + 'abstract_method', 'abstract_property', 'AbstractMethodError', + 'decorate_per_instance', +] + +#========================================================= +#interface tests +#========================================================= +#XXX: should these groups be moved into bps.types? +# which place would new users expect them to be located? + +#XXX: should we standardize on is_xxx or isxxx; or support both? +# these funcs are so lightweight, isxxx is probably better + +NumericTypes = (int, float, long) #XXX: add decimal? +SequenceTypes = (list, tuple, set) #heterogenous sequence types (ie, sequences excluding string) +OrderedTypes = (list, tuple) #heterogenous ordered sequences types +if sys.version >= (3, 0): + StringTypes = (str,) + ClassTypes = (type,) + _classobj = None +else: + StringTypes = types.StringTypes + _classobj = types.ClassType + ClassTypes = (types.ClassType, types.TypeType) + +def is_seq(obj): + "tests if *obj* is a known heterogenous sequence type" + return isinstance(obj, SequenceTypes) +isseq = is_seq + +def is_oseq(obj): + "tests if *obj* is a known ordered heterogenous sequence" + return isinstance(obj, OrderedTypes) +isoseq = is_oseq + +def is_num(obj): + "tests if *obj* is a known numeric type" + return isinstance(obj, NumericTypes) +isnum = is_num + +#add is_integral / is_real ? + +def is_str(obj): + "tests if *obj* is a known string type" + return isinstance(obj, StringTypes) +isstr = is_str + +def is_class(obj): + "test if *obj* is old or new style class object" + return isinstance(obj, ClassTypes) +isclass = is_class + +def is_pair(obj): + "check if object is an ordered pair" + return is_oseq(obj) and len(obj) == 2 +ispair = is_pair + +def is_iter(obj): + "check if object is an iterator/generator" + return hasattr(obj,"next") and hasattr(obj,"__iter__") + +def hasattrs(obj, include=None, exclude=None): + "helper for testing if object matches expected protocol by checking for whole sets of attrs" + return (not include or all(hasattr(obj, attr) for attr in include)) \ + and not (exclude and any(hasattr(obj,attr) for attr in exclude)) + +#========================================================= +#class inspection +#========================================================= +def is_overridden(attr, owner, parent): + """check if a method has been shadowed. + + :arg attr: + the method name to check + :arg owner: + the object (instance or class) to check + :param parent: + parent class to compare against. + :returns: + returns ``True`` if *attr*, as defined by *parent*, + is overridden by *owner* or any class between it and parent. + Otherwise, returns ``False``. + """ + #TODO: this currently only deals with methods, could extend if needed + #TODO: could make parent default to immediate parent of owner + new = getattr(owner, attr) + old = getattr(parent, attr) + return getattr(new, "im_func", None) is not old.im_func + +def find_attribute(owner, target, first=True, required=False): + """search class hierarchy of *owner* to find which attribute *target* + is being stored under. + + Given a *target* object, and an *owner* object to search (either the class or an instance), + try to determine what attribute of the class the property is stored under. + By default this will return the first attribute the target object is found at. + If it is not found, ``None`` will be returned. + + This is useful for property constructors which need to introspect + and find what attribute they have been stored under at runtime (see example below). + + :Parameters: + owner + The object which should be scanned for the property. + This may be an instance or a class. + + target + The property (or other object such as a function) to search + through the attrs of the class hierarchy for. + + first : bool + If set to ``False``, *all* attributes the match + will be returned as a list. Otherwise only + the first match will be returned. + + required : bool + If true, a RuntimeError will be raised if the target cannot be found. + + An example of how to use this in a property class:: + + >>> class LazyConstructor(object): + >>> def __init__(self, func): + >>> self.func = func + >>> self.name = None + >>> def __get__(self, obj, cls): + >>> if obj is None: + >>> return self + >>> if self.name is None: #cache result for later + >>> self.name = find_attribute(cls, self, required=True) + >>> assert self.name not in obj.__dict__ + >>> value = obj.__dict__[self.name] = self.func() + >>> #we should never get called again for this object + >>> return value + + """ + #resolve instances to their class type if needed + if not isinstance(owner, ClassTypes): + owner = type(owner) + if hasattr(owner, "__mro__"): + mro = owner.__mro__[:-1] #remove 'object' cause we don't need to search it + elif hasattr(owner, "__bases__"): + assert isinstance(owner, types.ClassType) + mro = inspect.getmro(owner) + else: + raise RuntimeError, "unknown object type: %r" % (owner,) + #traverse class dicts using MRO, bypassing getattr & property code + if not first: + out = [] + for cls in mro: + for k,v in cls.__dict__.iteritems(): + if v is target: + if first: + return k + else: + out.append(k) + if first: + log.warning("find_attribute failed: owner=%r target=%r", owner, target) + if not required: + return None + else: + if out or not required: + return out + raise RuntimeError, "object %r does not appear in the class dictionary of %r" % (target, owner) + +def get_cls_kwds(cls): + """Return list of keyword arguments accepted by the specified class's constructor. + + This performs it's job by recursively examining + the __init__ methods in the specified class and it's bases. + + .. todo:: + Could look for a class attribute such as "__kwds__" or something, + but should survey if any major projects have set up a de facto standard first. + """ + kwds = set() #set of kwds we've seen + self_kwds = set() #set of kwds used by class as 'self' argument (usually just contains 'self') + + #for each class in MRO, read arguments of it's init method + for c in cls.__mro__: + f = c.__dict__.get("__init__") + if not isinstance(f, types.FunctionType): + continue + #only if it's a function object do we check + #list of names, and **kwds slot. + names, _, varkwds, _ = inspect.getargspec(f) + if not names: + continue + self_name = names[0] + if self_name not in kwds: + #only store a function's "self" argument + #if that kwd wasn't being shadowed by a higher-level + #__init__ method... that way, when we remove all self-names + #below, we don't remove shadowed self-names + self_kwds.add(self_name) + kwds.update(names) + if not varkwds: + #if it doesn't support varkwds, + #assume we don't need to go higher up class hierarchy. + break + #remove all kwds being used for "self" + kwds.difference_update(self_kwds) + return list(kwds) + +##def get_func_kwds(func): +## """Return list of legal kwd args for the given function""" +## return inspect.getargspec(func)[0] + +def func_accepts_key(func, key): + """test if function accepts a given kwd parameter. + + :arg func: + function or class object. + (if a class, the __init__ method is examined). + :arg key: + the key (or list of keys) which must be accepted. + + :returns: + ``True`` if the keyword is accepted, else ``False``. + """ + #check this is a class + method = False + if isclass(func): + #TODO: make use get_cls_kwds, but need to make sure nothing + #relies on this function's **kwds behavior. +## return key in get_cls_kwds(func) + func = func.__init__.im_func + method = True + elif isinstance(func, partial): + while isinstance(func, partial): + func = func.func + args, varargs, varkw, defaults = inspect.getargspec(func) + if bool(varkw): + #XXX: is there some de facto protocol to check for this? + # for now, just assume it accepts everything + return True + if method: + args = args[1:] + if is_seq(key): + return all(k in args for k in key) + else: + return key in args + +#========================================================= +#module inspection +#========================================================= +def get_module(name): + "return module by absolute name" + return __import__(name, None, None, ['dummy'], 0) + +def get_module_exports(module): + """return list of attrs exported from module by default. + + This is the same as ``list(module.__all__)`` IF the module defined that variable. + Otherwise, this returns a list approximating the default python behavior. + """ + if is_str(module): + module = get_module(module) + try: + return list(module.__all__) + except AttributeError: + return [n for n in dir(module) if not n.startswith("_")] + +_script_exts = set([".py", ".pyc", ".pyo"]) +if os.name == "posix": + _cmod_exts = set([".so"]) +elif os.name == "nt": + _cmod_exts = set([".pyd"]) + _script_exts.add(".pyw") +else: + #TODO: what do other os's use? + _cmod_exts = set() + +def lookup_module(path, name=False): + """find loaded module given path it came from. + + given a path to a .py file or a package directory, + this attempts to find the loaded module that was derived + from the file. this attempts to be the reverse of ``module.__path``, + using inspection of ``sys.modules``. it's not perfect, + but there's no analog in the inspect module. + + :arg path: + path to python file or package directory. + + :param name: + optionally this function can return the full name + of the module, rather than the module itself. + + :returns: + name or module instance (see name flag) if + a loaded module was found which matches path. + + if no corresponding module has been loaded yet, + or the path does not correspond to a python module, + returns ``None``. + """ + global _script_exts, _cmod_exts + #FIXME: gotta be a better way to figure this info out, + #inspect doesn't seem to have the right func for the job. + + if os.path.isfile(path): + #figure out what type of file this is + root, ext = os.path.splitext(path) + root = os.path.abspath(root) + if ext in _script_exts: + #if it's a script, search for all known script extensions + targets = set(root + ext for ext in _script_exts) + elif ext in _cmod_exts: + #if it's a compiled module, search for all known compiled module extensions + targets = set(root + ext for ext in _cmod_exts) + else: + #no idea what to do + log.warning("lookup_module(): path has unknown extension: %r", path) + return None + test = targets.__contains__ + elif os.path.isdir(path): + #assume it's a package dir, and set root to be the init file inside the directory + root = os.path.abspath(os.path.join(path, "__init__")) + targets = set(root + ext for ext in _script_exts) + test = targets.__contains__ + elif not os.path.exists(path): + log.warning("lookup_module(): path doesn't exist: %r", path) + return None + else: + log.warning("lookup_module(): unsupported file type: %r", path) + return None + + #try and find target in loaded modules + #NOTE: would like to use iteritems(), but dict changes size on us :( + #FIXME: there may be multiple matches (eg: posixpath and os.path), + # and this just returns the first one it finds. we could do better, + # say returning all of them if requested, or preferring one + # whose name matches the pathname (ie, original over aliases) + for mod_name, module in sys.modules.items(): + mod_file = getattr(module, "__file__", None) + #XXX: do we need to run mod_path through abspath ? + if mod_file and test(mod_file): + log.debug("lookup_module(): resolved path to module: %r => %r", path, mod_name) + if name: + return mod_name + else: + return module + + #give up + log.warning("lookup_module(): can't resolve path to loaded module: %r", path) + return None + +#========================================================= +#class manipulation +#========================================================= +##def get_private_attr(obj, attr, default=Undef, name=Undef): +## if name is Undef: +## if isinstance(obj,type): +## name = obj.__name__ +## else: +## name = obj.__class__.__name__ +## if name[0] == "_": +## attr = "%s__%s" % (name,attr) +## else: +## astr = "_%s__%s" % (name,attr) +## if default is Undef: +## return getattr(obj,astr) +## else: +## return getattr(obj,astr,default) +## +##def set_private_attr(obj, attr, value, name=Undef): +## if name is Undef: +## if isinstance(obj,type): +## name = obj.__name__ +## else: +## name = obj.__class__.__name__ +## if name[0] == "_": +## attr = "%s__%s" % (name,attr) +## else: +## astr = "_%s__%s" % (name,attr) +## return getattr(obj,astr,value) + +#========================================================= +#monkeypatching +#========================================================= +def monkeypatch(target, attr=None, wrap=False, clobber=True): + """Decorator to aid in monkeypatching. + + The decorated function will be patched into `target` + under the attribute same name as the wrapped function. + The attribute can be overriden via the `attr` kwd. + + This was posted by GVR somewhere on the internet. + *It's not just evil, it's easy-to-use evil!* + Pretend this isn't here unless you really need it. + + :Parameters: + target + the target object which we're replacing an attribute of + attr + [optional] + attribute to be replaced. if not specified, + taken from the name of the function this decorates. + wrap + if true, original value will be passed in as first positional argument, + if false (the default), it will be discarded. + clobber + By default, this function will overwrite any existing + value stored in the target attribute. If this is set + to ``False``, an error will be raised if the attribute + contains data. + + Usage:: + + >>> from bps.meta import monkeypatch + >>> #say we have a class... + >>> class MyClass(object): + >>> def a(self, x=10): + >>> return x+1 + >>> m = MyClass() + >>> m.a() + 11 + >>> #and later we want to patch method 'a' + >>> @monkeypatch(MyClass) + >>> def a(self, x=10): + return x*2 + >>> m.a() + 20 + >>> #say we want to patch it (again) while calling previous copy + >>> @monkeypatch(MyClass, wrap=True) + >>> def a(orig, self, x=10): + >>> return orig(self,x)+5 + >>> m.a() + 25 + """ + def builder(func): + name = attr or func.__name__ + if not clobber and getattr(target, name, None): + raise AttributeError, "monkeypatch target already exists: target=%r attr=%r" % (target, name) + if wrap: + orig = getattr(target, name) + if isinstance(target, type): + #can't use partial since it's going in a class + def wrapper(*a, **k): + return func(orig, *a, **k) + update_wrapper(wrapper, orig) + setattr(target, name, wrapper) + return + func = partial(func, orig) + setattr(target, name, func) + return builder + +def monkeypatch_mixin(target, first=False): + """Modify a class by appending another class to it's list of bases. + This is mainly useful for monkeypatching a mixin class. + + :arg target: class to be patched + :param first: if mixin should be placed at beginning of bases, not end + + Usage:: + + >>> from bps.meta import monkeypatch_mixin + >>> #say you have a class... + >>> class MyClass(object): + >>> pass + >>> MyClass.__bases__ + (object,) + >>> #and somewhere else, you want to patch one in + >>> class OtherClass(object): + >>> pass + >>> monkeypatch_mixin(MyClass)(OtherClass) + >>> MyClass.__bases__ + (object,OtherClass) + + .. note:: + If target is subclass of mixin, + this function will silently do nothing. + """ + def builder(mixin): + #check if it's already merged in + if issubclass(target, mixin): + return mixin + + #check if we can't due to circular ref + if issubclass(mixin, target): + raise TypeError, "mixin %r cannot derive from target %r" % (mixin, target) + + #TODO: figure out greatest common ancestor, + # and (if it's in target.__bases__, just replace it w/ mixin) + if first: + target.__bases__ = (mixin,) + target.__bases__ + else: + target.__bases__ += (mixin,) + + return mixin + + return builder + +#========================================================= +#source code inspection +#========================================================= +_cache = {} #FIXME: make weakkeyref? +def get_class_range(cls): + """given a class, returns a tuple of ``(path,first,last)``, defined as follows: + + path + filepath that class was defined in (may not exist, just used for identification) + + first + smallest firstlineno of any method defined in the class + + last + largest firstlineno of any method defined in the class + + .. note:: + This function is mainly used by :func:`init_super_property`. + It may have some strange behaviors related to that use-case, + which may need to be cleared up when other use-cases are found. + """ + global _cache + if cls in _cache: + return _cache[cls] + + #first, figure out path of class + path = getattr(sys.modules.get(cls.__module__), "__file__", None) + if path is None: + #builtin, return fake info + log.debug("get_class_range(%r): <builtin>", cls) + return None, None, None + npath = _get_base_path(path) + + #find largest & smallest firstlineno of all of class's methods + #which are defined in class path + first = None + last = None + for k, v in cls.__dict__.iteritems(): + if hasattr(v, "__get__") and not isinstance(v, (MultipleSuperProperty, SingleSuperProperty)): + v = v.__get__(None, cls) + if hasattr(v, "im_func"): + c = v.im_func.func_code + elif hasattr(v, "func_code"): + c = v.func_code + else: + continue + if _get_base_path(c.co_filename) != npath: + continue + if first is None or c.co_firstlineno < first: + first = c.co_firstlineno + if last is None or c.co_firstlineno > last: + last = c.co_firstlineno + log.debug("get_class_range(%r): path=%r start=%r end=%r", cls, path, first, last) + return path, first, last + +def _get_base_path(path): + """helper func used to normalize filepaths + returned by getfile(cls) and func.co_filename paths" + """ + return os.path.splitext(os.path.abspath(path))[0] + +#========================================================= +#super descriptor +#========================================================= +class SingleSuperProperty(object): + """helper for init_super_property() which provides + a __super attribute for a single class.""" + __thisclass__ = None #class super() should resolve relative to + + def __init__(self, cls): + self.__thisclass__ = cls + + def __get__(self, obj, cls): + if obj is None: obj=cls + return super(self.__thisclass__, obj) + + def __delete__(self, obj): + raise ValueError, "__super attributes are read-only" + + def __set__(self, obj, value): + raise ValueError, "__super attributes are read-only" + +class MultipleSuperProperty(object): + """helper for init_super_property() which provides + a __super attribute which uses different __thisclass__ values + depending on which class accesses the attribute. + + it takes as input the class name it's managing __$NAME__super for, + and (via stack inspection), picks the class with that name which seems + to contain the calling code. the algorithm works reliably for most cases, + including multiple classes in the same file, sharing the same name. + + however, situtations such as explicitly accessing the private namespace + from code which lies outside the class are not handled, as there seems + to be no "best" behavior in such a situation. this case is pretty rare, + however. + + another drawback is this is a rather complicated and expensive algorithm. + luckily, init_super_property() only uses this property + when multiple classes are sharing the same private namespace. + """ + name = None #: name of classes whose shared namespace this manages __super for + + def __init__(self, name): + self.name = name + + def __get__(self, obj, cls): + #get list of all parent classes with the desired name + name = self.name + choices = [ + c for c in cls.__mro__ + if c.__name__ == name + ] + if len(choices) == 0: + raise RuntimeError, "no classes named %r found in mro of %r" % (name, cls) + elif len(choices) == 1: + thisclass = choices[0] + else: + #remove candidates with different module path + frame = inspect.currentframe(1) + path = frame.f_globals.get("__file__") + choices = [ + c for c in choices if + getattr(sys.modules.get(c.__module__), "__file__", None) == path + ] + if len(choices) == 0: + raise RuntimeError, "no classes named %r from file %r found in mro of %r" % (name, path, cls) + elif len(choices) == 1: + thisclass = choices[0] + else: + #now the unreliable part: + #try and guess which class defined the frame's code, + #based on the line numbers used by the candidates's methods. + target = frame.f_code.co_firstlineno + def match_class(cls): + _path, first, last = get_class_range(cls) + assert _path == path, "class unexpected changed path" + if first is None: + return True #XXX: discard cls if we have no info? + return first <= target and last >= target + choices = [ + c for c in choices + if match_class(c) + ] + if len(choices) == 0: + raise RuntimeError, "no classes named %r from file %r including line %r found in mro of %r" % (name, path, target, cls) + elif len(choices) == 1: + thisclass = choices[0] + else: + #FIXME: what do we do now? + #multiple classes in same file, seemingly both including target lineno. + #kinda weird. + log.warning("multiple matches for thisclass: name=%r path=%r line=%r choices=%r", name, path, target, choices) + #only case i can think of where this could occur is two nested classes w/ same name, + #so for now, we pick the one w/ largest starting lineno + #FIXME: what about case where one of choices had first=None? + thisclass = choices[0] + first = get_class_range(thisclass)[1] + for choice in choices[1:]: + v = get_class_range(choice)[1] + if v > first: + thisclass = choice + first = v + + #ok, thisclass has been chosen, so generate super() + if obj is None: + obj = cls + return super(thisclass, obj) + + def __delete__(self, obj, cls): + raise ValueError, "__super attributes are read-only" + + def __set__(self, obj, value): + raise ValueError, "__super attributes are read-only" + +def instrument_super(cls, optimize=True): + """Sets up a ``__super`` descriptor in the private namespace of the specified class. + + This function should be able to instrument any class which inherits from :class:`object`. + + :param optimize: + Setting this to ``False`` disables the fast __super implementation, + when normally, the choice will be autodetected. + This is mainly a helper for when autodetection fails. + + Usage Example:: + + >>> class MyClass(object): + >>> def __init__(self, **kwds): + >>> self.__super.__init__(**kwds) + >>> init_super_property(MyClass) #calling this makes self.__super work, above. + + .. note:: + This is not needed if you are inheriting from :class:`bps.types.BaseClass`, + as that class takes care of calling this function for all subclasses. + + .. note:: + Since this method of automatic ``super()`` support relies on the class's + private namespace being unique, two classes in the mro with the same + name will have to share a single attribute. The code behind this function + attempts to compensate for this case, but occasionally may get confused. + + .. warning:: + This function assumes it will be called on a parent + class before it's child classes, or never called for the parent classes. + The remaining case (where it's called to instrument a parent class + AFTER it's been called on a child class) messes up the autodetection algorithm. + + """ + attr = "_%s__super" % cls.__name__.lstrip("_") + #check to see if another class using the same namespace already has __super defined... + #if so, we have to use the less desirable _MultipleSuperProperty() + #XXX: this check fails to detect if parent __super is initialized AFTER child __super + if not optimize or (hasattr(cls, attr) and attr not in cls.__dict__): + value = MultipleSuperProperty(cls.__name__) + else: + value = SingleSuperProperty(cls) + #set new __super property + setattr(cls, attr, value) + +#========================================================= +#other decorators +#========================================================= +def abstract_method(func): + """Method decorator which indicates this is a placeholder method which + should be overridden by subclass. + + This is mainly useful when defining framework classes that must be + subclassed before they will be useful. + + If called directly, this method will raise an :exc:`AbstractMethodError` + (which is a subclass of :exc:`NotImplementedError`). + """ + msg = "object %(self)r method %(name)r is abstract, and cannot be called" + def wrapper(self, *args, **kwds): + text = msg % dict(self=self, name=wrapper.__name__) + raise AbstractMethodError(text) + update_wrapper(wrapper, func) + return wrapper + +abstractmethod = abstract_method #for compat with python syntax + +class class_property(object): + """Decorator which acts like a combination of classmethod+property (limited to read-only)""" + + def __init__(self, func): + self.im_func = func + + def __get__(self, obj, cls): + return self.im_func(cls) + +class fallback_property(object): + """Decorator which acts like a combination of classmethod+fallback_method (limited to read-only)""" + def __init__(self, func): + self.im_func = func + + def __get__(self, obj, cls): + return self.im_func(obj, cls) + +class fallback_method(object): + """Decorator which lets method act like a class OR instance method. + + function will be called with prototype ``func(obj,cls,*args,**kwds)``, + where ``obj`` is ``None`` if invoked from a class. + """ + def __init__(self, func): + self.im_func = func + self.__name__ = func.__name__ + self.__doc__ = func.__doc__ + self._cache = WeakKeyDictionary() + + def _bind_func(self, obj, cls): + func = self.im_func + def method(*a, **k): + return func(obj, cls, *a, **k) + update_wrapper(method, func) + ##if obj is None: + ## method.__repr__ = lambda : "<fallback_method %s.%s; bound to class>" % (cls.__name__, func.__name__) + ##else: + ## method.__repr__ = lambda : "<fallback_method %s.%s; bound to instance %r>" % (cls.__name__, func.__name__, obj) + method.__name__ = "<fallback_method_wrapper>" #NOTE: would like to implement repr() above, but can't. + + method.im_func = func + method.im_self = obj + method.im_class = cls + return method + + def __get__(self, obj, cls): + if obj is not None: + #create method and store in object, so next access calls it directly (shadowing this descriptor) + method = self._bind_func(obj, cls) + setattr(obj, self.__name__, method) + return method + + #old style classes can't do weakrefs, so can't cache our result + elif _classobj and isinstance(cls, _classobj): + return self._bind_func(None, cls) + + #can't shadow ourselves in subclass, so using internal cache for methods + else: + cache = self._cache + method = cache.get(cls) + if method is None: + method = cache[cls] = self._bind_func(None, cls) + return method + +def abstract_property(name, doc=None): + """Property constructor which indicates this is a placeholder attribute which + should be overridden by subclass. + + This is mainly useful when defining framework classes that must be + subclassed before they will be useful. + + If read or written from an instance, this method will raise an :exc:`AbstractMethodError` + (which is a subclass of :exc:`NotImplementedError`). + """ + if not name: + raise ValueError, "property name must be specified" + if not doc: + doc = "abstract property %r" % (name,) + msg = "object %(self)r attribute %(name)r is abstract, and must be implemented by a subclass" + def helper(self, *args, **kwds): + text = msg % dict(self=self, name=name) + raise AbstractMethodError(text) + return property(helper, helper, helper, doc) + +def decorate_per_instance(decorator, attr=True, bind="function"): + """creates a decorator to be applied per-instance to the instance method. + + .. note:: + What this function does may be considered "slightly deep magic", + at least by those who don't normally traffic in such things. + It performs some python trickery which (if it's not the trickery you need) + can safely be ignored. + + This is a decorator-decorator: that is, instead of wrapping an + already decorated function, it's designed to wrap the decorator itself. + + What is returned is in fact a descriptor object, + which lazily uses the provided decorator to wrap + the function on a per-instance basis, rather than + wrap it a single time for underlying function, + which is what a unmodified decorator does. + + This allows decorators such as :func:`cached_method` have + separate scopes for each instance, instead of one shared + scope for the entire function (such as :func:`cached_function`). + + :param decorator: + This should be a decorator function. It will be called + for every instance of the class whose method we are decorating. + + :param attr: + If attr is ``True`` (the default), the name of + the decorated function will be assumed to be the final name + of the attribute. If attr is ``None``, an attempt + will be made to determine the attr at runtime. + Otherwise, attr should be a string which will override + the assumed attr. + + :param bind: + This controls how the decorator will be attached to the instance. + There are currently two possible values: + + ``function`` + The default behavior: + + The decorator will be passed the underlying function, + and it's return value passed to python to create the bound method. + This mode has the following properties: + + * The local scope of the decorator will be per-instance, + not per function. This is what distinguishes this meta decorator + from simply decorating the original function directly. + + * If your decorator stores any state in ``func.__dict__``, + it will have to share that dict between all object instances. + If this is not desirable, consider using ``method`` mode, below. + + * Like a normal function, *self* will always be the first argument + when your decorated function is called. + + ``method`` + The decorator will be passed a bound instance method + instead of the original function, and it's return value + will be returned as if it were the desired method. + This has the following properties: + + * The decorator scope will still be per-instance like function mode. + + * ``func.__dict__`` will unique per instance, since the function + provided will in fact be a instancemethod object that is unique + per instance, as opposed to function mode. + + * Since this mode occurs after *self* has been bound into the method, + *self* will NOT be present as the first argument in your decorator + (though it can be accessed via the instancemethod object's im_self). + + :returns: + This returns a decorator which + (when passed a function), will return + an :class:`instance_decorating_descriptor` object + that will lazyily bind the method on a per-instance + basis. + + A usage example:: + >>> from bps.cache import cached_function + >>> from bps.meta import decorate_per_instance + >>> class MyClass(object): + >>> #this is essentially how cached_method() is implemented... + >>> @decorate_per_instance(cached_function(args=1)) + >>> def myfunc(self, value): + >>> return value*2 + + .. caution:: + + The current implementation of this function violates normal decorator + behavior, because the result of it's constructor is not a callable function, + but a descriptor object. Because of this, should wrapping the topmost (outermost) + decorator applied to a function, since most decorators will not be + able to handle a descriptor object. + """ + def instance_decorator(func): + return instance_decorating_descriptor(func, decorator, attr=attr, bind=bind) + return instance_decorator + +class instance_decorating_descriptor(object): + """property which decorates methods on a per-instance basis. + + this is the backend for :func:`decorate_per_instance`, + see that function for details. + """ + def __init__(self, func, decorator, attr=True, bind="function"): + assert func + self.func = func + if attr is True: + self.attr = func.__name__ + else: + self.attr = attr #none or a string + # we could also clone func's docstring / other things update_wrapper does + assert decorator + self.decorator = decorator + if bind == "function": + self.create = self.create_function + elif bind == "method": + self.create = self.create_method + else: + raise ValueError, "unknown bind mode: %r" % (bind,) + + create = None #filled in by init + + def create_function(self, obj, cls): + "create() implementation using func mode binding" + func = self.decorator(self.func) + return instancemethod(func, obj, cls) + + def create_method(self, obj, cls): + "create() implementation using meth mode binding" + meth = instancemethod(self.func, obj, cls) + return self.decorator(meth) + + def __get__(self, obj, cls): + #take first opportunity to figure out what attribute we're stored in + if self.attr is None: + self.attr = find_attribute(cls, self, required=True) + #deal with class-level access + if obj is None: + #XXX: could return a decorated but unbound instance + #XXX: could do some clever alternate attribute storage for class methods + return self + #create decorated method, and put it in object's dictionary + # to shadow this descriptor, so we won't get called again + wrapped_method = obj.__dict__[self.attr] = self.create(obj, cls) + return wrapped_method + +#hack to get function which creates method instances. +#instance_decorating_descriptor was chosen just because it was there, +#this type can be gonna from any method. +instancemethod = type(instance_decorating_descriptor.__get__) + +#========================================================= +#functional stuff +#========================================================= +class Params(object): + """object which represents set of args and kwds. + + Like partial() but without the function. + + The args attribute will contain the args tuple. + The kwds attribute will contains the kwds dict. + """ + + @classmethod + def normalize(cls, value): + "given tuple, dict, or Params object, return a Params object" + if hasattr(value, "args") and hasattr(value, "kwds"): + #assume it's already a Params objs + return value + elif hasattr(value, "keys"): + #assume it's a dict + return cls(**value) + #XXX: if is_str, call parse? + else: + #assume it's a sequence / iterable of positional args + return cls(*value) + + @classmethod + def parse(cls, source, kwds=True, scope=None): + """parse params string, returning Params object. + + :arg source: the source string to parse + :arg kwds: whether kwd arguments should be accepted (defaults to True) + :param scope: optional dictionary to use as global scope when evaluating string. + + :returns: + Params object containing the parsed args and kwds. + + :raises ValueError: if string can't be parsed into Params. + + Usage Example:: + + >>> from bps.meta import Params + >>> x = Params.parse("1,2,'a',t=5") + >>> x + Params(1,2,'a',t=5) + >>> x.args + (1, 2) + >>> x.kwds + { 't': 5 } + + .. warning:: + This currently uses 'eval', so it shouldn't be considered secure. + In the future, a simpler parser may be written for safety. + """ + assert is_str(source), "expected string: %r" % (source,) + if not source.strip(): + return cls() + if kwds: + grab = cls + else: + def grab(*a): + return cls(*a) + if scope: + g = scope.copy() + else: + g = {} + g['__grab'] = grab + try: #assume it's already parenthesized + result = eval("__grab " + source, g) + if isinstance(result, cls): + return result + #else probably had format of "(1,2),3", so wrong value was returned. + except SyntaxError: + pass + #try wrapping with parens + try: + result = eval("__grab (" + source + ")", g) + except SyntaxError: + raise ValueError, "bad params string: %r" % (source,) + if isinstance(result, cls): + return result + raise ValueError, "bad params string: %r" % (source,) + + def __init__(self, *args, **kwds): + "create new params object from args" + self.args = list(args) + self.kwds = kwds + + def __getitem__(self, key): + if isinstance(key, int): + #it's an argument + return self.args[key] + else: + #it's a keyword + return self.kwds[key] + + #XXX: could do a get() method, __contains__ method, etc + #XXX: could have __iter__ yield the ints then kwds for use with __getitem__ + + def append_modified(self, kwds, default=None): + "append all specified kwds, but only if value doesn't match default" + target = self.kwds + for k,v in kwds.iteritems(): + if v != default: + target[k] = v + + def append(self, *args, **kwds): + "append positional parameters, update kwd parameters" + if args: + self.args.extend(args) + if kwds: + self.kwds.update(kwds) + + def insert(self, pos, *args, **kwds): + "insert positional parameters, update kwd parameters" + if args: + self.args[pos:pos] = args + if kwds: + self.kwds.update(kwds) + +## def apply(self, func): +## return func(*self.args, **self.kwds) + + def clone(self, *args, **kwds): + "create new params object with args appended to existing args" + other = Params(*self.args, **self.kwds) + if args: + other.args.extend(args) + if kwds: + other.kwds.update(kwds) + return other + + def clear(self): + del self.args[:] + self.kwds.clear() + + def render(self, offset=0): + """render parenthesized parameters. + + ``Params.parse(p.render())`` should always return + a params object equal to the one you started with. + + ``p.render(1)`` is useful for method arguments, + when you want to exclude the first argument + from being displayed. + """ + txt = '' + for a in self.args[offset:]: + txt += "%r, " % (a,) + kwds = self.kwds + for k in sorted(kwds): + txt += "%s=%r, " % (k, kwds[k]) + if txt.endswith(", "): + txt = txt[:-2] + return txt + + def render_class(self, class_): + "render a nice repr for the class using the current parameters" + if not hasattr(class_,"__bases__"): #allow obj to be passed in for convience + class_ = class_.__class__ + return "%s.%s(%s)" % (class_.__module__,class_.__name__, self) + + def __str__(self): + return self.render() + + def __repr__(self): + return "Params(%s)" % self.render() + + def __eq__(self, other): + if hasattr(other, "args") and hasattr(other, "kwds"): + return self.args == other.args and self.kwds == other.kwds +## if is_oseq(other) and not self.kwds: +## return self.args == other +## if isinstance(other, dict) and not self.args: +## return self.kwds == other + return False + + def __ne__(self, other): + return not self.__eq__(other) + +#========================================================= +# +#========================================================= |
