"""Detailed implementation - see python.html Where, in python.html and related texts, I used code fragments to suggest ways of doing parts of the job described, I tried to ensure that they were intelligible in terms of what I'd (thus far) said in the relevant text. The following code presumes all the ideas on which I eventually settled in the discussion: here I implement the relevant stuff as a cohesive whole. This started out simply as a transcript of those fragments, suitably combined, and has evolved (extensively). A `lookup' is a function taking one (string) argument. The argument is understood as an attribute name; what the lookup returns will be understood as the value of that attribute; if it doesn't provide a value for the given attribute, it signals this by raising AttributeError. While the primitive pnamespace(), see bonnet.py, takes a lookup and packages it as an `object', most lookups will appear in lists (lookup chains). See bonnet.py's mason() and within(). One special key is to be supported by all lookups: the __dir__ attribute should be a list of all attribute names that don't start or end in an underscore. In bonnet.py, along with various tools belonging `under the bonnet' of an interpreter, you'll find documentation of what a `builder' is, for use in place of `class' in the build statement, generalising python's class statement. Contents: instance -- basic builder implementing the magic of instances. architect -- archetype (i.e. typedef documentation) for builder classes. method -- an object specifying the magic for class-style method inheritance. builder -- a builder class which builds builder classes. class -- a builder which builds (very nearly) a python class. See check.py for a sketch of how to type-check namespaces; novelty.py for various bits and pieces; lazy.py for how to extend classes with further lookup schemes. The lookup list for a bonnet.within has one entry per base; the other builders extend this in various ways, principally via the magic mechanisms of instance, which see. Greater sophistication emerges once we get to builder and class. $Id: python.py,v 1.8 2005/02/12 17:46:26 eddy Exp $ """ # " from bonnet import within, public, aslookup # Scheme names it would be unwise to use: pythonic_protocols = ( 'lookups', 'self', 'data', 'wrap', 'magic', 'dir', # mine # attributes: 'bases', 'class', 'dict', # standard python methods: 'init', 'len', 'del', 'nonzero', 'cmp', 'rcmp', 'getattr', 'setattr', 'delattr', 'getitem', 'setitem', 'delitem', 'getslice', 'setslice', 'delslice', 'neg', 'add', 'radd', 'sub', 'rsub', 'mul', 'rmul', 'div', 'rdiv', 'mod', 'rmod', 'divmod', 'rdivmod' ) def instance(*bases, class=None, **what, exclude=pythonic_protocols): """Builds an instance. Is a builder: see documentation of bonnet.mason for documentation of its parameters. Recognises one keyword argument, class, with default None. If class is not None, it becomes the instance's __class__ attribute. The suite wrapper has, as its .__self__ attribute, the built object. Neither object can see a __lookups__ attribute: this is deliberate ! Any .__wrap__ attribute of class is used to specify methods for the suite wrapper; the wrapper accesses these before falling back on the built object. In the absence of such an attribute, __dict__-based attribute modification functionality is provided to the wrapper. Note that this is the case when class is None, in particular when a build statement uses instance as its builder. If you do not want such functionality, equip your class with a .__wrap__ attribute which does *not* provide it. If either __wrap__ or the fall-back supplies attribute modifiers, using these to store __setattr__ or __delattr__ will only affect the built instance, not the suite wrapper: it will carry on using the same attribute modifiers it was created with. Any .__magic__ attribute of class is used to specify magic lookup schemes for the built object. Each public attribute of class.__magic__ should be a callable taking three arguments; the name of the attribute, with __ added before and after, is used as an attribute name of class; if class has such an attribute, the callable is called, during instantiation, with this as first argument, the instance's suite-wrapper as second and the instance's lookup list as third. The callable will typically use the magic attribute and suite wrapper to build a lookup which it will add to the start or end of the lookup chain. Note that the various magic schemes are implemented in haphazard order. Finally, the built object is given a dictionary-based lookup, consulting the dictionary that the suite wrapper sees as .__dict__, and a lookup implementing the __getattr__ protocol; the former is its first lookup, the latter is its last. Magic functionality has the option of keeping the suite wrapper around after the suite has executed, e.g. as the self parameter of methods. In such a case, if you want to keep the outside world's hands off the wrapper, your magic needs to be careful not to go handing it out to strangers. If a method needs to return the built object, for instance, it should return self.__self__ rather than self (which is the wrapper). See method, below, for implementation of some schemes; the various builders in lazy.py for similar; and builder, below, for how values for class get to provide sensible magic. """ try: what['__builder__'] except KeyError: what['__builder__'] = instance if class is not None: what['__class__'] = class inst, shell = apply(within, bases, what) # now build the suite wrapper: wrap, husk = within(inst, __self__=inst, __dict__={}, __del__=lambda : None) # equip suite wrapper with primitive behaviours: try: sweet = class.__wrap__ except AttributeError: # provide fall-backs: def setit(key, val, *, **, bok=wrap.__dict__): bok[key] = val def delit(key, *, **, bok=wrap.__dict__): del bok[key] def grab(key, *, **, bok={ '__setattr__': setit, '__delattr__': delit }): try: return bok[key] except KeyError: raise AttributeError, key else: # use whatever sweet gives us (and no more): def grab(key, *, **, glean=aslookup(sweet), obj=wrap): def ans(*args, **what, meth=glean(key), self=obj): return apply(meth, (self,) + args, what) return ans husk.__lookups__.insert(0, grab) del husk # implement all schemes for the instance itself: try: schemes = class.__magic__ except AttributeError: pass else: for nom in schemes.__dir__: if nom not in exclude: try: obj = getattr(class, '__' + nom + '__') except AttributeError: pass else: getattr(schemes, nom)(obj, wrap, shell.__lookups__): # finally, __getattr__ and __dict__ for attribute lookup; but always first # and last, respectively, not in hap-hazard order among schemes. def getit(key, *, **, book=wrap.__dict__): if key == '__dir__': return filter(public, book.keys()) try: return book[key] except KeyError: raise AttributeError, key def getat(key, *, **, obj=inst): if key != '__getattr__': try: return obj.__getattr__(key) except AttributeError: pass raise AttributeError, key shell.__lookups__.insert(0, getit) shell.__lookups__.append(getat) # outer = within(wrap, __lookups__=shell.__lookups__, __del__=lambda : None)[0] del shell # fatuous: but stated for emphasis ... # Could use outer in place of wrap to support __lookups__ in the suite: return inst, wrap # typedef ... instance architect: # Note that instance supports attribute modification in its suite, which the # interpreter could use to store the __doc__ - but it should provide __doc__ # as a keyword argument to builders (e.g. so that a within can begin with a # doc comment explaining what its suite is doing, without messing up the doc # of a base). """The generic interface for builder classes. A builder class is archetypically instantiated by using it in a build statement: it builds an instance, equipping it with magic. It is, equally callable, with the usual arguments and returns of a builder. See bonnet.mason(), the archetype of builders, for further details. A builder class provides its built instances with attribute lookup functionality, as documented by instance, above. This will include borrowing attributes from the __data__ attribute of the builder class, if it has one, else from the builder class itself; more importantly, it will involve some lookups provided by the __magic__ attribute of the builder class. In pursuit of that, the __magic__ attribute of a builder class must borrow from the __magic__ attributes of *all* bases of the builder class that have one. Likewise for each magic attribute implied by an attribute of __magic__, e.g. if __magic__ supports method, the __method__ of the derived builder class must have those of the base builder classes as its bases. In both case, though: if no base has the magic attribute, the derived class doesn't need one; if only one base has it, this can simply be borrowed without creating a stub via which to access it (and we can uniquify the list of bases before testing this ;^) The same base-forwarding is applied to the __wrap__ attribute, which equips the suite-wrapper with interesting behaviour: if absent, instance gives its suite wrapper fall-back attribute modification behaviour; but if __wrap__ is present, attribute modification will only be feasible in so far as __wrap__ provides for it (i.e. you'll usually provide __setattr__ and __delattr__ on any __wrap__, unless you *intend* to forbid relevant functionality even in the suite). Similar base-forwarding is applied to the __data__ attribute, save that a base which lacks this attribute is used in its stead, and a __data__ attribute is always provided on the builder class, even if it has no bases (and, consequently, no attributes In the suite of a builder class, use instance to build a replacement for any magic object: instance __method__ (__method__,): def stuff(self, src, wrap): pass # or whatever This will replace the __method__ attribute of the builder class with one borrowing from the one provided when the builder class was built and extending it with the attributes added during the suite (here: stuff). A magic lookup scheme is at liberty to provide the built instance with access to __lookups__ (and __dict__), or to equip it with sophisticated attribute modification functionality. For illustrations, see lazy.py and check.py """ instance __magic__: """__magic__ provides hooks for specific lookup schemes A (class or) builder class has a __magic__ attribute: which borrows from the matching attributes of any bases (except when exactly one base has this attribute, in which case the __magic__ attribute is simply borrowed whole). Each public attribute of __magic__ must be callable: it describes a `scheme' by which the builder class equips its instances with useful behaviour. The scheme comprises two parts: the callable, which sets up each instance to have the relevant behaviour, and a magic attribute of the builder class. The latter's name is obtained by wrapping that of the scheme in __...__, e.g. the scheme named method is associated with the magic __method__ attribute of (classes and) builder classes. The public attributes of __magic__ must be callable; when an instance of the builder class is initialised, this callable is invoked with arguments * the associated magic attribute of the builder class, * the wrapper of the instance being initialised and * the lookup-list of the instance being built. The callables for various schemes are invoked in haphazard order. Before any of them are called, the instance's __lookups__ list only contains the lookups of bases from which the instance borrows. After all schemes have been called, the instance's dictionary-based lookup is installed as first lookup and its __getattr__ lookup is installed as last lookup. (Ensuring these two are first and last prevents us from being able to implement them via __magic__ themselves.) Each scheme's callable will typically install a lookup in the instance's __lookups__ list: a lookup scheme should .insert(0, ...) to be consulted before bases or .append(...) to be consulted after them. """ pass def __call__(*bases, **what): # a builder class always has a __call__ """The call attribute of a builder class is a builder. It is invoked with the usual arguments for a builder, bases and keywords, and its return is the usual (built-object, suite-wrapper) pair. This is the signature of the builder class' __call__ attribute: note, however, that the attribute may actually be provided for by inheritance from whatever built the builder class, so the underlying function might have a different signature: see, for instance, the __call__ provided by builder, which both it and class inherit. """ # Superficial stub: return apply(instance, bases, what) # That should make it easier to keep track of how builder is constructed. # OK, so let's specify a few schemes: # first, a pseudo-scheme: instance wrap: instance __magic__: def wrap(funcs, inst, lookups): pass # stub to force propagation of __wrap__ instance __wrap__: # rely on instance to supply a suitable __del__ ... def __delattr__(self, key): del self.__dict__[key] def __setattr__(self, key, value): self.__dict__[key] = value # then the non-method attribute carrier: instance data: instance __magic__: def data(given, inst, lookups): # Should an instance borrow from its class before or after its bases ? lookups.insert(0, aslookup(given)) __data__ = None # and the method carrier: instance method: instance __magic__: def method(funcs, inst, lookups): def fetch(key, *, **, meth=aslookup(funcs), obj=inst): if key == '__dir__': return meth(key) def ans(*args, **what, func=meth(key), self=obj): return apply(func, (self,) + args, what) return ans # Should an instance inherit from its class before or after borrowing from its bases ? lookups.insert(0, fetch) instance __method__: # and we may as well have the default forms of a few read-only methods ... def __init__(self): """Default instance initialiser.""" pass def __del__(self=None, *, **, naked=__self__.__del__): """Default instance destructor. Note that any __del__ defined on a __method__ or __wrap__ object (or, indeed, any similar curriable-carrier) must give its parameter a default: the function *will* be called when its carrier is tidied away (just as methods curried from it are called when relevant instances are tidied away) with no arguments; it should be sure to do the right thing in that instance ! Thus the typical definition of a __del__ method will resemble: def __del__(self=None, *, **, up=__self__.__del__): if self is None: up() else: # do whatever is apt for my instance, self; ending in up(self) # unless up is a raw __del__ wherein a `raw' __del__ is the one stored on the carrier (which is the __self__ of the wrapper in whose namespace the __del__ gets defined) to tidy *it* away. If you don't know whether up is raw, protect the second call to up in a try clause catching TypeError. """ if self is None: naked() else: pass # do stuff appropriate to self. def __repr__(self): ans = self.__text(repr) # no point computing that again later ... self.__repr__ = lambda a=ans: a return ans def __str__(self): ans = self.__text(str) # no point computing that again later ... self.__str__ = lambda a=ans: a return ans def __text(self, text): form = 'object' try: k = self.__class__ except AttributeError: pass else: if self is not k: try: nom = k.__name__ except AttributeError: nom = text(k) else: try: nom = '%s.%s' % (k.__module__, nom) except AttributeError: pass form = nom + ' instance' return '<' + form + ' at ' + hex(id(self)) + '>' instance builder (method, data, wrap): """Primitive builder for sophisticated objects. """ # supply call method: instance __method__ (__method__,): def __attrseq(name, seq): # tool function # uniquified map(lambda x: getattr(x, name), seq) with None thrown out, if present. row = [ None ] # so we know where it is and uniquifying ensures it's not duplicated for b in seq: try: here = getattr(b, name) except AttributeError: pass else: # if here not in row: row.append(here) # in uses == rather than is as test for it in row: if it is here: break else: row.append(here) return row[1:] # omit None def __call__(self, *bases, **what, forbid=pythonic_protocols, glean=__attrseq, spawn=instance): # include self's schemes as well as those of bases: sources = bases + (self,) row = glean('__magic__', sources) if len(row) > 1: names = what['__magic__'] = apply(within, tuple(row))[0] names = names.__dir__ elif row: names = row[0].__dir__ else: names = () for nom in names: # arguably: if nom in forbid: raise NameError, ... assert nom not in forbid, 'scheme name and deep magic clash: ' + nom name = '__' + nom + '__' row = glean(name, sources) if len(row) > 1: what[name] = apply(within, tuple(row))[0] try: what['__builder__'] except KeyError: what['__builder__'] = self what.update({ '__bases__': bases, 'class': self }) return apply(spawn, bases, what) del __attrseq # Arrange to be usable as a builder class: # we have to list these bases; but what we build won't have to. instance __magic__ (method.__magic__, data.__magic__, wrap.__magic__): pass # Bury a mimic of method's currier in the __getattr__ hook: def __getattr__(key, *, **, meths=aslookup(__method__)): def thod(*args, **what, func=meths(key)): return apply(func, (builder,) + args, what) return thod # builder is now callable and behaves a lot like one of its own instances;^) # Consider: # builder builder (builder,): pass # change what builder refers to, but not its behaviour. # or, likewise, # builder builder (method, data, wrap): ... a suite containing just the __method__ above ... del pythonic_protocols builder class (method, data, wrap): """A builder class which builds (conventional) classes. Arguments and returns are as usual for builders. The built objects are classes. As discussed in python2.html, this differs from a python 1 class is that referring to methods of the class explicitly (as opposed to via inheritance) requires dereference via .__method__: classname.__method__.methodname """ instance __wrap__ (__wrap__,): def __get(self, magic): """Provides for copy-on-write, as needed for attribute modification.""" try: return self.__dict__[magic] except KeyError: pass try: ans = instance(getattr(self, magic)) except AttributeError: ans = instance() setattr(self, magic, ans) return ans def __delattr__(self, key, *, **): missing = 1 for it in ( '__data__', '__method__' ): try: delattr(self.__get(it), key) except AttributeError: pass else: missing = None # but I won't let you del the magic attributes of schemes ! if missing: raise AttributeError, key def __setattr__(self, key, val, *, **): if (key[:2] == '__' == key[-2:] and hasattr(self.__magic__, key[2:-2])): self.__dict__[key] = val elif callable(val): setattr(self.__get('__method__'), key, val) else: setattr(self.__get('__data__'), key, val) instance __method__ (__method__,): # Classes use this __call__ where builder classes, such as class, use # the one they inherit from builder. def __call__(self, *args, **what, spawn=instance): """Return a new instance of class self. The instance is built, thereby obtaining all the lookups of the class' various schemes; using those lookups, an __init__ method is sought on the instance; this is invoked with the arguments passed to the present __call__. """ try: what['__builder__'] except KeyError: what['__builder__'] = class inst, wrap = spawn(class=self) # Perform any __init__ method wrap supports: apply(wrap.__init__, args, what) # Return the wrapper: python instances are the same things as appear # as the self of their methods. return wrap _rcs_log = """ $Log: python.py,v $ Revision 1.8 2005/02/12 17:46:26 eddy Add support for __builder__ attribute (in case not supplied by parser ...) Revision 1.7 2005/02/12 16:45:33 eddy Lots of doc fixes, some tidy-up, more systematic handling of imprudent schemes, borrow class data early. Revision 1.6 2002/01/30 17:52:21 eddy Major revolution. Inlined post-within precursors in instance and simplified the resulting mess; added __wrap__ to carry wrapper methods from class; shunted class-specific magic (redirected attribute modification) into class, unburdening (gennie renamed as) builder. Moved all surviving precursors of instance from python.py to bonnet.py, emptying old junk from bonnet.py into novelty.py. Made the function defining a scheme receive the lookups list as third parameter; removed access to this list from instance wrappers; made wrapper's __dict__-based attribute modifier fallbacks come early, like __wrap__-derived kit, rather than late. Changed __meth__ to __method__, `generate' to `build', __schemes__ to __magic__. Re-documented plenty of stuff, revised web pages. Revision 1.5 2002/01/26 15:11:40 eddy tidy-up and simplification; moved instance early; next stop wyrm.py ... Revision 1.4 2002/01/23 04:02:34 eddy made __dir__ attribute universal; renamed initspace to extend; cleaned up scheme inheritance Revision 1.3 2002/01/22 19:16:24 eddy resync.d justthecode with python; tweaked some docs; untabified Revision 1.2 2002/01/22 18:58:13 eddy changed generators round to use __schemes__, with meth as just one such; added lazy.py Revision 1.1 2001/10/17 18:00:20 eddy added in all files previously missing from RCS Revision 1.15 2000/01/16 23:17:19 eddy Restored the missing what.update() in gennie (doh). Provided gennie and initspace with more configurability (for check.py). Revision 1.14 2000/01/15 18:08:47 eddy Re-built everyone using wrapped(). Revision 1.13 2000/01/14 19:26:36 eddy Major progress ... now for another revolution. Revision 1.12 2000/01/08 13:43:24 eddy Built instspace primitively, obsoleting fixspace (and namespace) and making it practical to generate class here, obsoleting class.py Revision 1.11 2000/01/07 23:19:00 eddy A revolution is a-coming ... Tidied up fixspace (plus some tweaks) just in time to obsolete it ... Revision 1.10 2000/01/03 23:14:11 eddy class.py no longer needs namespace: fixspace, within and aslookup suffice ;^) Revision 1.9 2000/01/02 18:37:10 eddy Major over-haul of docs. Added generator() for generic doc. The closure revolution. Removal of notes and of type-check sketch. Revision 1.8 1999/12/18 17:39:18 eddy Tweaks. Revision 1.7 1999/12/18 15:48:07 eddy Major revolution. Made __lookups__ private to generator-suite. Re-arranged everything else to suit. Added draft type-checker. Revision 1.6 1999/12/13 20:22:53 eddy Removed redundant caveat, added missing bits to doc, tweaked stuff. Revision 1.5 1999/12/13 19:47:17 eddy Like I said in April, revolution has happened. Removed class and ancillaries to class.py, rearranged the remains. Revision 1.4 1999/04/07 21:32:03 eddy The newdict revolution blew over, re-emerged, got co-opted and set the ground for a roll up to above a dotted line, which has now emerged, now calling for a further revolution. Revision 1.3 1999/04/06 22:07:54 eddy Time to refactor newdict Revision 1.2 1999/03/28 21:15:21 eddy Some steady work. Initial Revision 1.1 1999/03/27 01:29:14 eddy """