<repo_name>catatonicprime/sacp<file_sep>/README.md
# Simple Apache Config Parser
Welcome to the Simple Apache Config Parser! This package is intended to ease the parsing/analysis of apache config files. This parser uses the Apache Config Lexer provided by the [pygments](http://pygments.org/) project.

This project is still very much in its infancy, but my focus is on providing easy to use/understand object interfaces to analyze and modify apache config files while attempting to minimize the deltas between the original configs and the modified content. If this software is not quite meeting your needs, drop in an Issue and I'll do my best to address/help, but even if that's failing checkout this other neat parser [apacheconfig](https://github.com/etingof/apacheconfig).

# Example usage
Here are some example usages. Note that these examples assume your current working directory properly set to match Include patterns. In the examples below this is for a CentOS installation where the root apache directory tends to be /etc/httpd/.

## Parsing
Parsing a config file is easy!
```python
from sacp import *
cf = ConfigFile(file="conf/httpd.conf")
```
This will automatically parse the httpd.conf from the current directory. Any dependent configs (e.g. those that are listed in an Include or IncludeOptional directive) will also be loaded.

## Walking the nodes
Visiting all the nodes is also easy!
```python
from sacp import *
  
def visit(node):
    print("{}{}".format(node.depth * " ", type(node)))

cf = ConfigFile(file="conf/httpd.conf")
NodeVisitor([cf]).visit(visitor=visit)
```
This visits all of the nodes in the config file, including it's children, and prints each node type with it's relative depth represented as well.

# Contribute
Want to contribute? Awesome! Fork, code, and create a PR.
<file_sep>/tests/tests.py
import unittest
import os
from sacp import *


class TestInclude(unittest.TestCase):
    def test_path(self):
        # Ensure we have an 'Include' node
        parser = Parser(data='Include files/small_vhost.conf')
        self.assertTrue(parser)
        self.assertEqual(len(parser.nodes), 1, 'Parser returned incorrect number of nodes for Include')
        include = parser.nodes[0]
        self.assertTrue(isinstance(include, Include))

        # Ensure the path matches the expected path above.
        self.assertEqual(include.path, 'files/small_vhost.conf', 'Include path does not match expected.')

    def test_path_absolute_glob(self):
        # Ensure we have an 'Include' node
        parser = Parser(data="Include {}/files/glob/*.conf".format(os.getcwd()))
        self.assertTrue(parser)
        self.assertEqual(len(parser.nodes), 1, 'Parser returned incorrect number of nodes for Include')
        include = parser.nodes[0]
        self.assertTrue(isinstance(include, Include))

    def test_exceptions(self):
        with self.assertRaises(IncludeError):
            Parser(data='Include')
        with self.assertRaises(ValueError):
            Parser(data='Include nonexistent.conf')

    def test_child_tokens_not_accessed(self):
        include = Parser(data='Include files/small_vhost.conf').nodes[0]
        self.assertEqual(len(include.tokens), 4)


class TestIncludeOptional(unittest.TestCase):
    def test_path(self):
        # Ensure we have an 'Include' node
        parser = Parser(data='IncludeOptional files/small_vhost.conf')
        self.assertTrue(parser)
        self.assertEqual(len(parser.nodes), 1, 'Parser returned incorrect number of nodes for IncludeOptional')
        include = parser.nodes[0]
        self.assertTrue(isinstance(include, IncludeOptional))

        # Ensure the path matches the expected path above.
        self.assertEqual(include.path, 'files/small_vhost.conf', 'IncludeOptional path does not match expected.')

    def test_exceptions(self):
        with self.assertRaises(IncludeError):
            Parser(data='IncludeOptional')

    def test_failed_include(self):
        Parser(data='IncludeOptional nonexistent.conf')


class TestParser(unittest.TestCase):
    def test_parents(self):
        # Ensure we have a known structure.
        configFile = ConfigFile(file='files/small_vhost.conf')
        self.assertTrue(configFile)
        self.assertTrue(isinstance(configFile, ConfigFile))
        self.assertTrue(configFile.children)

        # Extract the VirtualHost from the structure.
        vhost = configFile.children[0]
        self.assertTrue(isinstance(vhost, VirtualHost))
        self.assertTrue(isinstance(vhost._parent, ConfigFile))

        # Extract the first child, which should be a Directive, from the VirtualHost
        directive = vhost.children[0]
        self.assertTrue(isinstance(directive, Directive))

        # Ensure the Directives parent is properly typed to a VirtualHost
        self.assertTrue(isinstance(directive._parent, VirtualHost))

    def test_parser(self):
        nodes = Parser(data="ServerName github.com").nodes
        self.assertEqual(len(nodes), 1)
        with self.assertRaises(ValueError):
            Parser(data="ServerName github.com", nodefactory=0)
        with self.assertRaises(ValueError):
            Parser(data="ServerName github.com", acl=NodeFactory())

    def test_children(self):
        configFile = ConfigFile(file='files/small_httpd.conf')
        self.assertTrue(configFile)
        self.assertTrue(isinstance(configFile, ConfigFile))
        self.assertTrue(configFile.children)
        self.assertGreaterEqual(len(configFile.children), 3)
        comment = configFile.children[0]
        self.assertTrue(isinstance(comment, Comment))
        directive = configFile.children[1]
        self.assertTrue(isinstance(directive, Directive))
        multilineComment = configFile.children[2]
        self.assertTrue(isinstance(multilineComment, Comment))
        self.assertTrue('Multi-line\\\ncomment' in str(multilineComment))

    def test_exceptions(self):
        with self.assertRaises(ValueError):
            configFile = ConfigFile(file='files/lex_errors.conf')


class TestNode(unittest.TestCase):
    def test_append_child(self):
        # Ensure we have some structure.
        configFile = ConfigFile(file='files/small_vhost.conf')

        # Ensure we have a new node, it can be blank.
        node = Node()
        self.assertTrue(node)

        # Ensure the node can be successfully appended.
        configFile.append_child(node)
        self.assertGreaterEqual(configFile.children.index(node), 0)

        # Ensure the node has been modified to have the correct parent.
        self.assertEqual(node.parent, configFile)

        # Test the depth of the node.
        self.assertEqual(node.depth-1, node.parent.depth)

    def test_append_children(self):
        # Ensure we have some structure.
        configFile = ConfigFile(file='files/small_vhost.conf')

        # Ensure we have a new node, it can be blank.
        node = Node()
        self.assertTrue(node)

        # Ensure the configFile can be successfully appended.
        node.append_children([configFile])
        self.assertGreaterEqual(node.children.index(configFile), 0)

        # Ensure the configFile has been modified to have the correct parent.
        self.assertEqual(configFile.parent, node)

        # Test the depth of the node.
        self.assertEqual(configFile.depth-1, configFile.parent.depth)

    def test_node_factory(self):
        nf = NodeFactory()
        node = Node()
        with self.assertRaises(NotImplementedError):
            nf.build(node)


class TestDirective(unittest.TestCase):
    def test_name(self):
        configFile = ConfigFile(file='files/small_vhost.conf')
        vhost = configFile.children[0]
        self.assertEqual(vhost.name, "VirtualHost")
        sn = vhost.children[0]
        self.assertEqual(sn.name, "ServerName")


class TestServerName(unittest.TestCase):
    def test_server_name(self):
        configFile = ConfigFile(file='files/small_vhost.conf')
        vhost = configFile.children[0]
        sn = vhost.children[0]
        self.assertTrue(sn.isValid)
        self.assertEqual(sn.arguments[0], "github.com")
        self.assertEqual(vhost.server_name.arguments[0], "github.com")

    def test_empty_server_name(self):
        configFile = ConfigFile(file='files/bad_vhost.conf')
        vhost = configFile.children[0]
        sn = vhost.children[0]
        self.assertFalse(sn.isValid)

    def test_bad_servername(self):
        nodes = Parser(data='ServerName invalid/domain').nodes
        sn = nodes[0]
        self.assertFalse(sn.isValid)


class TestServerAlias(unittest.TestCase):
    def test_server_alias(self):
        configFile = ConfigFile(file='files/small_vhost.conf')
        vhost = configFile.children[0]
        sa = vhost.children[1]
        self.assertTrue(sa.isValid)
        self.assertEqual(sa.arguments[0], "www.github.com")
        self.assertEqual(vhost.server_alias.arguments[0], "www.github.com")
        self.assertEqual(sa.arguments[1], "m.github.com")
        self.assertEqual(vhost.server_alias.arguments[1], "m.github.com")

    def test_bad_server_alias(self):
        nodes = Parser(data='ServerAlias www.github.com bad/alias github.com').nodes
        sa = nodes[0]
        self.assertFalse(sa.isValid)

    def test_empty_server_alias(self):
        configFile = ConfigFile(file='files/bad_vhost.conf')
        vhost = configFile.children[0]
        sa = vhost.children[1]
        self.assertFalse(sa.isValid)


class TestConfigFile(unittest.TestCase):
    def test_write(self):
        testPath = '/tmp/.small_vhost.conf'
        configFile = ConfigFile(file='files/small_vhost.conf')
        cf_str_left = str(configFile)
        self.assertFalse(os.path.exists(testPath))
        configFile._file = testPath
        configFile.write()
        self.assertTrue(os.path.exists(testPath))
        configFile = ConfigFile(file=testPath)
        cf_str_right = str(configFile)
        self.assertEqual(cf_str_left, cf_str_right)
        os.remove(testPath)


class TestNodeVisitors(unittest.TestCase):
    def __init__(self, methodName="runTest"):
        (unittest.TestCase).__init__(self, methodName=methodName)
        self._parsed_nodes = ConfigFile(file='files/small_visitors.conf').children

    def reset_test_state(self):
        self._node_visit_index = 0
        self._node_list = []

    def visitor(self, node):
        self._node_list.append((node, self._node_visit_index))
        self._node_visit_index += 1

    def test_node_visitor(self):
        self.reset_test_state()
        NodeVisitor(self._parsed_nodes).visit(visitor=self.visitor)
        self.assertEqual(self._node_visit_index, 2)
        self.assertEqual(self._node_list[0][1], 0)
        self.assertEqual(self._node_list[1][1], 1)
        self.assertTrue(isinstance(self._node_list[0][0], VirtualHost))
        self.assertTrue(isinstance(self._node_list[1][0], VirtualHost))

    def test_dfnode_visitor(self):
        self.reset_test_state()
        DFNodeVisitor(self._parsed_nodes).visit(visitor=self.visitor)
        self.assertEqual(self._node_visit_index, 4)
        self.assertEqual(self._node_list[0][1], 0)
        self.assertEqual(self._node_list[1][1], 1)
        self.assertEqual(self._node_list[2][1], 2)
        self.assertEqual(self._node_list[3][1], 3)
        self.assertTrue(isinstance(self._node_list[0][0], VirtualHost))
        self.assertTrue(isinstance(self._node_list[1][0], ServerName))
        self.assertTrue(isinstance(self._node_list[2][0], VirtualHost))
        self.assertTrue(isinstance(self._node_list[3][0], ServerName))

    def test_bfnode_visitor(self):
        self.reset_test_state()
        BFNodeVisitor(self._parsed_nodes).visit(visitor=self.visitor)
        self.assertEqual(self._node_visit_index, 4)
        self.assertEqual(self._node_list[0][1], 0)
        self.assertEqual(self._node_list[1][1], 1)
        self.assertEqual(self._node_list[2][1], 2)
        self.assertEqual(self._node_list[3][1], 3)
        self.assertTrue(isinstance(self._node_list[0][0], VirtualHost))
        self.assertTrue(isinstance(self._node_list[1][0], VirtualHost))
        self.assertTrue(isinstance(self._node_list[2][0], ServerName))
        self.assertTrue(isinstance(self._node_list[3][0], ServerName))


class TestDefaultFactory(unittest.TestCase):
    def test_factory_builds(self):
        configFile = ConfigFile(file='files/factory.conf')
        self.assertTrue(isinstance(configFile.children[0], Comment))
        self.assertTrue(isinstance(configFile.children[1], Comment))
        self.assertTrue(isinstance(configFile.children[2], Directive))
        self.assertTrue(isinstance(configFile.children[3], Directive))
        self.assertTrue(isinstance(configFile.children[4], ScopedDirective))
        self.assertTrue(isinstance(configFile.children[5], Directory))
        self.assertTrue(isinstance(configFile.children[6], Directory))
        self.assertTrue(isinstance(configFile.children[7], DirectoryMatch))
        self.assertTrue(isinstance(configFile.children[8], Files))
        self.assertTrue(isinstance(configFile.children[9], Files))
        self.assertTrue(isinstance(configFile.children[10], FilesMatch))
        self.assertTrue(isinstance(configFile.children[11], Location))
        self.assertTrue(isinstance(configFile.children[12], LocationMatch))
        self.assertTrue(isinstance(configFile.children[13], Proxy))
        self.assertTrue(isinstance(configFile.children[14], ProxyMatch))


class TestScopedDirective(unittest.TestCase):
    def test_argument_child_tokens(self):
        configFile = ConfigFile(file='files/small_vhost.conf')
        self.assertTrue(isinstance(configFile.children[0], ScopedDirective))
        sd = configFile.children[0]
        self.assertTrue(len(sd.arguments) == 1)


class TestLineEnumerator(unittest.TestCase):
    def test_line_numbers(self):
        cf = ConfigFile(file='files/factory.conf')
        le = LineEnumerator(nodes=[cf])
        self.assertTrue(isinstance(le.lines[0][0], Comment))
        self.assertTrue(le.lines[0][1] == 1)
        self.assertTrue(isinstance(le.lines[1][0], Comment))
        self.assertTrue(le.lines[1][1] == 2)
        self.assertTrue(isinstance(le.lines[2][0], Directive))
        self.assertTrue(le.lines[2][1] == 5)


    def test_line_number_recurse_include(self):
        parser = Parser(data='Directive simple\nInclude files/factory.conf\nDirective simple')
        le = LineEnumerator(nodes=parser.nodes)
        self.assertTrue(isinstance(le.lines[0][0], Directive))
        self.assertTrue(le.lines[0][1] == 1)
        self.assertTrue(isinstance(le.lines[1][0], Include))
        self.assertTrue(le.lines[1][1] == 2)
        self.assertTrue(isinstance(le.lines[-1][0], Directive))
        self.assertTrue(le.lines[2][1] == 3)

    def test_line_number_recurse_include_optional(self):
        parser = Parser(data='Directive simple\nIncludeOptional files/factory.conf\nDirective simple')
        le = LineEnumerator(nodes=parser.nodes)
        self.assertTrue(isinstance(le.lines[0][0], Directive))
        self.assertTrue(le.lines[0][1] == 1)
        self.assertTrue(isinstance(le.lines[1][0], IncludeOptional))
        self.assertTrue(le.lines[1][1] == 2)
        self.assertTrue(isinstance(le.lines[-1][0], Directive))
        self.assertTrue(le.lines[2][1] == 3)
<file_sep>/sacp/base.py
from .node import *
from pygments.lexer import RegexLexer, default, words, bygroups, include, using
from pygments.token import Text, Comment as pygComment, Operator, Keyword, Name, String, \
    Number, Punctuation, Whitespace, Literal
import pygments
import glob


class Parser:
    def __init__(self, data, nodefactory=None, parent=None, acl=None):
        # Use specified node generator to generate nodes or use the default.
        if nodefactory is None:
            nodefactory = DefaultFactory()
        if not isinstance(nodefactory, NodeFactory):
            raise ValueError("nodefactory must be of type NodeFactory")
        self._nodefactory = nodefactory

        # Use specified lexer to generate tokens or use the default.
        if acl is None:
            acl = ApacheConfLexer(ensurenl=False, stripnl=False)

        if not isinstance(acl, ApacheConfLexer):
            raise ValueError("acl must be of type ApacheConfLexer")

        self._stream = pygments.lex(data, acl)

        # Start parsing and tracking nodes
        self.nodes = []
        node = self.parse(parent=parent)
        while node:
            self.nodes.append(node)
            node = self.parse(parent=parent)

    def parse(self, parent=None):
        node = Node(parent=parent)
        # Flag that indicates we will be exiting a scoped directive after this
        # node completes building.
        for token in self._stream:
            if token[0] is Token.Error:
                raise ValueError("Config has errors, bailing.")
            node.pretokens.append(token)

            # Nodes don't have types until their first non-whitespace token is
            # matched, this aggregates all the whitespace tokens to the front
            # of the node.
            if not node.type_token:
                continue

            # The node has a type, the lexer will return either a Token.Text
            # with an empty OR string comprised only of newlines before the next node info is
            # available.
            if token[0] is Token.Text and (token[1] == '' or re.search(r'^\n+\s*$', token[1])):
                return self._nodefactory.build(node)

            # When handling Tag tokens, e.g. nested components, we need to
            # know if we're at the start OR end of the Tag. Check for '</'
            # first and flag that this node will be the closing tag or not,
            # if so and > is encountered then return this node since it is
            # complete, this closes the scoped directive.
            if token[0] is Token.Name.Tag and token[1][0] == '<' and token[1][1] == '/':
                node.closeTag = True
            if token[0] is Token.Name.Tag and token[1][0] == '>':
                # If we're closing a tag it's time to return this node.
                if node.closeTag:
                    return self._nodefactory.build(node)

                # Otherwise, we're starting a Tag instead, begin building out
                # the children nodes for this node.
                child = self.parse(parent=node)
                while child and child.closeTag is False:
                    node.children.append(child)
                    child = self.parse(parent=node)

                # If the child was a </tag> node it, migrate it's tokens into
                # posttokens for this node.
                if child and child.closeTag:
                    for pt in child.tokens:
                        node.posttokens.append(pt)
                return self._nodefactory.build(node)
        if len(node.tokens) > 0:
            # At the end of files we may sometimes have some white-space stragglers
            # this if block captures those into a new node.
            return node
        return None


class DefaultFactory(NodeFactory):
    def __init__(self):
        NodeFactory.__init__(self)
        pass

    def build(self, node):
        if node.type_token[0] is Token.Name.Tag:
            node = ScopedDirective(node=node)
        elif node.type_token[0] is Token.Name.Builtin:
            node = Directive(node=node)
        elif node.type_token[0] is Token.Comment:
            node = Comment(node=node)

        if isinstance(node, ScopedDirective):
            if node.name.lower() == 'virtualhost':
                node = VirtualHost(node=node)
            if node.name.lower() == 'directory':
                node = Directory(node=node)
            if node.name.lower() == 'directorymatch':
                node = DirectoryMatch(node=node)
            if node.name.lower() == 'files':
                node = Files(node=node)
            if node.name.lower() == 'filesmatch':
                node = FilesMatch(node=node)
            if node.name.lower() == 'location':
                node = Location(node=node)
            if node.name.lower() == 'locationmatch':
                node = LocationMatch(node=node)
            if node.name.lower() == 'proxy':
                node = Proxy(node=node)
            if node.name.lower() == 'proxymatch':
                node = ProxyMatch(node=node)
        if isinstance(node, Directive):
            if node.name.lower() == 'servername':
                node = ServerName(node=node)
            elif node.name.lower() == 'serveralias':
                node = ServerAlias(node=node)
            elif node.name.lower() == 'include':
                node = Include(node=node)
            elif node.name.lower() == 'includeoptional':
                node = IncludeOptional(node=node)

        # Fix up children's parent.
        for child in node.children:
            child._parent = node

        return node


class Directive(Node):
    @property
    def name(self):
        return self.type_token[1]

    @property
    def arguments(self):
        """
        return: Array of arguments following a directive.
        example: 'ServerName example.com' returns [u'example.com']
        example: 'Deny from all' returns [u'from', u'all']
        """
        args = []
        directiveIndex = self._pretokens.index(self.type_token)
        for token in self._pretokens[directiveIndex+1:]:
            if token[0] is Token.Text and (not token[1] or token[1].isspace()):
                continue
            if token[0] is Token.Name.Tag:
                continue
            args.append(token[1].strip())
        return args


class ScopedDirective(Directive):
    @property
    def name(self):
        return super(ScopedDirective, self).name.split('<')[1]


class Comment(Node):
    pass


class ConfigFile(Node):
    def __init__(self, node=None, file=None):
        Node.__init__(self, node=node)
        self._file = file
        if file:
            with open(file, "r") as f:
                data = f.read()
            self._parser = Parser(data, parent=self)
            self._children = self._parser.nodes

    def write(self):
        with open(self._file, "w") as self.__fh:
            self.__fh.write(str(self))


class VirtualHost(ScopedDirective):
    @property
    def server_name(self):
        for node in self.children:
            if isinstance(node, ServerName):
                return node

    @property
    def server_alias(self):
        for node in self.children:
            if isinstance(node, ServerAlias):
                return node


class ServerName(Directive):
    @property
    def isValid(self):
        if len(self.arguments) == 0 or len(self.arguments) > 1:
            return False

        regex = '^((?P<scheme>[a-zA-Z]+)(://))?(?P<domain>[a-zA-Z_0-9.]+)(:(?P<port>[0-9]+))?\\s*$'
        match = re.search(regex, self.arguments[0])
        if match:
            return True
        return False


class ServerAlias(Directive):
    @property
    def isValid(self):
        if len(self.arguments) == 0:
            return False

        regex = '^(?P<domain>[a-zA-Z_0-9.]+)\\s*$'
        for name in self.arguments:
            match = re.search(regex, name)
            if not match:
                return False
        return True


class IncludeError(Exception):
    pass


class Include(Directive):
    def __init__(self, node=None):
        Node.__init__(self, node=node)
        if not self.path:
            raise IncludeError("path cannot be none")
        if len(glob.glob(self.path)) == 0:
            raise ValueError("Include directive failed to include '{}'".format(self.path))
        for path in glob.glob(self.path):
            cf = ConfigFile(file=path)
            cf._parent = self
            self._children.append(cf)

    @property
    def path(self):
        """
        :return: The glob pattern used to load additional configs.
        """
        if len(self.arguments) > 0:
            return self.arguments[0].strip()
        return None

    @property
    def tokens(self):
        """
        :return: List of all the tokens for this node concatenated together, excluding children.
        """
        # Process pretokens, skip children, ignore posttokens because Includes will never have them.
        tokenList = []
        for token in self._pretokens:
            tokenList.append(token)
        return tokenList


class IncludeOptional(Include):
    def __init__(self, node=None):
        try:
            Include.__init__(self, node=node)
        except ValueError:
            # Optional means we ignore when no files match the path and the ValueError exception is raised
            pass


class Directory(ScopedDirective):
    pass


class DirectoryMatch(ScopedDirective):
    pass


class Files(ScopedDirective):
    pass


class FilesMatch(ScopedDirective):
    pass


class Location(ScopedDirective):
    pass


class LocationMatch(ScopedDirective):
    pass


class Proxy(ScopedDirective):
    pass


class ProxyMatch(ScopedDirective):
    pass
<file_sep>/sacp/utilities.py
from .base import *


class LineEnumerator(NodeVisitor):
    def __init__(self, nodes):
        NodeVisitor.__init__(self, nodes=nodes)
        self._lineno = 1
        self.lines = []
        self.visit(visitor=self.visitor)

    def visit(self, visitor):
        # We need to do depth-first node visit except for into Include or IncludeOptional.
        for node in self._nodes:
            visitor(node)
            if isinstance(node, Include):
                continue
            if node.children:
                nv = LineEnumerator(node.children)
                nv.visit(visitor)

    def visitor(self, node):
        if isinstance(node, ConfigFile):
            self._lineno = 1
        if node.type_token is None:
            return
        type_index = node._pretokens.index(node.type_token)
        for token in node._pretokens[:type_index]:
            self._lineno += token[1].count('\n')    
        self.lines.append((node, self._lineno))
        for token in node._pretokens[type_index:]:
            self._lineno += token[1].count('\n')
<file_sep>/sacp/node.py
import re
from pygments.token import Token


class Node:
    """
    Node structure:

            Node:
        /     |     \
    pre - children - post

    When processing tokens, e.g. when rendering, we process pre tokens first,
    then the children nodes, then the post tokens.

    Example:
    <VirtualHost *:80>
    ServerName server
    </VirtualHost>

    [<VirtualHost][ ][*:80][>][\n] -> pretokens
    [ServerName][ server][\n] -> children / pretokens
    [</VirtualHost][>][\n] -> posttokens
    """

    def __init__(self, node=None, close_tag=False, parent=None):
        self._parent = parent
        self._pretokens = []
        self._children = []
        self._posttokens = []
        self.closeTag = close_tag
        if node:
            self._parent = node._parent
            self._pretokens = node._pretokens or []
            self._children = node._children or []
            self._posttokens = node._posttokens or []
            self.closeTag = node.closeTag

    @property
    def tokens(self):
        """
        :return: List of all the tokens for this node & it's children
                 concatenated together.
        """
        tokenList = []
        for token in self._pretokens:
            tokenList.append(token)
        for child in self._children:
            for token in child.tokens:
                tokenList.append(token)
        for token in self._posttokens:
            tokenList.append(token)
        return tokenList

    @property
    def parent(self):
        return self._parent

    @property
    def pretokens(self):
        return self._pretokens

    @property
    def children(self):
        return self._children

    @property
    def posttokens(self):
        return self._posttokens

    @property
    def type_token(self):
        """
        :return: returns the first non-whitespace token for the node. This is the first indicator of the type for this node.
        """
        for token in self._pretokens:
            if token[0] is Token.Text and token[1].isspace():
                continue
            return token
        return None

    @property
    def depth(self):
        depth = 0
        node = self._parent
        while node:
            depth += 1
            node = node._parent
        return depth

    def append_child(self, node):
        node._parent = self
        self._children.append(node)

    def append_children(self, nodes):
        for node in nodes:
            self.append_child(node)

    def __str__(self):
        s = ''
        for token in self.tokens:
            s += "{}".format(token[1])
        return s


class NodeFactory:
    def __init__(self):
        pass

    def build(self, node):
        """
        Used to build a begin building a new Node, is called as soon as a parser identifies enough information to construct a node.
        """
        raise NotImplementedError


class NodeVisitor:
    def __init__(self, nodes=None):
        self._nodes = nodes

    def visit(self, visitor):
        for node in self._nodes:
            visitor(node)


class DFNodeVisitor(NodeVisitor):
    def visit(self, visitor):
        for node in self._nodes:
            visitor(node)
            if node.children:
                nv = DFNodeVisitor(node.children)
                nv.visit(visitor)


class BFNodeVisitor(NodeVisitor):
    def visit(self, visitor):
        for node in self._nodes:
            visitor(node)

        for node in self._nodes:
            if node.children:
                nv = BFNodeVisitor(node.children)
                nv.visit(visitor)
<file_sep>/sacp/__init__.py
from .base import *
from .utilities import *
