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__
DeletingI'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
