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):
    def build_int(self, node: int, children: list[TreeNode]):
        return IntegerNode(node)
>>> CustomBuilder().build_tree(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:

class Bar(Foo):

class CustomBuilder(Builder):
    def build_foo(self, node: Foo, children: list[TreeNode]):
        return StringNode("foo")

    def build_bar(self, node: Bar, children: list[TreeNode]):
        return StringNode("bar")
>>> CustomBuilder().build_tree(Foo())
>>> CustomBuilder().build_tree(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):

    def expand_list(self, node: list):
        """Returns an iterable over the node's children"""
        yield from node

    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):
    def expand_non_graphtage_class(node: NonGraphtageClass):
        yield node.value
        yield node.attributes

    def build_non_graphtage_class(node: NonGraphtageClass, children: List[TreeNode]) -> CustomNode:
        return CustomNode(*children)