Skip to content

Commit

Permalink
Exposed public interface for writable loader (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
sydneyli authored and etingof committed Dec 12, 2019
1 parent 22fff01 commit 4e9c315
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 36 deletions.
14 changes: 11 additions & 3 deletions apacheconfig/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,24 @@
from apacheconfig.lexer import make_lexer
from apacheconfig.parser import make_parser
from apacheconfig.loader import ApacheConfigLoader
from apacheconfig.wloader import ApacheConfigWritableLoader
from apacheconfig.error import ApacheConfigError
from apacheconfig import flavors # noqa: F401


@contextmanager
def make_loader(**options):
def make_loader(writable=False, **options):
ApacheConfigLexer = make_lexer(**options)
ApacheConfigParser = make_parser(**options)

yield ApacheConfigLoader(ApacheConfigParser(ApacheConfigLexer()),
**options)
if writable:
options["preservewhitespace"] = True
yield ApacheConfigWritableLoader(
ApacheConfigParser(ApacheConfigLexer(), start='contents'),
**options)
else:
yield ApacheConfigLoader(ApacheConfigParser(ApacheConfigLexer()),
**options)


__all__ = ['make_lexer', 'make_parser', 'make_loader',
Expand Down
54 changes: 52 additions & 2 deletions apacheconfig/wloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,58 @@
import abc
import six

from apacheconfig.reader import LocalHostReader

from apacheconfig import error


class ApacheConfigWritableLoader(object):
"""Manages a writable object represented by a single config file.
Args:
parser (:class:`apacheconfig.ApacheConfigParser`): compiled parser
to use when loading configuration directives.
options (dict): keyword args of options to set for loader. Should be
same set of options passed to parser.
"""

def __init__(self, parser, **options):
self._parser = parser
self._options = dict(options)
if 'reader' in self._options:
self._reader = self._options['reader']
else:
self._reader = LocalHostReader()

def load(self, filepath):
"""Loads config file into a raw modifiable AST.
Args:
filepath (Text): path of config file to load. Expects UTF-8
encoding.
Returns:
:class:`apacheconfig.ListNode` AST containing parsed config file.
Raises:
IOError: If there is a problem opening the file specified.
"""
with self._reader.open(filepath) as f:
return self.loads(f.read())

def loads(self, text):
"""Loads config text into a raw modifiable AST.
Args:
text (Text): (Text) containing the configuration to load.
Returns:
:class:`apacheconfig.ListNode` AST containing parsed config.
"""
ast = self._parser.parse(text)
return ListNode(ast, self._parser)


def _restore_original(word):
"""If the `word` is a Quoted string, restores it to original.
"""
Expand Down Expand Up @@ -109,7 +158,7 @@ def __init__(self, raw, parser):
self._trailing_whitespace = ""
self._parser = parser
for elem in raw[1:]:
if isinstance(elem, six.text_type) and elem.isspace():
if isinstance(elem, six.string_types) and elem.isspace():
self._trailing_whitespace = elem
elif elem[0] == "block":
self._contents.append(BlockNode(elem, parser))
Expand Down Expand Up @@ -439,14 +488,15 @@ def __init__(self, raw, parser):
"``apacheconfig.parser``. First element of data is not "
"\"block\" typestring.")
start = 1
if isinstance(raw[start], six.text_type) and raw[start].isspace():
if isinstance(raw[start], six.string_types) and raw[start].isspace():
self._whitespace = raw[start]
start += 1
self._full_tag = LeafNode(('statement',) + raw[start])
self._close_tag = raw[-1]
self._contents = None
if raw[start + 1]: # If we have a list of elements to process.
super(BlockNode, self).__init__(raw[start + 1], parser)
self._type = raw[0]

@classmethod
def parse(cls, raw_str, parser):
Expand Down
70 changes: 39 additions & 31 deletions tests/unit/test_wloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@
except ImportError:
import unittest

import os
import shutil
import tempfile

from apacheconfig import flavors
from apacheconfig import make_lexer
from apacheconfig import make_parser
from apacheconfig.wloader import BlockNode
from apacheconfig.wloader import LeafNode
from apacheconfig.wloader import ListNode
from apacheconfig import make_loader
from apacheconfig.error import ApacheConfigError


class WLoaderTestCaseWrite(unittest.TestCase):

def setUp(self):
ApacheConfigLexer = make_lexer(**flavors.NATIVE_APACHE)
ApacheConfigParser = make_parser(**flavors.NATIVE_APACHE)
self.parser = ApacheConfigParser(ApacheConfigLexer(), start="contents")
context = make_loader(writable=True, **flavors.NATIVE_APACHE)
self.loader = context.__enter__()
self.addCleanup(context.__exit__, None, None, None)

def testChangeItemValue(self):
cases = [
Expand All @@ -43,7 +43,7 @@ def testChangeItemValue(self):
'include new/path/to/file'),
]
for raw, new_value, expected in cases:
node = LeafNode.parse(raw, self.parser)
node = next(iter(self.loader.loads(raw)))
node.value = new_value
self.assertEqual(expected, node.dump())

Expand All @@ -53,7 +53,7 @@ def testChangeBlockValue(self):
("<block>\n</block>", "name2", "<block name2>\n</block>"),
]
for raw, new_value, expected in cases:
node = BlockNode.parse(raw, self.parser)
node = next(iter(self.loader.loads(raw)))
node.arguments = new_value
self.assertEqual(expected, node.dump())

Expand All @@ -72,7 +72,7 @@ def testAddToContents(self):
("# comment\n", 0, "\n1 2", "\n1 2\n# comment\n"),
]
for raw, index, to_add, expected in cases:
node = ListNode.parse(raw, self.parser)
node = self.loader.loads(raw)
node.add(index, to_add)
self.assertEqual(expected, node.dump())

Expand All @@ -85,17 +85,17 @@ def testRemoveFromContents(self):
("a # comment", 0, " # comment")
]
for raw, index, expected in cases:
node = ListNode.parse(raw, self.parser)
node = self.loader.loads(raw)
node.remove(index)
self.assertEqual(expected, node.dump())


class WLoaderTestCaseRead(unittest.TestCase):

def setUp(self):
ApacheConfigLexer = make_lexer(**flavors.NATIVE_APACHE)
ApacheConfigParser = make_parser(**flavors.NATIVE_APACHE)
self.parser = ApacheConfigParser(ApacheConfigLexer(), start="contents")
context = make_loader(writable=True, **flavors.NATIVE_APACHE)
self.loader = context.__enter__()
self.addCleanup(context.__exit__, None, None, None)

def testChangeItemValue(self):
cases = [
Expand All @@ -109,15 +109,13 @@ def testChangeItemValue(self):
'include new/path/to/file'),
]
for raw, new_value, expected in cases:
node = LeafNode.parse(raw, self.parser)
node = next(iter(self.loader.loads(raw)))
node.value = new_value
self.assertEqual(expected, node.dump())

def _test_item_cases(self, cases, expected_type, parser=None):
if not parser:
parser = self.parser
def _test_item_cases(self, cases, expected_type):
for raw, expected_name, expected_value in cases:
node = LeafNode.parse(raw, self.parser)
node = next(iter(self.loader.loads(raw)))
self.assertEqual(expected_name, node.name,
"Expected node('%s').name to be %s, got %s" %
(repr(raw), expected_name, node.name))
Expand Down Expand Up @@ -160,17 +158,17 @@ def testLoadApacheInclude(self):
(' include path', 'include', 'path'),
('\ninclude path', 'include', 'path'),
]
self._test_item_cases(cases, 'include', self.parser)
self._test_item_cases(cases, 'include')

def testDumpUnicodeSupport(self):
text = "\n value is 三"
node = LeafNode.parse(text, self.parser)
node = next(iter(self.loader.loads(text)))
dump = node.dump()
self.assertTrue(isinstance(dump, six.text_type))
self.assertEquals(dump, text)

def testStrUnicodeBuiltIns(self):
node = LeafNode.parse("\n option value", self.parser)
node = next(iter(self.loader.loads("\n option value")))
self.assertTrue(isinstance(str(node), str))
self.assertTrue(isinstance(node.__unicode__(), six.text_type))
self.assertEquals(
Expand All @@ -186,13 +184,14 @@ def testLoadContents(self):
('a b\n<b>\n</b> \n', ('a b', '\n<b>\n</b>')),
]
for raw, expected in cases:
node = ListNode.parse(raw, self.parser)
node = self.loader.loads(raw)
self.assertEqual(len(node), len(expected))
for got, expected in zip(node, expected):
self.assertEqual(got.dump(), expected)
self.assertEqual(raw, node.dump())

def testMalformedContents(self):
from apacheconfig.wloader import ListNode
cases = [
(["block", ("b",), [], "b"], "Wrong typestring."),
(["statement", "option", " ", "value"], "Wrong typestring."),
Expand All @@ -201,7 +200,7 @@ def testMalformedContents(self):
]
for case in cases:
self.assertRaises(ApacheConfigError, ListNode, case[0],
self.parser)
None)

def testModifyListIndexError(self):
cases = [
Expand All @@ -213,20 +212,21 @@ def testModifyListIndexError(self):
('\n', "add", 1),
]
for raw, fn, index in cases:
node = ListNode.parse(raw, self.parser)
node = self.loader.loads(raw)
self.assertEqual(raw, node.dump())
if fn == "add":
self.assertRaises(IndexError, node.add, index, " # comment")
else:
self.assertRaises(IndexError, node.remove, index)

def testListAddParsingError(self):
node = ListNode.parse("a b\nc d", self.parser)
node = self.loader.loads("a b\nc d")
cases = ["a b\nc d", ""]
for case in cases:
self.assertRaises(ApacheConfigError, node.add, 0, case)

def testMalformedBlocks(self):
from apacheconfig.wloader import BlockNode
cases = [
(["contents", []], "Wrong typestring."),
(["statement", "option", " ", "value"], "Wrong typestring."),
Expand All @@ -235,9 +235,10 @@ def testMalformedBlocks(self):
]
for case in cases:
self.assertRaises(ApacheConfigError, BlockNode, case[0],
self.parser)
None)

def testMalformedItem(self):
from apacheconfig.wloader import LeafNode
cases = [
(["contents", []], "Wrong typestring."),
(["block", ("b",), [], "b"], "Wrong typestring."),
Expand All @@ -256,12 +257,12 @@ def testLoadBlocks(self):
('\n<b>\n</b>', None),
]
for (raw, value) in cases:
node = BlockNode.parse(raw, self.parser)
node = next(iter(self.loader.loads(raw)))
self.assertEqual("b", node.tag)
self.assertEqual(value, node.arguments)
self.assertEqual(raw, node.dump())

def testLoadWholeConfig(self):
def testLoadWholeConfigFromFile(self):
text = """\
# a
Expand All @@ -275,6 +276,13 @@ def testLoadWholeConfig(self):
c "d d"
</a>
# a
"""
node = ListNode.parse(text, self.parser)
t = tempfile.mkdtemp()
filepath = os.path.join(t, "config")
with open(filepath, "w") as f:
f.write(text)
node = self.loader.load(filepath)
self.assertEqual(text, node.dump())
shutil.rmtree(t)

0 comments on commit 4e9c315

Please sign in to comment.