2

I created a nested dictionary based on AttrDict found there :

Object-like attribute access for nested dictionary

I modified it to contain str commands in "leaves" that gets executed when the value is requested/written to :

commands = {'root': {'com': {'read': 'READ_CMD', 'write': 'WRITE_CMD'} } }

class AttrTest()
    def __init__:
        self.__dict__['attr'] = AttrDict(commands)

test = AttrTest()
data = test.attr.root.com.read    # data = value read with the command
test.attr.root.com.write = data   # data = value written on the com port

While it works beautifully, I'd like to :

  • Avoid people getting access to attr/root/com as these returns a sub-level dictonary
  • People accessing attr.root.com directly (through __getattribute__/__setattr__)

Currently, I'm facing the following problems :

  • As said, when accessing the 'trunk' of the nested dict, I get a partial dict of the 'leaves'
  • When accessing attr.root.com it returns {'read': 'READ_CMD', 'write': 'WRITE_CMD'}
  • If detecting a read I do a forward lookup and return the value, but then attr.root.com.read fails

Is it possible to know what is the final level Python will request in the "path" ?

  • To block access to attr/root
  • To read/write the value accessing attr.root.com directly (using forward lookup)
  • To return the needed partial dict only if attr.root.com.read or attr.root.com.write are requested

Currently I've found nothing that allows me to control how deep the lookup is expected to go.

Thanks for your consideration.

Kochise
  • 504
  • 5
  • 10

1 Answers1

0

For a given attribute lookup you cannot determine how many others will follow; this is how Python works. In order to resolve x.y.z, first the object x.y needs to be retrieved before the subsequent attribute lookup (x.y).z can be performed.

What you can do however, is return a proxy object that represents the (partial) path instead of the actual underlying object which is stored in the dict. So for example if you did test.attr.com then this would return a proxy object which represents the path attr.com to-be-looked up on the test object. Only when you encounter a read or write leaf in the path, you would resolve the path and read/write the data.

The following is a sample implementation which uses an AttrDict based on __getattr__ to provide the Proxy objects (so you don't have to intercept __getattribute__):

from functools import reduce


class AttrDict(dict):
    def __getattr__(self, name):
        return Proxy(self, (name,))

    def _resolve(self, path):
        return reduce(lambda d, k: d[k], path, self)


class Proxy:
    def __init__(self, obj, path):
        object.__setattr__(self, '_obj', obj)
        object.__setattr__(self, '_path', path)

    def __str__(self):
        return f"Path<{'.'.join(self._path)}>"

    def __getattr__(self, name):
        if name == 'read':
            return self._obj._resolve(self._path)[name]
        else:
            return type(self)(self._obj, (*self._path, name))

    def __setattr__(self, name, value):
        if name != 'write' or name not in (_dict := self._obj._resolve(self._path)):
            raise AttributeError(f'Cannot set attribute {name!r} for {self}')
        _dict[name] = value


commands = {'root': {'com': {'read': 'READ_CMD', 'write': 'WRITE_CMD'} } }

test = AttrDict({'attr': commands})
print(f'{test.attr = !s}')                # Path<attr>
print(f'{test.attr.root = !s}')           # Path<attr.root>
print(f'{test.attr.root.com = !s}')       # Path<attr.root.com>
print(f'{test.attr.root.com.read = !s}')  # READ_CMD
test.attr.root.com.write = 'test'
test.attr.root.write = 'illegal'  # raises AttributeError
a_guest
  • 34,165
  • 12
  • 64
  • 118