Welcome to Treepace tutorial. First, we import the library:
from treepace import *
The basic unit of all trees is a node.
Node("label")
In Treepace, any object (not only a string) can become a label of the node.
from glob import glob
from IPython.display import display
with open(glob('*.ipynb')[0], 'rb') as file_handle:
display(Node(file_handle))
A node has children, which can have other children...
root = Node('root',
[Node('c1'), Node('c2',
[Node('subchild')])])
A tree is defined by the reference to the root node.
Tree(root)
It is possible to load and save a tree to various formats like tab-indented / parenthesized text or XML.
print(Tree.load('root (element1 (sub-element) element2)').save(IndentedText))
root element1 sub-element element2
A subtree is a connected part of the tree consisting of the selected nodes of the main tree (highlighted with blue).
Subtree([root, root.children[1]])
As we will see later, searching methods return Match
objects. Each match consists of groups (subtrees), where the group 0 represents the whole match – just like in a regex. In this tutorial, it will be highlighted with green color.
c2 = root.children[1]
Match([Subtree([c2, c2.parent]),
Subtree([c2])
])
To search for a pattern anywhere in the tree, use the search()
method. The result is a list of matches.
The most basic pattern is a dot which matches one arbitrary node.
tree = Tree.load('a (b c)')
tree.search('.')
![]() | , | ![]() | , | ![]() |
A text literal matches the nodes whose string representation is equal to the given literal.
tree.search('a')
![]() |
A pattern can contain arbitrary Python code, enclosed in square brackets. The expression is evaluated for each relevant node (accessible in the expression via the variable node
) and matches if its result equals True
.
tree.search('[node.value != "c"]')
![]() | , | ![]() |
An underscore is a shortcut for node.value
.
tree.search('[_.upper() == "C"]')
![]() |
Multiple node patterns can be connected using relations. In the following example, we search for a node 'a' which has a child 'b'. The whole subtree is returned – not only the final component.
tree.search('a < b')
![]() |
Other availabe relations are: immediately following sibling (,
), any sibling (&
) and parent (>
).
tree.search('a < b, c')[0]
tree.search('a < c & b')[0]
The 'parent' relationship is implicitly followed by a 'match any node' pattern. This is useful to form queries like this:
Tree.load('a (b (c) d (e))').search('a < b <c>, d')[0]
To mark a part of the match as a group, use brackets. The groups are numbered from 1 and can be nested.
tree.search('{a < {b}, {c}}')[0]
It is possible to back-reference saved groups by $n
.
Tree.load('m (n (o) m (n))').search('{m < n}, $1')[0]
More complicated relationship between the nodes in a match can be expressed using back-references in a predicate.
nums = Tree(Node(1, [Node(-1), Node(0.5)]))
match = nums.search('{[_ != 2]} < [abs(_) == $1]')
match[0].group(0)
To assert that the match must begin exactly at the root node, use the match()
method.
Tree.load('node (node (node))').match('node < node')
![]() |
If the match must cover all nodes of the tree, the fullmatch()
method can be called. This is useful for validation.
fruits = Tree.load('fruits (apple pear apple)')
display(fruits)
if fruits.fullmatch('fruits < apple & pear'):
print('The stock contains at least one apple and pear, but no other fruit.')
else:
print('The condition is not met.')
The stock contains at least one apple and pear, but no other fruit.
The replace()
method substitutes all matches of the pattern with the given replacement. Although it is not necessary, we will first search for the pattern (for illustration):
shop = Tree.load('shop (item (bread) item (water) item (roll) item (water))')
pattern = '{item} < water'
display(shop.search(pattern))
![]() | , | ![]() |
The actual replacement is simple:
shop.replace(pattern, '$1 < juice')
display(shop)
The transformation consists of one or more rules in the form: pattern -> replacement
. Each rule is repeated until a match is found. In addition, the whole list of rules is repeatead while at least one rule finds a match. To illustrate this behavior, the following transformation is performed:
subject = Tree.load('a (b)')
print('Original:')
display(subject)
subject.transform('''x -> y
a -> x''')
print('Transformed:')
display(subject)
Original:
Transformed:
A more useful transformation follows. Here is a sample XML document:
text = '''<article>
<heading>An example</heading>
<content>
<calc>
<plus>
<elem>3</elem>
<elem>4</elem>
</plus>
</calc>
</content>
</article>'''
doc = Tree.load(text, XmlText)
doc
We will replace a semantic document representation with its visual HTML form and solve a mathematical expression.
doc.transform('''
article -> html < body
heading -> h1
content -> p
calc < plus < elem<{.}>, elem<{.}> -> [text(num($1) + num($2))]
''')
display(doc)
print(doc.save(XmlText))
<html> <body> <h1>An example</h1> <p>7</p> </body> </html>
This concludes the tutorial. You can install the library by running
py -m pip install treepace
on Windows or
pip install treepace
on Linux.