Getting started#

Simply import easytree and create nested nodes on the fly using the dot notation.

>>> import easytree

>>> tree = easytree.dict()
>>> tree.foo.bar.baz = "Hello world!"
>>> tree
{
    "foo": {
        "bar": {
            "baz": "Hello world!"
        }
    }
}

Inheritence#

easytree.dict inherits from the native python dict class.

>>> tree = easytree.dict({"foo":"bar"})
>>> isinstance(tree, dict)
True

easytree.list also inherits from the native python list class.

>>> numbers = easytree.list([1,3,5])
>>> isinstance(numbers, list)
True

Setting or assigning values#

Instead of raising an AttributeError, reading a new attribute on an easytree.dict creates and returns a new child Node.

>>> tree = easytree.dict()
>>> tree.address # new undefined node
<Node 'address'>

Reading or setting an attribute on such child node dynamically casts it as an easytree.dict.

>>> tree.address.country = "United States"
>>> tree.address # now a dict
{"country": "United States"}

Alternatively, using a list method such as append dynamically casts the new node as an easytree.list

>>> tree = easytree.dict()
>>> tree.address
<Node 'address'>

>>> tree.address.country.append("United States")
>>> tree.address
{"country": ["United States"]}

Note

Technically, casting replaces the node with an appropriate class instance (e.g. dict or list) in the parent object

Of course, you can use the dot or bracket notation interchangeably, both to read and assign nested values

>>> tree = easytree.dict({"foo":"bar"})
>>> tree["foo"]
"bar"
>>> tree.foo
"bar"

Note

The bracket notation remains necessary if the key is not a valid attribute identifier name.

>>> tree = easytree.dict()
>>> tree["attribute with space"] = True
>>> tree
{"attribute with space": True}

Nested assignment#

Dictionaries assigned to an easytree.dict or added to an easytree.list are themselves cast as easytree.dict instances, allowing you to use the dot notation on nested dictionaries.

>>> friends = easytree.list()
>>> friends.append({"firstname": "Alice"})
>>> isinstance(friends[0], easytree.dict)
True
>>> friends[0].address.country = "Netherlands"
>>> friends
[
    {
        "firstname": "Alice",
        "address": {
            "country": "Netherlands"
        }
    }
]

Lists assigned to an easytree.dict are cast as easytree.list instances.

>>> tree = easytree.dict({"numbers": [1,3,5]})
>>> isinstance(tree.numbers, easytree.list)
True

Tuple values assigned to an easytree.dict are also cast.

>>> tree = easytree.dict({"country": ("France", {"capital": "Paris"})})
>>> isinstance(tree.country, tuple)
True
>>> tree.country[0]
'France'
>>> tree.country[0].capital
'Paris'

Getter#

The get method of easytree.dict is supercharged to query deeply-nested trees.

>>> profile = easytree.dict()
>>> profile.friends.append({"name":"Bob", "address":{"country":"France"}})
>>> profile.get(["friends", 0, "address", "country"])
France
>>> profile.get(["friends", 0, "address", "street"])
None

Hint

Normally, this would raise an error, as a list is not hashable. This means no collisions are possible between keys and such list queries.

Context manager#

The context manager returns the node, such that writing deeply-nested trees is easier:

>>> order = easytree.dict()
>>> with order.customer.delivery.address as a:
...     a.country = "United States"
...     a.city    = "New York"
...     a.street  = "5th avenue"
>>> order
{
    "order": {
        "customer": {
            "delivery": {
                "address": {
                    "country": "United States",
                    "city": "New York",
                    "street": "5th avenue"
                }
            }
        }
    }
}

Because the append method returns a reference to the last appended item, writing deeply-nested trees which combine easytree.dict and easytree.list nodes is also easy:

>>> profile = easytree.dict()
>>> with profile.friends.append({"firstname":"Flora"}) as friend:
...     friend.birthday = "25/02"
...     friend.address.country = "France"
>>> profile
{
    "friends": [
        {
            "firstname": "Flora",
            "birthday": "25/02",
            "address": {
                "country": "France"
            }
        }
    ]
}

The undefined node#

An undefined node object created when an undefined attribute is read from an easytree.dict node.

>>> person = easytree.dict()
>>> person.address
<Node 'address'>

Assigning or reading an attribute from an undefined node casts it as a dictionary.

>>> person = easytree.dict()
>>> person.address.country = "Nigeria"
>>> person.address
{"country": "Nigeria"}

Using the bracket notation works identically.

>>> person = easytree.dict()
>>> person["address"].country = "Nigeria"
>>> person.address
{"country": "Nigeria"}

An undefined node evaluates to False.

>>> person = easytree.dict()
>>> if not person.address:
...     print("address is missing")
address is missing

Pitfalls#

By definition, and unless an easytree is sealed or frozen, reading an undefined attribute will not raise an exception.

>>> profile = easytree.dict({"firstname":"David"})
>>> profile.firstnam #typo
<Node 'firstnam'>

Using a numeric key on an undefined node will cast the node as a dictionary, not a list.

>>> profile = easytree.dict({"firstname":"David"})
>>> profile.friends[0].name = "Flora"
>>> profile
{
    "friends": {
        0: "Flora"
    }
}

Dictionary and lists added to an easytree are cast to an easytree.dict or easytree.list instance. This means identity is not preserved.

>>> point = {"x":1, "y":1}
>>> graph = easytree.list([point])
>>> point in graph
True
>>> graph[0] is point
False