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, ListNode, StringNode, TreeNode
from graphtage.builder import BasicBuilder, 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 isinstance() of 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")
@Builder.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.sequences.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.DataClassNode.
from graphtage import IntegerNode, ListNode, StringNode
from graphtage.dataclasses import DataClassNode
class CustomNode(DataClassNode):
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))))
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(self, node: NonGraphtageClass):
yield node.name
yield node.value
yield node.attributes
@Builder.builder(NonGraphtageClass)
def build_non_graphtage_class(self, node: NonGraphtageClass, children: list[TreeNode]) -> CustomNode:
return CustomNode(*children)
Data Class Slots
The annotations on a graphtage.dataclasses.DataClassNode subclass become its slots: the children of the
node, in the order they are declared. Only annotations that name a graphtage.TreeNode subclass directly are
turned into slots. Every other annotation is ignored, including a subscripted generic like list[IntegerNode],
which is skipped rather than rejected:
class SkipsTheSecondAnnotation(DataClassNode):
name: StringNode
items: list[IntegerNode]
>>> SkipsTheSecondAnnotation._SLOTS
('name',)
Slot types are enforced when the node is constructed. Passing a node of the wrong type raises a ValueError:
>>> CustomNode(StringNode("the name"), StringNode("1337"), ListNode(()))
Traceback (most recent call last):
...
ValueError: Expected a node of type IntegerNode for argument CustomNode.value but instead got StringNode('1337')
A subclass cannot redefine a slot that one of its ancestors already declares. Doing so raises a TypeError when
the subclass is defined, not when it is instantiated:
>>> class Redefined(CustomNode):
... name: StringNode
Traceback (most recent call last):
...
TypeError: Dataclass Redefined cannot redefine slot 'name' because it is already defined in its superclass CustomNode
Initializing a Data Class Node
DataClassNode.__init__ assigns the slots from its positional
and keyword arguments, so overriding it means reimplementing that assignment. Override
graphtage.dataclasses.DataClassNode.post_init() instead. It is called once the slots have been assigned, and it
should not call super().post_init(): every implementation in the class hierarchy is called automatically,
starting with the least derived data class and ending with the class being instantiated.
class UnquotedName(DataClassNode):
name: StringNode
def post_init(self):
self.name.quoted = False
Note
An implementation that a subclass inherits without overriding runs once, not once per class that inherits it.