Constructing Graphtage Trees
Graphtage operates on trees represented by the graphtage.TreeNode
base class.
There are various predefined specializations of tree nodes, such as graphtage.IntegerNode
for integers, graphtage.ListNode
for lists, and graphtage.DictNode
for dictionaries. graphtage.TreeNode
has an optional parent
and a potentially empty set of children
.
Graphtage provides a graphtage.builder.Builder
class for conveniently converting arbitrary objects into a tree of TreeNode
objects. It uses Python magic to define the conversions.
from graphtage import IntegerNode, TreeNode
from graphtage.builder import Builder
class CustomBuilder(Builder):
@Builder.builder(int)
def build_int(self, node: int, children: list[TreeNode]):
return IntegerNode(node)
>>> CustomBuilder().build_tree(10)
IntegerNode(10)
The @Builder.builder(int)
decorator specifies that the function is able to build a Graphtage TreeNode object from inputs that are instanceof()
the type int. If there are multiple builder functions that match a given object, the function associated with the most specialized type is chosen. For example:
class Foo:
pass
class Bar(Foo):
pass
class CustomBuilder(Builder):
@Builder.builder(Foo)
def build_foo(self, node: Foo, children: list[TreeNode]):
return StringNode("foo")
@Build.builder(Bar)
def build_bar(self, node: Bar, children: list[TreeNode]):
return StringNode("bar")
>>> CustomBuilder().build_tree(Foo())
StringNode("foo")
>>> CustomBuilder().build_tree(Bar())
StringNode("bar")
Expanding Children
So far we have only given examples of the production of leaf nodes, like integers and strings.
What if a node has children, like a list? We can handle this using the @Builder.expander
decorator. Here is an example of how a list can be built:
class CustomBuilder(Builder):
...
@Builder.expander(list)
def expand_list(self, node: list):
"""Returns an iterable over the node's children"""
yield from node
@Builder.builder(list)
def build_list(self, node: list, children: list[TreeNode]):
return ListNode(children)
>>> CustomBuilder().build_tree([1, 2, 3, 4])
ListNode([IntegerNode(1), IntegerNode(2), IntegerNode(3), IntegerNode(4)])
If an expander is not defined for a type, it is assumed that the type is a leaf with no children.
If the root node or one of its descendants is of a type that has no associated builder function, a NotImplementedError
is raised.
Graphtage has a subclassed builder graphtage.builder.BasicBuilder
that has builders and expanders for the Python basic types like int
, float
, str
, bytes
, list
, dict
, set
, and tuple
. You can extend graphtage.builder.BasicBuilder
to implement support for additional types.
Custom Nodes
Graphtage provides abstract classes like graphtage.ContainerNode
and graphtage.SequenceNode
to aid in the implementation of custom node types. But the easiest way to define a custom node type is to extend off of graphtage.dataclasses.DataClass
.
from graphtage import IntegerNode, ListNode, StringNode
from graphtage.dataclasses import DataClass
class CustomNode(DataClass):
name: StringNode
value: IntegerNode
attributes: ListNode
This will automatically build a node type that has three children: a string, an integer, and a list.
>>> CustomNode(name=StringNode("the name"), value=IntegerNode(1337), attributes=ListNode((IntegerNode(1), IntegerNode(2), IntegerNode(3))))
Let’s say you have another, non-graphtage class that corresponds to CustomNode
:
class NonGraphtageClass:
name: str
value: int
attributes: list[int]
You can add support for building Graphtage nodes from this custom class as follows:
class CustomBuilder(BasicBuilder):
@Builder.expander(NonGraphtageClass)
def expand_non_graphtage_class(node: NonGraphtageClass):
yield node.name
yield node.value
yield node.attributes
@Builder.builder(NonGraphtageClass)
def build_non_graphtage_class(node: NonGraphtageClass, children: List[TreeNode]) -> CustomNode:
return CustomNode(*children)