.. _Builders: Constructing Graphtage Trees ============================ Graphtage operates on trees represented by the :class:`graphtage.TreeNode` base class. There are various predefined specializations of tree nodes, such as :class:`graphtage.IntegerNode` for integers, :class:`graphtage.ListNode` for lists, and :class:`graphtage.DictNode` for dictionaries. :class:`graphtage.TreeNode` has an optional :attr:`parent ` and a potentially empty set of :func:`children `. Graphtage provides a :class:`graphtage.builder.Builder` class for conveniently converting arbitrary objects into a tree of :class:`TreeNode ` objects. It uses Python magic to define the conversions. .. code-block:: python 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 :meth:`@Builder.builder(int) ` decorator specifies that the function is able to build a Graphtage `TreeNode` object from inputs that are :func:`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: .. code-block:: python 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 :meth:`@Builder.expander ` decorator. Here is an example of how a list can be built: .. code-block:: python 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 :exc:`NotImplementedError` is raised. Graphtage has a subclassed builder :class:`graphtage.builder.BasicBuilder` that has builders and expanders for the Python basic types like :class:`int`, :class:`float`, :class:`str`, :class:`bytes`, :class:`list`, :class:`dict`, :class:`set`, and :class:`tuple`. You can extend :class:`graphtage.builder.BasicBuilder` to implement support for additional types. Custom Nodes ------------ Graphtage provides abstract classes like :class:`graphtage.ContainerNode` and :class:`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 :class:`graphtage.dataclasses.DataClassNode`. .. code-block:: python 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``: .. code-block:: python class NonGraphtageClass: name: str value: int attributes: list[int] You can add support for building Graphtage nodes from this custom class as follows: .. code-block:: python 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 :class:`graphtage.dataclasses.DataClassNode` subclass become its *slots*: the children of the node, in the order they are declared. Only annotations that name a :class:`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: .. code-block:: python 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 :exc:`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 :exc:`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 ------------------------------ :meth:`DataClassNode.__init__ ` assigns the slots from its positional and keyword arguments, so overriding it means reimplementing that assignment. Override :meth:`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. .. code-block:: python 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.