Working with Objects#

The previous examples used plain strings as data objects. However, any Python object can be stored, as long as it is hashable.

Assume we have the following Objects:

class Person:
    def __init__(self, name, *, age, guid=None):
        self.name = name
        self.age = age
        self.guid = guid

    def __repr__(self):
        return f"Person<{self.name}, {self.age}>"


class Department:
    def __init__(self, name, *, guid=None):
        self.name = name
        self.guid = guid

    def __repr__(self):
        return f"Department<{self.name}>"

We can add instances of these classes to our tree:

dev = tree.add(Department("Development"))
alice = Person("Alice", age=23, guid="{123-456}")
dev.add(alice)
...

For bookkeeping, lookups, and serialization, every data object needs a data_id. This value defaults to hash(data), which is good enough in many cases.

assert tree[alice].data_id == hash(alice)

However the hash value cannot always be calculated and also is not stable enough to be useful for persistence. In our example, we already have object GUIDs, which we want to use instead. This can be achieved by passing a callback to the tree:

def _calc_id(tree, data):
    if isinstance(data, fixture.Person):
        return data.guid
    return hash(data)

tree = Tree(calc_data_id=_calc_id)

As a result, persons now use the GUID as data_id:

Tree<'2009255653136'>
├── Node<'Department<Development>', data_id=125578508105>
│   ├── Node<'Person<Alice, 23>', data_id={123-456}>
│   ├── Node<'Person<Bob, 32>', data_id={234-456}>
│   ╰── Node<'Person<Charleen, 43>', data_id={345-456}>
╰── Node<'Department<Marketing>', data_id=125578508063>
    ├── Node<'Person<Charleen, 43>', data_id={345-456}>
    ╰── Node<'Person<Dave, 54>', data_id={456-456}>

Lookup works by data object or data_id as expected:

assert tree[alice].data_id == "{123-456}"
assert tree[alice].data.guid == "{123-456}"
assert tree["{123-456}"].data.name == "Alice"

assert tree.find(data_id="{123-456}").data is alice

Shadow Attributes (Attribute Aliasing)#

When storing arbitrary objects within a tree node, all its attributes must be accessed through the node.data attribute.
This can be simplified by using the shadow_attrs argument, which allow to access node.data.age as node.age for example:

tree = Tree("Persons", shadow_attrs=True)
dev = tree.add(Department("Development"))
alice = Person("Alice", age=23, guid="{123-456}")
alice_node = dev.add(alice)

# Standard access using `node.data`:
assert alice_node.data is alice
assert alice_node.data.guid == "{123-456}"
assert alice_node.data.age == 23

# Direct access using shadowing:
assert alice_node.guid == "{123-456}"
assert alice_node.age == 23

# Note caveat: `node.name` is not shadowed, but a native property:
assert alice.data.name == "Alice"
assert alice.name == "Person<Alice, 23>"

# Note also: shadow attributes are readonly:
alice.age = 24       # ERROR: raises AttributeError
alice.data.age = 24  # OK!

Note

Aliasing only works for attribute names that are not part of the native Node data model. So these attributes will always return the native values: children, data_id, data, kind, meta, node_id, parent, tree, and all other methods and properties.

Note also that shadow attributes are readonly.