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

Reply via email to