Source code for condconfigparser.parser

# -*- coding: utf-8 -*-

# parser.py --- Parser module of CondConfigParser
#
# Copyright (c) 2014, Florent Rougon
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of the CondConfigParser Project.

"""Parser module of CondConfigParser.

This module defines a :class:`Node` class, one subclass for every node
type that can occur in the abstract syntax tree, and a :class:`Parser`
class which implements the configuration file parsing using the output
of the lexer (:class:`condconfigparser.lexer.Lexer`).

"""


import re
import collections
import textwrap                 # minor use, could be easily replaced

from .exceptions import ParseError, UndefinedVariable, InTestTypeError
from .lexer import TokenType


[docs] class Node: """Node class for the abstract syntax tree. This class defines all the usual :class:`list` methods; they act on the list referenced to by the :attr:`children` attribute. Data belonging to the node that is not in the form of a :class:`Node` instance (or instance of a subclass) must be stored in other attributes than :attr:`children`. This class implements :meth:`object.__eq__` and :meth:`object.__ne__` for equality testing. This allows one to compare two abstract syntax trees by using ``==`` or ``!=`` with the two root nodes. """ def __init__(self, children=None): #: List of children nodes (:class:`Node` instances) self.children = [] if children is None else children def __eq__(self, other): return type(self) == type(other) and \ self.children == other.children def __ne__(self, other): return not self.__eq__(other) def __getattr__(self, name): if name in ("append", "extend"): return getattr(self.children, name) def __len__(self): return self.children.__len__() def __getitem__(self, key): return self.children.__getitem__(key) def __delitem__(self, key): return self.children.__delitem__(key) def __setitem__(self, key, value): return self.children.__setitem__(key, value) def __iter__(self): return self.children.__iter__() def __reversed__(self): return self.children.__reversed__() def __contains__(self, item): return self.children.__contains__(item) def __str__(self): return "<{}>".format(type(self).__name__) def __repr__(self): return "{0}.{1}({2!r})".format(__name__, type(self).__name__, self.children) def __format__(self, format_spec): """Support for :samp:`format({node}, "tree")` and \ :samp:`"{:tree}".format({node})`.""" if not format_spec: return str(self) else: assert format_spec == "tree", format_spec if self.children: indent = 2 childLines = '\n'.join( [ textwrap.indent("{:tree}".format(child), " "*indent) for child in self.children ]) return "{}\n{}".format(self, childLines) else: return str(self)
[docs] @classmethod def simplify(cls, node, parent, childNum): """Recursively replace useless :class:`AndTestNode` and \ :class:`OrTestNode` instances. Every :class:`AndTestNode` or :class:`OrTestNode` that only has one child is replaced with this only child. This process is recursively done on all children of *node*. Return the possibly-new root node of the tree. """ res = node if isinstance(node, (AndTestNode, OrTestNode)) and \ len(node.children) == 1: if parent is None: res = cls.simplify(node.children[0], None, None) else: parent.children[childNum] = node.children[0] cls.simplify(node.children[0], parent, childNum) else: for i, child in enumerate(node.children): cls.simplify(child, node, i) return res
[docs] def undefVariables(self, varNames): """Return the set of undefined variables for the tree rooted at *self*. This method recursively explores the subtree rooted at *self*. If any node encountered refers to a variable whose name is not in *varNames*, it remembers that variable as undefined. The return value is a :class:`frozenset` containing a :class:`condconfigparser.lexer.VariableToken` instance for every instance of an undefined variable in the tree rooted at *self*. """ return frozenset().union( *[ child.undefVariables(varNames) for child in self.children ])
[docs] class RootNode(Node): """Node representing the root of the abstract syntax tree for a \ config file/stream. Corresponds to the :token:`root` grammar production. """ def __init__(self, assignments, config): Node.__init__(self, [assignments, config]) def __repr__(self): return "{}.{}({!r}, {!r})".format(__name__, type(self).__name__, self.assignments, self.config) # Convenience properties @property def assignments(self): """:class:`VarAssignmentsNode` instance.""" return self.children[0] @assignments.setter def assignments(self, value): self.children[0] = value @property def config(self): """:class:`ConfigNode` instance.""" return self.children[1] @config.setter def config(self, value): self.children[1] = value
[docs] class VarAssignmentsNode(Node): """Node representing zero or more variable assignments. Corresponds to the :token:`varAssignments` grammar production. """
[docs] class AssignmentNode(Node): """Node representing a variable assignment. Corresponds to the :token:`variableAssignment` grammar production. """ def __init__(self, lhs, rhs): Node.__init__(self, [lhs, rhs]) def __repr__(self): return "{}.{}({!r}, {!r})".format(__name__, type(self).__name__, self.lhs, self.rhs) # Convenience properties to access the left hand side, variable name and # right hand side of the assignment @property def lhs(self): """:class:`VariableNode` instance for the assigned-to variable.""" return self.children[0] @lhs.setter def lhs(self, value): self.children[0] = value @property def name(self): """Name of the assigned-to variable.""" return self.children[0].name @property def rhs(self): """:class:`Node` instance representing the expression that defines \ the variable.""" return self.children[1] @rhs.setter def rhs(self, value): self.children[1] = value
def _truncateConfigLines(cfgLines): l = [] for line in cfgLines: # Truncate the config lines since they will all be printed on a # single line... if len(line) >= 15: # 10 + len(" ...") = 15 - 1 l.append("{} ...".format(line[:10])) else: l.append(line) return l
[docs] class ConfigNode(Node): """Node representing the default, unconditional section of the config \ file as well as the list of its conditional sections. Corresponds to the :token:`config` grammar production. """ def __init__(self, defaultConfigLines=None, sections=None): defaultConfigLines = \ defaultConfigLines if defaultConfigLines is not None else [] sections = sections if sections is not None else [] Node.__init__(self, sections) #: List of unconditional configuration lines (:class:`str` instances) self.defaultConfigLines = defaultConfigLines def __eq__(self, other): return type(self) == type(other) and \ self.children == other.children and \ self.defaultConfigLines == other.defaultConfigLines def __repr__(self): return "{}.{}({!r}, {!r})".format(__name__, type(self).__name__, self.defaultConfigLines, self.sections) def __str__(self): return "<{} {!r}>".format(type(self).__name__, _truncateConfigLines(self.defaultConfigLines)) # Convenience property @property def sections(self): """List of :class:`SectionNode` instances.""" return self.children @sections.setter def sections(self, value): self.children = value
[docs] class SectionNode(Node): """Node representing a conditional section of the config file. Corresponds to the :token:`section` grammar production. """ def __init__(self, startToken, predicate, rawConfigLines): Node.__init__(self, [predicate]) for n in ("startToken", "rawConfigLines"): setattr(self, n, locals()[n]) def __eq__(self, other): return type(self) == type(other) and \ self.children == other.children and \ self.startToken == other.startToken and \ self.rawConfigLines == other.rawConfigLines def __repr__(self): return "{}.{}({!r}, {!r}, {!r})".format( __name__, type(self).__name__, self.startToken, self.predicate, self.rawConfigLines) def __str__(self): return "<{} {!r}>".format(type(self).__name__, _truncateConfigLines(self.rawConfigLines)) # Convenience property @property def predicate(self): """Abstract syntax tree representing the predicate. This may be an :class:`OrTestNode`, an :class:`AndTestNode`, a :class:`ListLiteralNode`, etc. In any case, it is an instance of a :class:`Node` subclass. """ return self.children[0] @predicate.setter def predicate(self, value): self.children[0] = value
[docs] class OrTestNode(Node): """Node representing a short-circuit, logical ``or`` test. The semantics are the same as in Python, including the result of the evaluation. cf. https://docs.python.org/3/reference/expressions.html#boolean-operations Corresponds to the :token:`orTest` grammar production. """
[docs] def eval(self, context): """Return the node value, after recursive evaluation of its children.""" for child in self.children: res = child.eval(context) if res: break return res
[docs] class AndTestNode(Node): """Node representing a short-circuit, logical ``and`` test. The semantics are the same as in Python, including the result of the evaluation. cf. https://docs.python.org/3/reference/expressions.html#boolean-operations Corresponds to the :token:`andTest` grammar production. """
[docs] def eval(self, context): """Return the node value, after recursive evaluation of its children.""" for child in self.children: res = child.eval(context) if not res: break return res
[docs] class NotTestNode(Node): """Node representing a logical ``not`` test. Related to the :token:`notTest` grammar production. """ def __init__(self, child): Node.__init__(self, [child]) def __repr__(self): return "{}.{}({!r})".format(__name__, type(self).__name__, self.child)
[docs] def eval(self, context): """Return the node value, after recursive evaluation of its child.""" return not self.child.eval(context)
# Convenience property @property def child(self): """Child node of the :class:`NotTestNode` instance. It is an instance of a :class:`Node` subclass. """ return self.children[0] @child.setter def child(self, value): self.children[0] = value
[docs] class BinOpNodeBase(Node): """Base class for nodes having ``lOp`` and ``rOp`` properties.""" def __init__(self, opToken, lOp, rOp): """Initialize a :class:`BinOpNodeBase` instance. *opToken* is the binary operator token instance (lexeme). It may be used to obtain the start/end line and column numbers of the binary operator, for instance. *lOp* and *rOp* are the left and right operands and must be instances of a subclass of :class:`Node`. """ Node.__init__(self, [lOp, rOp]) self.opToken = opToken def __repr__(self): return "{}.{}({!r}, {!r}, {!r})".format( __name__, type(self).__name__, self.opToken, self.lOp, self.rOp) # Convenience properties to access the left and right operands of # the binary operator @property def lOp(self): """Left operand of the binary operator. It is an instance of a subclass of :class:`Node`. """ return self.children[0] @lOp.setter def lOp(self, value): self.children[0] = value @property def rOp(self): """Right operand of the binary operator. It is an instance of a subclass of :class:`Node`. """ return self.children[1] @rOp.setter def rOp(self, value): self.children[1] = value
[docs] class EqualsTestNode(BinOpNodeBase): """Node representing an ``==`` test. Corresponds to the :token:`equalsTest` grammar production. """
[docs] def eval(self, context): """Return the node value, after recursive evaluation of its children.""" return self.lOp.eval(context) == self.rOp.eval(context)
[docs] class NotEqualsTestNode(BinOpNodeBase): """Node representing an ``!=`` test. Corresponds to the :token:`notEqualsTest` grammar production. """
[docs] def eval(self, context): """Return the node value, after recursive evaluation of its children.""" return self.lOp.eval(context) != self.rOp.eval(context)
[docs] class InTestNode(BinOpNodeBase): """Node representing an ``in`` test. Corresponds to the :token:`inTest` grammar production. """
[docs] def eval(self, context): """Return the node value, after recursive evaluation of its children.""" try: res = self.lOp.eval(context) in self.rOp.eval(context) except TypeError as e: raise InTestTypeError(self.opToken, e) from e return res
[docs] class VariableNode(Node): """Node representing a variable reference. Corresponds to the :token:`variable` grammar symbol. """ def __init__(self, variableToken): Node.__init__(self) #: :class:`condconfigparser.lexer.VariableToken` instance self.token = variableToken #: Variable name (:class:`str`) self.name = variableToken.string def __eq__(self, other): return type(self) == type(other) and \ self.token == other.token def __repr__(self): return "{}.{}({!r})".format(__name__, type(self).__name__, self.token) def __str__(self): return "<{} {!r}>".format(type(self).__name__, self.name)
[docs] def eval(self, context): """Return the node value, according to *context*. The node value is the value of the variable whose name is given by :attr:`name`, according to *context*. """ try: return context[self.name] except KeyError as e: raise UndefinedVariable(self.name) from e
[docs] def undefVariables(self, varNames): """Return the set of undefined variables. The set of undefined variables is determined under the assumption that every variable with a name in *varNames* is defined. """ if self.name in varNames: return frozenset() else: return frozenset((self.token,))
[docs] class StringLiteralNode(Node): """Node representing a string literal. Corresponds to the :token:`stringLiteral` grammar symbol. """ def __init__(self, value): Node.__init__(self) #: The actual string represented by the node (:class:`str` instance) self.value = value def __eq__(self, other): return type(self) == type(other) and \ self.value == other.value def __repr__(self): return "{}.{}({!r})".format(__name__, type(self).__name__, self.value) def __str__(self): return "<{} {!r}>".format(type(self).__name__, self.value)
[docs] def eval(self, context): """Return the node value, which is the string literal represented by \ the node.""" return self.value
[docs] class BoolLiteralNode(Node): """Node representing a boolean literal (i.e., ``True`` or ``False``). Corresponds to the :token:`boolLiteral` grammar symbol. """ def __init__(self, value): Node.__init__(self) #: The actual boolean represented by the node (:class:`bool` instance) self.value = value def __eq__(self, other): return type(self) == type(other) and \ self.value == other.value def __repr__(self): return "{}.{}({!r})".format(__name__, type(self).__name__, self.value) def __str__(self): return "<{} {!r}>".format(type(self).__name__, self.value)
[docs] def eval(self, context): """Return the node value, which is the boolean literal represented by \ the node.""" return self.value
[docs] class ListLiteralNode(Node): """Node representing a list literal. The elements of the list are represented by the :class:`Node` instances forming the list referred to by the :attr:`children` attribute (same order, of course). Corresponds to the :token:`listLiteral` grammar symbol. """ def __init__(self, items): Node.__init__(self, items)
[docs] def eval(self, context): """Return the node value, which is the list literal represented by \ the node.""" return [ item.eval(context) for item in self.children ]
[docs] class Parser: """Parser class of CondConfigParser. This class implements a recursive descent parser that performs look-ahead in order to avoid any need for backtracking. The algorithm is typical of a LL(1) parser that does not use parse tables. For more information, you may refer to: https://dickgrune.com/Books/PTAPG_1st_Edition/ """ def __init__(self, lexer): #: :class:`condconfigparser.lexer.Lexer` instance self.lexer = lexer #: Queue holding the tokens generated by the :attr:`lexer` self.queue = collections.deque() #: Token generator from :attr:`lexer` self.tokenGenerator = lexer.tokenGenerator() #: Last token popped from :attr:`queue`, i.e., consumed by the parser self.lastToken = None
[docs] def enqueue(self, num): """Pull *num* tokens from :attr:`tokenGenerator`, pushing them into \ :attr:`queue`.""" while len(self.queue) < num: try: self.queue.append(next(self.tokenGenerator)) except StopIteration: break return len(self.queue)
[docs] def peekAt(self, index): """Look at an element of :attr:`queue` without consuming it. If *index* is 0, look at the first unprocessed token in :attr:`queue`. If *index* is 1, look at the next unprocessed token, etc.. Return the looked-at token, or ``None`` if it does not exist. """ queueLen = self.enqueue(index+1) if index < queueLen: return self.queue[index] else: return None
[docs] def peekSeveral(self, num): """Return the topmost *num* tokens without consuming them. Return a list of length at most *num*. If EOF is reached before enough tokens can be read, the returned list will have less than *num* elements. If the returned list *l* has at least one element, :samp:`{l}[0]` is the first unprocessed token; if it has at least two elements, :samp:`{l}[1]` is the second unprocessed token, etc. """ queueLen = self.enqueue(num) return list(self.queue)[:num]
[docs] def readToken(self): """Read a token from :attr:`queue`. If there are no tokens in :attr:`queue`, return ``None``. Otherwise, consume one token from :attr:`queue`, save it in :attr:`lastToken` for later reference and return it. """ if not self.enqueue(1): return None # No more token (EOF) else: self.lastToken = self.queue.popleft() return self.lastToken
[docs] def match1(self, tokenType): """Match one token of type *tokenType*. Read a token from :attr:`queue`. If none is available or if the token is not of type *tokenType*, raise :exc:`ParseError`. Otherwise, return the token that was read. """ t = self.readToken() if t is None: # EOF raise ParseError((self.lexer.line, self.lexer.col), "EOF reached while trying to read a <{}> token" .format(tokenType.name)) elif t.type != tokenType: raise ParseError(self.lastToken.pos(), "expected a <{}> token, but found a <{}> instead" .format(tokenType.name, t.type.name)) return t
[docs] def matchZeroOrMore(self, tokenType): """Match zero or more tokens of type *tokenType*. Return the list of matched tokens (the match is greedy). """ l = [] while True: t = self.peekAt(0) if t is None: break # EOF elif t.type == tokenType: l.append(self.match1(tokenType)) else: break return l
[docs] def matchOneOrMore(self, tokenType): """Match one or more tokens of type *tokenType*. Return the list of matched tokens (the match is greedy). Raise :exc:`ParseError` if the first unprocessed token is not of type *tokenType*. """ return [self.match1(tokenType)] + self.matchZeroOrMore(tokenType)
[docs] def root(self): """Match a :token:`root` production.""" t = self.peekAt(0) if t is None: # EOF return RootNode(VarAssignmentsNode(), ConfigNode()) # empty elif t.type == TokenType.varAssignmentsStart: assignments = self.varAssignments() else: assignments = VarAssignmentsNode() # empty return RootNode(assignments, self.config())
[docs] def varAssignments(self): """Match a :token:`varAssignments` production.""" self.match1(TokenType.varAssignmentsStart) a = self.varAssigs() self.match1(TokenType.varAssignmentsEnd) self.matchOneOrMore(TokenType.newline) return a
[docs] def varAssigs(self): """Match a :token:`varAssigs` production.""" self.matchZeroOrMore(TokenType.newline) assignments = VarAssignmentsNode() while True: t = self.peekAt(0) if t is None: break elif t.type == TokenType.variable: assignments.append(self.variableAssignment()) nextToken = self.peekAt(0) if nextToken is not None and \ nextToken.type != TokenType.varAssignmentsEnd: self.matchOneOrMore(TokenType.newline) elif t.type == TokenType.varAssignmentsEnd: break else: raise ParseError(self.lastToken.pos(), "unexpected token while parsing a " "<varAssigs>: {}".format(self.lastToken)) return assignments
[docs] def variableAssignment(self): """Match a :token:`variableAssignment` production.""" variable = VariableNode(self.match1(TokenType.variable)) self.match1(TokenType.assignOp) return AssignmentNode(variable, self.orTest())
[docs] def config(self): """Match a :token:`config` production.""" defaultConfigLines = [ t.string for t in self.matchZeroOrMore(TokenType.rawConfigLine) ] sections = [] while True: t = self.peekAt(0) if t is None: break else: sections.append(self.section()) return ConfigNode(defaultConfigLines, sections)
[docs] def section(self): """Match a :token:`section` production.""" startToken, predicate = self.predicate() rawConfigLines = [ t.string for t in self.matchZeroOrMore(TokenType.rawConfigLine) ] return SectionNode(startToken, predicate, rawConfigLines)
[docs] def predicate(self): """Match a :token:`predicate` production.""" startToken = self.match1(TokenType.predicateStart) ot = self.orTest() self.match1(TokenType.predicateEnd) self.matchOneOrMore(TokenType.newline) return (startToken, ot)
[docs] def orTest(self): """Match an :token:`orTest` production.""" l = [self.andTest()] while True: t = self.peekAt(0) if t is None or t.type != TokenType.orOp: break else: self.match1(TokenType.orOp) l.append(self.andTest()) return OrTestNode(l)
[docs] def andTest(self): """Match an :token:`andTest` production.""" l = [self.notTest()] while True: t = self.peekAt(0) if t is None or t.type != TokenType.andOp: break else: self.match1(TokenType.andOp) l.append(self.notTest()) return AndTestNode(l)
[docs] def notTest(self): """Match a :token:`notTest` production.""" t = self.peekAt(0) if t is None: raise ParseError((self.lexer.line, self.lexer.col), "EOF reached while trying to read a <notTest>") elif t.type == TokenType.notOp: self.match1(TokenType.notOp) node = NotTestNode(self.notTest()) else: node = self.atomicBool() return node
[docs] def atomicBool(self): """Match an :token:`atomicBool` production.""" # All derivations of <atomicBool> start with an <expr>. In order to # avoid any backtracking, we have to factor it out: eat an <expr> now # and maybe pass it as parameter to equalsTest(), notEqualsTest() or # inTest(), depending on the token following the <expr>. expr = self.expr() t = self.peekAt(0) if t is None: return expr elif t.type == TokenType.equalsOp: return self.equalsTest(expr) elif t.type == TokenType.notEqualsOp: return self.notEqualsTest(expr) elif t.type == TokenType.inOp: return self.inTest(expr) else: return expr
# cf. comments in atomicBool() concerning the lOp parameter
[docs] def equalsTest(self, lOp): """Match an :token:`equalsTest` production.""" opToken = self.match1(TokenType.equalsOp) rOp = self.expr() return EqualsTestNode(opToken, lOp, rOp)
# cf. comments in atomicBool() concerning the lOp parameter
[docs] def notEqualsTest(self, lOp): """Match a :token:`notEqualsTest` production.""" opToken = self.match1(TokenType.notEqualsOp) rOp = self.expr() return NotEqualsTestNode(opToken, lOp, rOp)
# cf. comments in atomicBool() concerning the lOp parameter
[docs] def inTest(self, lOp): """Match an :token:`inTest` production.""" opToken = self.match1(TokenType.inOp) rOp = self.expr() return InTestNode(opToken, lOp, rOp)
[docs] def expr(self): """Match an :token:`expr` production.""" t = self.peekAt(0) if t is None: raise ParseError((self.lexer.line, self.lexer.col), "EOF reached while trying to read an <expr>") elif t.type == TokenType.variable: node = VariableNode(self.match1(TokenType.variable)) elif t.type == TokenType.openParen: self.match1(TokenType.openParen) node = self.orTest() self.match1(TokenType.closeParen) else: node = self.literal() return node
[docs] def literal(self): """Match a :token:`literal` production.""" t = self.peekAt(0) if t is None: raise ParseError((self.lexer.line, self.lexer.col), "EOF reached while trying to read a <literal>") elif t.type == TokenType.stringLiteral: return StringLiteralNode( self.match1(TokenType.stringLiteral).value) elif t.type == TokenType.listStart: return self.listLiteral() else: return self.boolLiteral()
[docs] def boolLiteral(self): """Match a :token:`boolLiteral` production.""" t = self.peekAt(0) if t is None: raise ParseError((self.lexer.line, self.lexer.col), "EOF reached while trying to read a <boolLiteral>") elif t.type == TokenType.true: self.match1(TokenType.true) return BoolLiteralNode(True) else: self.match1(TokenType.false) return BoolLiteralNode(False)
[docs] def listLiteral(self): """Match a :token:`listLiteral` production.""" self.match1(TokenType.listStart) items = [] # Set to False after the first element has been read first = True while True: t = self.peekAt(0) if t is None: raise ParseError((self.lexer.line, self.lexer.col), "EOF reached while trying to read a " "<listLiteral>") elif t.type == TokenType.listEnd: self.match1(TokenType.listEnd) break elif first: items.append(self.orTest()) first = False else: self.match1(TokenType.comma) items.append(self.orTest()) return ListLiteralNode(items)
[docs] def buildTree(self): """Parse a complete configuration file. Return the root node of the corresponding abstract syntax tree, after simplification (cf. :meth:`Node.simplify`). """ rootNode = self.root() return Node.simplify(rootNode, None, None)