Steven Bethard wrote: > Hmmm... This does seem sensible. And sidesteps the issue about > suggesting that subclasses use Namespace.update instead of > namespaceinstance.update -- the latter just won't work! (This is a Good > Thing, IMHO.)
Yeah, I thought so too. It also crystallised for me that the real problem was that we were trying to use the attribute namespace for something different from what it is usually used for, and so Python's normal lookup sequence was a hindrance rather than a help.
I guess Guido knew what he was doing when he provided the __getattribute__ hook ;)
One other point is that dealing correctly with meta-issues like this greatly boosts the benefits from having this module in the standard library. I would expect most of the 'roll-your-own' solutions that are out there deal badly with this issue. "Inherit from namespaces.Namespace" is a simpler instruction than "figure out how to write an appropriate __getattribute__ method".
Anyway, it is probably worth digging into the descriptor machinery a bit further in order to design a lookup scheme that is most appropriate for namespaces, but the above example has convinced me that object.__getattribute__ is NOT it :)
Yeah, I'm going to look into this a bit more too, but let me know if you have any more insights in this area.
My gut feel is that we want to get rid of all the decriptor behaviour for normal names, but keep it for special names. After all, if people actually wanted that machinery for normal attributes, they'd be using a normal class rather than a namespace.
I'm also interested in this because I'd like to let the 'Record' class (see below) be able to store anything in the defaults, including descriptors and have things 'just work'.
The __getattribute__ I posted comes close to handling that, but we need __setattr__ and __delattr__ as well in order to deal fully with the issue, since override descriptors like property() can affect those operations. Fortunately, they should be relatively trivial:
# Module helper function def _isspecial(name): return name.startswith("__") and name.endswith("__")
# And in Namespace def __getattribute__(self, name): """Namespaces only use __dict__ and __getattr__ for non-magic attribute names. Class attributes and descriptors like property() are ignored """ getattribute = super(Namespace, self).__getattribute__ try: return getattribute("__dict__")[name] except KeyError: if _isspecial(name) # Employ the default lookup system for magic names return getattribute(name) else: # Skip the default lookup system for normal names if hasattr(self, "__getattr__"): return getattribute("__getattr__")(name) else: raise AttributeError('%s instance has no attribute %s' % (type(self).__name__, name))
def __setattr__(self, name, val): """Namespaces only use __dict__ for non-magic attribute names. Descriptors like property() are ignored""" if _isspecial(name): super(Namespace, self).__setattr__(name, val) else: self.__dict__[name] = val
def __delattr__(self, name): """Namespaces only use __dict__ for non-magic attribute names. Descriptors like property() are ignored""" if _isspecial(name): super(Namespace, self).__delattr__(name) else: del self.__dict__[name]
In action:
Py> def get(self): print "Getting" ... Py> def set(self, val): print "Setting" ... Py> def delete(self): print "Deleting" ... Py> prop = property(get, set, delete) Py> class C(object): ... x = prop ... Py> c = C() Py> c.x Getting Py> c.x = 1 Setting Py> del c.x Deleting Py> class NS(namespaces.Namespace): ... x = prop ... Py> ns = NS() Py> ns.x Traceback (most recent call last): File "<stdin>", line 1, in ? File "namespaces.py", line 40, in __getattribute__ raise AttributeError('%s instance has no attribute %s' AttributeError: NS instance has no attribute x Py> ns.x = 1 Py> del ns.x Py> ns.__x__ Getting Py> ns.__x__ = 1 Setting Py> del ns.__x__ Deleting
I've attached my latest local version of namespaces.py (based on the recent PEP draft) which includes all of the above. Some other highlights are:
NamespaceView supports Namespace instances in the constructor
NamespaceChain inherits from NamespaceView (as per my last message about that)
LockedView is a new class that supports 'locking' a namespace, so you can only modify existing names, and cannot add or remove them
NamespaceChain and LockedView are the main reason I modified NamespaceView to directly support namespaces - so that subclassed could easily support using either.
Record inherits from LockedView and is designed to make it easy to define and create fully specified "data container" objects.
Cheers, Nick.
-- Nick Coghlan | [EMAIL PROTECTED] | Brisbane, Australia --------------------------------------------------------------- http://boredomandlaziness.skystorm.net
def _isspecial(name): return name.startswith("__") and name.endswith("__")
class Namespace(object): """Namespace([namespace|dict|seq], **kwargs) -> Namespace object The new Namespace object's attributes are initialized from (if provided) either another Namespace object's attributes, a dictionary, or a sequence of (name, value) pairs, then from the name=value pairs in the keyword argument list. """ def __init__(*args, **kwargs): """Initializes the attributes of a Namespace instance. Calling __init__ from a subclass is optional. Any _required_ initialisation will be done in __new__. Subclasses should preserve this characteristic. """ # inheritance-friendly update call type(args[0]).update(*args, **kwargs) def __getattribute__(self, name): """Namespaces only use __dict__ and __getattr__ for non-magic attribute names. Class attributes and descriptors like property() are ignored """ getattribute = super(Namespace, self).__getattribute__ try: return getattribute("__dict__")[name] except KeyError: if _isspecial(name): # Employ the default lookup system for magic names return getattribute(name) else: # Skip the default lookup system for normal names if hasattr(self, "__getattr__"): return getattribute("__getattr__")(name) else: raise AttributeError('%s instance has no attribute %s' % (type(self).__name__, name)) def __setattr__(self, name, val): """Namespaces only use __dict__ for non-magic attribute names. Descriptors like property() are ignored""" if _isspecial(name): super(Namespace, self).__setattr__(name, val) else: self.__dict__[name] = val def __delattr__(self, name): """Namespaces only use __dict__ for non-magic attribute names. Descriptors like property() are ignored""" if _isspecial(name): super(Namespace, self).__delattr__(name) else: del self.__dict__[name] def __eq__(self, other): """x.__eq__(y) <==> x == y Two Namespace objects are considered equal if they have the same attributes and the same values for each of those attributes. """ return (other.__class__ == self.__class__ and self.__dict__ == other.__dict__) def __repr__(self): """x.__repr__() <==> repr(x) If all attribute values in this namespace (and any nested namespaces) are reproducable with eval(repr(x)), then the Namespace object is also reproducable for eval(repr(x)). """ return '%s(%s)' % ( type(self).__name__, ', '.join('%s=%r' % (k, v) for k, v in sorted(self.__dict__.iteritems()))) def update(*args, **kwargs): """Namespace.update(ns, [ns|dict|seq,] **kwargs) -> None Updates the first Namespace object's attributes from (if provided) either another Namespace object's attributes, a dictionary, or a sequence of (name, value) pairs, then from the name=value pairs in the keyword argument list. """ if not 1 <= len(args) <= 2: raise TypeError('expected 1 or 2 arguments, got %i' % len(args)) self = args[0] if not isinstance(self, Namespace): raise TypeError('first argument to update should be ' 'Namespace, not %s' % type(self).__name__) if len(args) == 2: other = args[1] if isinstance(other, Namespace): other = other.__dict__ try: self.__dict__.update(other) except (TypeError, ValueError): raise TypeError('cannot update Namespace with %s' % type(other).__name__) self.__dict__.update(kwargs) class NamespaceView(Namespace): """NamespaceView([ns|dict]) -> new Namespace view of the dict Creates a Namespace that is a view of the original dictionary, that is, changes to the Namespace object will be reflected in the dictionary, and vice versa. For namespaces, the view is of the other namespace's attributes. """ def __init__(self, orig): if isinstance(orig, Namespace): self.__dict__ = orig.__dict__ else: self.__dict__ = orig class LockedView(NamespaceView): """LockedView(dict) -> new Namespace view of the dict Creates a NamespaceView that prevents addition of deletion of names in the namespace. Existing names can be rebound. """ def __setattr__(self, name, val): getattr(self, name) self.__dict__[name] = val def __delattr__(self, name): raise TypeError("%s does not permit deletion of attributes" % type(self).__name__) class NamespaceChain(NamespaceView): """NamespaceChain(dict, *chain) -> new attribute lookup chain The new NamespaceChain is firstly a standard NamespaceView for the supplied dictionary. However, when an attribute is looked up and is not found in the main dictionary, the sequence of chained objects is searched sequentially for an object with such an attribute. The first such attribute found is returned, or an AttributeError is raised if none is found. The list of chained objects is stored in the __namespaces__ attribute. """ def __init__(self, head, *args): NamespaceView.__init__(self, head) self.__namespaces__ = args def __getattr__(self, name): """Return the first such attribute found in the object list This is only invoked for attributes not found in the head namespace. """ for obj in self.__namespaces__: try: return getattr(obj, name) except AttributeError: pass raise AttributeError('%s instance has no attribute %s' % (type(self).__name__, name)) class Record(LockedView): _subfield_prefix = "_sub_" def __init__(self): """Record() -> Record instance Instantiates a Namespace populated based on a Record subclass definition. Normal attributes are placed in the instance dictionary on initialisation. Attributes whose names start with '_sub_' are called, and the result placed in the instance dictionary using a name without the subfield prefix. The subfield prefix used can be changed by setting the _subfield_prefix attribute in the subclass Setting the subfield prefix to None means no subfields will be automatically instantiated, and setting it to "" means that every field will be automatically instantiated. Any fields starting with an underscore are ignored (this includes subfields which start with an underscore after the subfield prefix has been stripped). For example: Py> from namespaces import Record Py> class Example(Record): ... a = 1 ... b = "" ... class _sub_sf(Record): ... c = 3 ... def _sub_calc(): return "Calculated value!" ... Py> x = Example() Py> x Example(a=1, b='', calc='Calculated value!', sf=_sub_sf(c=3)) Py> class Example2(namespaces.Record): ... _subfield_prefix = "" ... a = str ... b = int ... class x(namespaces.Record): pass ... def f(): return "Hi!" ... Py> x = Example2() Py> x Example2(a='', b=0, f='Hi!', x=x()) Py> x.c = 1 Traceback (most recent call last): ... AttributeError: Example2 instance has no attribute c """ definition = type(self) prefix = definition._subfield_prefix prefix_len = len(prefix) for field, value in definition.__dict__.iteritems(): if field.startswith(prefix): subfield = field[prefix_len:] if not subfield.startswith("_"): # Set the calculated value self.__dict__[subfield] = value() elif not field.startswith("_"): # Set the value self.__dict__[field] = value
-- http://mail.python.org/mailman/listinfo/python-list