easytree¶
A fluent tree builder, useful to create multi-level, nested JSON configurations.
Quickstart¶
Installing easytree
is simple with pip:
pip install easytree
Using easytree
is also easy
>>> import easytree
#let's create a deeply-nested chart configuration
>>> chart = easytree.Tree()
>>> chart.chart.type = "bar"
>>> chart.title.text = "France Olympic Medals"
>>> chart.xAxis.categories = ["Gold", "Silver", "Bronze"]
>>> chart.yAxis.title.text = "Count"
>>> chart.series.append(name="2016", data=[10, 18, 14])
>>> chart.series.append({"name":"2012"})
>>> chart.series[1].data = [11, 11, 13] #list items recursively become nodes
>>> easytree.serialize(chart)
{
"chart": {
"type": "bar"
},
"title": {
"text": "France Olympic Medals"
},
"xAxis": {
"categories": [
"Gold",
"Silver",
"Bronze"
]
},
"yAxis": {
"title": {
"text": "Count"
}
},
"series": [
{
"name": "2016",
"data": [
10,
18,
14
]
},
{
"name": "2012",
"data": [
11,
11,
13
]
}
]
}
Writing deeply-nested trees with list nodes is easy with a context-manager:
>>> chart = easytree.Tree()
>>> with chart.axes.append({}) as axis:
... axis.title.text = "primary axis"
>>> with chart.axes.append({}) as axis:
... axis.title.text = "secondary axis"
>>> chart.serialize()
{
"axes": [
{
"title": {
"text": "primary axis"
}
},
{
"title": {
"text": "secondary axis"
}
}
]
}
Tutorial¶
Consider the following tree, as an example:
config = easytree.Tree({
"date":"2020-02-20",
"size":{
"height":"1.56 cm",
"width":"30.41 cm"
},
"users":[
"Alice",
"Bob"
]
})
There are three types of nodes in this tree:
- value leaves (e.g. “2020-02-20”, “1.56 cm” or “Alice”)
- dict nodes (e.g. “size”)
- list nodes (e.g. “users”)
Instead of raising an AttributeError
, reading or setting a new attribute on a dict node creates and returns a new child node.
>>> config.memory.size = "512GB SSD" #memory node is created on the fly
>>> config
{
"date":"2020-02-20",
"size":{
"height":"1.56 cm",
"width":"30.41 cm"
},
"users":[
"Alice",
"Bob"
]
"memory":{
"size": "512GB SSD"
}
}
You can recursively create list and dict nodes on the fly:
>>> config.events.append(name="create", user="Alice") #events node is created on the fly
>>> config.events.append({"name:"edit", "user":"Bob"})
>>> config
{
"date":"2020-02-20",
"size":{
"height":"1.56 cm",
"width":"30.41 cm"
},
"users":[
"Alice",
"Bob"
]
"memory":{
"size": "512GB SSD"
},
"events":[
{
"name": "create",
"user": "Alice"
},
{
"name": "edit",
"user": "Bob"
}
]
}
The type of each newly-created node, unless given an explicit value, is initially undefined, and can change into a list node, a dict node (e.g. memory
) or a value-node (e.g. 512GB SSD
). The type of an undefined node is dynamically determined by subsequent interactions.
- if the
append
method is called on an undefined node, that node becomes a list node. - if an attribute is called on an undefined node (e.g.
node.name
), or a key is retrieved (e.g.node["name"]
), that node becomes a dict node. - any value appended or assigned at a node recursively becomes a node, whose type is determined by the type of the given value (list node for iterables (list, tuple, set), dict node for dictionaries, value-nodes for other types).
Example:
>>> root = easytree.Tree() #root is an undefined node
>>> root.name = "David" #root is now a dict node, and name is a string
>>> root.colors = ["blue", "brown"] #colors is a list node of strings
>>> root.cities.append(name="Paris") #cities is a list node of dict nodes
>>> root
{
"name": "David",
"colors": [
"blue",
"brown"
],
"cities": [
{
"name": "Paris"
}
]
}
Note
A dict
node has only two methods: get
and serialize
. Any other attribute called on an instance will create a new node, attach it to the instance and return it.
A list
node has only two methods: append
and serialize
. Any other attribute called on an instance raise an AttributeError
.
Once the type of a node is determined, it cannot morph into another type. For example:
>>> root = easytree.Tree({}) #explicitely set as dict node
>>> root.append(1)
AttributeError: 'dict' object has no attribute 'append'
Warning
For an undefined node, retrieving an integer key (e.g. node[0]
) is intrinsically ambiguous: did the user expect the first value of a list node (which should raise an IndexError
given that the list is then empty), or the value at the key 0
of a dict node? To avoid unintentionally creating dict nodes, an AmbiguityError
will be raised.
How does easytree
compare with jsontree
¶
easytree
differs from jsontree
(see here) in two important ways:
- elements, when attached or appended to an
easytree.Tree
, recursively become tree branches if they are themselves lists, sets, tuples or dictionaries. - serialization of an
easytree.Tree
merely converts the tree to a dictionary, list or underlying value (for leaves). It does not serialize to JSON. - starting with 0.1.8,
easytree
supports freezing or sealing trees to avoid accidentally create new nodes
Compare:
>>> import easytree
>>> tree = easytree.Tree()
>>> tree.friends = [{"name":"David"},{"name":"Celine"}]
>>> tree.friends[0].age = 29 #this works
>>> easytree.serialize(tree)
{'friends': [{'age': 29, 'name': 'David'}, {'name': 'Celine'}]}
with:
>>> import jsontree
>>> tree = jsontree.jsontree()
>>> tree.friends = [{"name":"David"},{"name":"Celine"}]
>>> tree.friends[0].age = 29 #this does not work
AttributeError: 'dict' object has no attribute 'age'
Sealing and freezing¶
Note
New with version 0.1.8
While easytree
makes it easy to create deeply-nested trees, it can also make it error prone when reading back properties.
Sealing and freezing allow to protect trees by restricting some or all mutations to a tree.
default | sealed | frozen | |
---|---|---|---|
read an existing node | ✅ | ✅ | ✅ |
create a new node | ✅ | ❌ | ❌ |
edit a node | ✅ | ✅ | ❌ |
delete a node | ✅ | ❌ | ❌ |
Sealing¶
Sealing a tree prevents the user from accidentally creating new nodes; it does allow to edit leaf values.
>>> person = easytree.Tree({"name:"Bob", "address":{"city":"New York"}}, sealed=True)
>>> person.name = "Alice" #you can still edit leaf values
>>> person.adress.city #typo spelling address
AttributeError: sealed node has no attribute 'adress'
You can seal
and unseal
a tree with the dedicated root-level functions. These functions return a new tree (i.e. these functions are not in-place).
Freezing¶
Freezing a tree prevents the user from accidentally creating new nodes or changing existing nodes.
>>> person = easytree.Tree({"name:"Bob", "address":{"city":"New York"}}, frozen=True)
>>> person.address.city = "Los Angeles"
AttributeError: cannot set attribute 'city' on frozen node
You can freeze
and unfreeze
a tree with the dedicated root-level functions. These functions return a new tree (i.e. these functions are not in-place).
API Documentation¶
-
easytree.
Tree
¶ alias of
easytree.tree.Node
-
class
easytree.
Node
(value=None, *, sealed=False, frozen=False)¶ -
append
(*args, **kwargs)¶ Appends a value to a list node. If the node type was previously undefined, the node becomes a list.
Note
The append method can take either one positional argument or one-to-many named (keyword) arguments. If passed one-to-many keyword arguments, the kwargs dictionary is added to the list.
Example
>>> tree = easytree.Tree() #undefined node >>> tree.append("hello world") #casts node to list >>> tree.append(name="David", age=29) #call with kwargs >>> tree.append({"animal":"elephant", "country":"India"}) #call with args >>> easytree.serialize(tree) ["Hello world",{"name":"David","age":29},{"animal":"elephant", "country":"India"}]
Note
The append method intentionally returns a reference to the last-added item, if that item is a new node. This allows for more fluent code using the context manager.
Example
>>> chart = easytree.Tree() >>> with chart.axes.append({}) as axis: ... axis.title.text = "primary axis" >>> with chart.axes.append({}) as axis: ... axis.title.text = "secondary axis" >>> chart.serialize() { "axes": [ { "title": { "text": "primary axis" } }, { "title": { "text": "secondary axis" } } ] }
-
get
(key, default=None)¶ Returns the value at a given key, or default if the key does not exists.
Example
>>> config = easytree.Tree({"context":{"starting":"2016-03-31"}}) >>> config.context.get("starting", "2014-01-01") 2016-03-31 >>> config.context.get("ending", "2021-12-31") 2021-12-31 >>> config.context.get("calendar") None
-
serialize
()¶ Recursively converts itself to a native python type (dict, list or None).
Example
>>> chart = easytree.Tree() >>> chart.chart.type = "bar" >>> chart.title.text = "France Olympic Medals" >>> chart.xAxis.categories = ["Gold", "Silver", "Bronze"] >>> chart.yAxis.title.text = "Count" >>> chart.series.append(name="2016", data=[10, 18, 14]) >>> chart.series.append({"name":"2012", "data":[11,11,13]}) >>> chart.serialize() { "chart": { "type": "bar" }, "title": { "text": "France Olympic Medals" }, "xAxis": { "categories": [ "Gold", "Silver", "Bronze" ] }, "yAxis": { "title": { "text": "Count" } }, "series": [ { "name": "2016", "data": [ 10, 18, 14 ] }, { "name": "2012", "data": [ 11, 11, 13 ] } ] }
-
-
easytree.
new
(root=None, *, sealed=False, frozen=False)¶ Creates a new
easytree.Tree
-
easytree.
serialize
(tree)¶ Recursively converts an
easytree.Tree
to a native python type.Example
>>> chart = easytree.Tree() >>> chart.chart.type = "bar" >>> chart.title.text = "France Olympic Medals" >>> chart.xAxis.categories = ["Gold", "Silver", "Bronze"] >>> chart.yAxis.title.text = "Count" >>> chart.series.append(name="2016", data=[10, 18, 14]) >>> chart.series.append({"name":"2012", "data":[11,11,13]}) >>> easytree.serialize(chart) { "chart": { "type": "bar" }, "title": { "text": "France Olympic Medals" }, "xAxis": { "categories": [ "Gold", "Silver", "Bronze" ] }, "yAxis": { "title": { "text": "Count" } }, "series": [ { "name": "2016", "data": [ 10, 18, 14 ] }, { "name": "2012", "data": [ 11, 11, 13 ] } ] }
-
easytree.
frozen
(tree)¶ Returns
True
if the tree is frozen
-
easytree.
freeze
(tree)¶ Returns a new frozen copy of the tree
-
easytree.
unfreeze
(tree)¶ Returns a new unfrozen copy of the tree
-
easytree.
sealed
(tree)¶ Returns
True
if the tree is sealed
-
easytree.
seal
(tree)¶ Returns a new sealed copy of the tree
-
easytree.
unseal
(tree)¶ Returns a new unsealed copy of the tree
-
easytree.
load
(stream, *args, frozen=False, sealed=False, **kwargs)¶ Deserialize a text file or binary file containing a JSON document to an
Tree
object*args
and**kwargs
are passed to thejson.load
functionExample
>>> with open("data.json", "r") as file: ... tree = easytree.load(file)
-
easytree.
loads
(s, *args, frozen=False, sealed=False, **kwargs)¶ Deserialize s (a str, bytes or bytearray instance containing a JSON document) to an
Tree
object*args
and**kwargs
are passed to thejson.loads
function
-
easytree.
dump
(obj, stream, *args, **kwargs)¶ Serialize
Tree
object as a JSON formatted string to stream (a .write()-supporting file-like object).*args
and**kwargs
are passed to thejson.dump
functionExample
>>> tree = easytree.Tree({"foo": "bar"}) >>> with open("data.json", "w") as file: ... easytree.dump(tree, file, indent=4)
-
easytree.
dumps
(obj, *args, **kwargs)¶ Serialize
Tree
to a JSON formatted string.*args
and**kwargs
are passed to thejson.dumps
function
Changelog¶
Version 0.1.0 (2020-08-01)¶
- created
easytree
Version 0.1.1 (2020-08-02)¶
- added ability to
append
dictionary using keyword arguments- added ability to iterate over a tree
- added ability to compute the length of a tree (for list nodes and dict nodes)
Version 0.1.2 (2020-08-03)¶
- overrode the
__new__
method to filter out primitive and object types- added ability to check for contains
Version 0.1.3 (2020-08-04)¶
- added the
serialize
method to the tree
Version 0.1.4 (2020-08-05)¶
- added context manager to return a reference to itself
- addressed infinite recursion in
__getattr__
append
now returns a reference to last added node, if it is a node
Version 0.1.5 (2020-08-08)¶
- refactored the
Tree
object into aTree
andNode
object- removed the
__new__
from theTree
root object to allow for inheritence
Version 0.1.6 (2020-08-16)¶
- fixed a bug where overriding an attribute would fail if it was already in the node
Version 0.1.7 (2021-05-01)¶
- passing an
easytree.Tree
object as an instanciation argument will copy the tree- added support for list slicing in list nodes
- improved error messages
- removed unncessary code in append method
- factored out constants
repr
now delegates to underlying value- addressed ipython canary
- added
get
method for dict nodes- implemented
__delitem__
,__delattr__
and__bool__