-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add initial AST node class hierarchy (#72)
Also introduced parser "flavors" to adjust it to syntax variations.
- Loading branch information
Showing
5 changed files
with
308 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# | ||
# This file is part of apacheconfig software. | ||
# | ||
# Copyright (c) 2018-2019, Ilya Etingof <[email protected]> | ||
# License: https://github.com/etingof/apacheconfig/LICENSE.rst | ||
# | ||
|
||
NATIVE_APACHE = { | ||
"preservewhitespace": True, | ||
"disableemptyelementtags": True, | ||
"multilinehashcomments": True, | ||
"useapacheinclude": True, | ||
} | ||
""" Set of options for apacheconfig to work with native Apache config files""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
# | ||
# This file is part of apacheconfig software. | ||
# | ||
# Copyright (c) 2018-2019, Ilya Etingof <[email protected]> | ||
# License: https://github.com/etingof/apacheconfig/LICENSE.rst | ||
# | ||
import abc | ||
import six | ||
|
||
|
||
def _restore_original(word): | ||
"""If the `word` is a Quoted string, restores it to original. | ||
""" | ||
if getattr(word, 'is_single_quoted', False): | ||
return "'%s'" % word | ||
if getattr(word, 'is_double_quoted', False): | ||
return '"%s"' % word | ||
return word | ||
|
||
|
||
@six.add_metaclass(abc.ABCMeta) | ||
class AbstractASTNode(object): | ||
"""Generic class containing data that represents a node in the config AST. | ||
""" | ||
|
||
@abc.abstractmethod | ||
def dump(self): | ||
"""Returns the contents of this node as in a config file.""" | ||
|
||
@abc.abstractproperty | ||
def typestring(self): | ||
"""Returns object typestring as defined by the apacheconfig parser.""" | ||
|
||
@abc.abstractproperty | ||
def whitespace(self): | ||
"""Returns preceding or trailing whitespace for this node as a string. | ||
""" | ||
|
||
@abc.abstractmethod | ||
@whitespace.setter | ||
def whitespace(self, value): | ||
"""Set preceding or trailing whitespace for this node. | ||
Args: | ||
value (str): value to set whitespace to. | ||
""" | ||
|
||
|
||
class LeafASTNode(AbstractASTNode): | ||
"""Creates object containing a simple list of tokens. | ||
Also manages any preceding whitespace. Can represent a key/value option, | ||
a comment, or an include/includeoptional directive. | ||
Examples of what LeafASTNode might look like for different directives:: | ||
"option" | ||
name: "option", value: None, whitespace: "" | ||
"include relative/path/*" | ||
name: "include", value: "relative/path/*", whitespace: "" | ||
"\\n option = value" | ||
name: "option", value: "value", whitespace: "\\n " | ||
"# here is a comment" | ||
name: "# here is a comment", value: None, whitespace: "" | ||
"\\n # here is a comment" | ||
name: "# here is a comment", value: None, whitespace: "\\n " | ||
To construct from a raw string, use the `parse` constructor. The regular | ||
constructor receives data from the internal apacheconfig parser. | ||
Args: | ||
raw (list): Raw data returned from ``apacheconfig.parser``. | ||
""" | ||
|
||
def __init__(self, raw): | ||
self._type = raw[0] | ||
self._raw = tuple(raw[1:]) | ||
self._whitespace = "" | ||
if len(raw) > 1 and raw[1].isspace(): | ||
self._whitespace = raw[1] | ||
self._raw = tuple(raw[2:]) | ||
|
||
@property | ||
def typestring(self): | ||
"""Returns object typestring as defined by the apacheconfig parser. | ||
Returns: | ||
The typestring (as defined by the apacheconfig parser) for | ||
this node. The possible values for this are ``"comment"``, | ||
``"statement"``, ``"include"``, ``"includeoptional"``. | ||
""" | ||
return self._type | ||
|
||
@property | ||
def whitespace(self): | ||
"""Returns preceding whitespace for this node. | ||
For example:: | ||
LeafASTNode('\\n option value').whitespace => "\\n " | ||
LeafASTNode('option value').whitespace => "" | ||
LeafASTNode('\\n # comment').whitespace => "\\n " | ||
""" | ||
return self._whitespace | ||
|
||
@whitespace.setter | ||
def whitespace(self, value): | ||
"""See base class. Operates on preceding whitespace.""" | ||
self._whitespace = value | ||
|
||
@classmethod | ||
def parse(cls, raw_str, parser): | ||
"""Factory for :class:`apacheconfig.LeafASTNode` by parsing data from a | ||
config string. | ||
Args: | ||
raw_str (string): The text to parse. | ||
parser (:class:`apacheconfig.ApacheConfigParser`): specify the | ||
parser to use. Can be created by ``native_apache_parser()``. | ||
Returns: | ||
:class:`apacheconfig.LeafASTNode` containing metadata parsed from | ||
``raw_str``. | ||
""" | ||
raw = parser.parse(raw_str) | ||
return cls(raw[1]) | ||
|
||
@property | ||
def name(self): | ||
"""Returns the name of this node. | ||
The name is the first non-whitespace token in the directive. Cannot be | ||
written. For comments, is the entire comment. | ||
""" | ||
return self._raw[0] | ||
|
||
@property | ||
def has_value(self): | ||
"""Returns ``True`` if this :class:`apacheconfig.LeafASTNode` has a value. | ||
``LeafASTNode`` objects don't have to have a value, like option/value | ||
directives with no value, or comments. | ||
""" | ||
return len(self._raw) > 1 | ||
|
||
@property | ||
def value(self): | ||
"""Returns the value of this item as a string. | ||
The "value" is anything but the name. Can be overwritten. | ||
""" | ||
if not self.has_value: | ||
return | ||
return self._raw[-1] | ||
|
||
@value.setter | ||
def value(self, value): | ||
"""Sets for the value of this item. | ||
Args: | ||
value (str): string to set new value to. | ||
.. todo:: (sydneyli) convert `value` to quotedstring when quoted | ||
""" | ||
if not self.has_value: | ||
self._raw = self._raw + (" ", value,) | ||
self._raw = self._raw[0:-1] + (value,) | ||
|
||
def dump(self): | ||
return (self.whitespace + | ||
"".join([_restore_original(word) for word in self._raw])) | ||
|
||
def __str__(self): | ||
return ("%s(%s)" | ||
% (self.__class__.__name__, | ||
str([self._type] + | ||
[_restore_original(word) for word in self._raw]))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
ply>=3.0 | ||
ply>=3.0 | ||
six |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
# | ||
# This file is part of apacheconfig software. | ||
# | ||
# Copyright (c) 2018-2019, Ilya Etingof <[email protected]> | ||
# License: https://github.com/etingof/apacheconfig/LICENSE.rst | ||
# | ||
try: | ||
import unittest2 as unittest | ||
|
||
except ImportError: | ||
import unittest | ||
|
||
from apacheconfig import LeafASTNode | ||
from apacheconfig import flavors | ||
from apacheconfig import make_lexer | ||
from apacheconfig import make_parser | ||
|
||
|
||
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") | ||
|
||
def testChangeItemValue(self): | ||
cases = [ | ||
('option value', 'value2', 'option value2'), | ||
('\noption value', 'value2', '\noption value2'), | ||
('\noption =\\\n value', 'value2', '\noption =\\\n value2'), | ||
('option value', 'long value', 'option long value'), | ||
('option value', '"long value"', 'option "long value"'), | ||
('option', 'option2', 'option option2'), | ||
('include old/path/to/file', 'new/path/to/file', | ||
'include new/path/to/file'), | ||
] | ||
for raw, new_value, expected in cases: | ||
node = LeafASTNode.parse(raw, self.parser) | ||
node.value = new_value | ||
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") | ||
|
||
def testChangeItemValue(self): | ||
cases = [ | ||
('option value', 'value2', 'option value2'), | ||
('\noption value', 'value2', '\noption value2'), | ||
('\noption =\\\n value', 'value2', '\noption =\\\n value2'), | ||
('option value', 'long value', 'option long value'), | ||
('option value', '"long value"', 'option "long value"'), | ||
('option', 'option2', 'option option2'), | ||
('include old/path/to/file', 'new/path/to/file', | ||
'include new/path/to/file'), | ||
] | ||
for raw, new_value, expected in cases: | ||
node = LeafASTNode.parse(raw, self.parser) | ||
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 | ||
for raw, expected_name, expected_value in cases: | ||
node = LeafASTNode.parse(raw, self.parser) | ||
self.assertEqual(expected_name, node.name, | ||
"Expected node('%s').name to be %s, got %s" % | ||
(repr(raw), expected_name, node.name)) | ||
self.assertEqual(expected_value, node.value, | ||
"Expected node('%s').value to be %s, got %s" % | ||
(repr(raw), expected_value, node.value)) | ||
self.assertEqual(raw, node.dump(), | ||
("Expected node('%s').dump() to be the same, " | ||
"but got '%s'" % (repr(raw), node.dump()))) | ||
self.assertEqual(expected_type, node.typestring, | ||
("Expected node('%s').typestring to be '%s', " | ||
"but got '%s'" % (repr(raw), expected_type, | ||
str(node.typestring)))) | ||
|
||
def testLoadStatement(self): | ||
cases = [ | ||
('option value', 'option', 'value'), | ||
('option', 'option', None), | ||
(' option value', 'option', 'value'), | ||
(' option = value', 'option', 'value'), | ||
('\noption value', 'option', 'value'), | ||
('option "dblquoted value"', 'option', 'dblquoted value'), | ||
("option 'sglquoted value'", "option", "sglquoted value"), | ||
] | ||
self._test_item_cases(cases, 'statement') | ||
|
||
def testLoadComment(self): | ||
comment = '# here is a silly comment' | ||
cases = [ | ||
(comment, comment, None), | ||
('\n' + comment, comment, None), | ||
(' ' + comment, comment, None), | ||
] | ||
self._test_item_cases(cases, 'comment') | ||
|
||
def testLoadApacheInclude(self): | ||
cases = [ | ||
('include path', 'include', 'path'), | ||
(' include path', 'include', 'path'), | ||
('\ninclude path', 'include', 'path'), | ||
] | ||
self._test_item_cases(cases, 'include', self.parser) |