Add parsernode utils to check keyword arguments and document the defaults in pydoc

This commit is contained in:
Joona Hoikkala 2019-08-20 20:36:46 +03:00
parent 864f06100a
commit 11f977fa22
No known key found for this signature in database
GPG key ID: D5AA86BBF9B29A5C
3 changed files with 152 additions and 47 deletions

View file

@ -116,20 +116,21 @@ class ParserNode(object):
ParserNode objects should have the following attributes:
# Reference to ancestor node, or None if the node is the root node of the
# configuration tree.
# configuration tree. Required.
ancestor: Optional[ParserNode]
# True if this node has been modified since last save.
# True if this node has been modified since last save. Default: False
dirty: bool
# Filepath of the file where the configuration element for this ParserNode
# object resides. This can be None if a configuration directive is defined in
# for example the httpd command line.
# object resides. For root node, the value for filepath is the httpd root
# configuration file. Filepath can be None if a configuration directive is
# defined in for example the httpd command line. Required.
filepath: Optional[str]
"""
@abc.abstractmethod
def __init__(self, ancestor=None, filepath="", dirty=False): # pragma: no cover
def __init__(self, **kwargs):
"""
Initializes the ParserNode instance, and sets the ParserNode specific
instance variables. This is not meant to be used directly, but through
@ -153,6 +154,7 @@ class ParserNode(object):
"""
# Linter rule exclusion done because of https://github.com/PyCQA/pylint/issues/179
@six.add_metaclass(abc.ABCMeta) # pylint: disable=abstract-method
class CommentNode(ParserNode):
@ -164,17 +166,18 @@ class CommentNode(ParserNode):
CommentNode stores its contents in class variable 'comment' and does not
have a specific name.
CommentNode objects should have the following attributes:
CommentNode objects should have the following attributes in addition to
the ones described in ParserNode:
# Contains the contents of the comment without the directive notation
# (typically # or /* ... */).
# (typically # or /* ... */). Required.
comment: str
"""
# pylint: disable=super-init-not-called
@abc.abstractmethod
def __init__(self, comment, ancestor, filepath, dirty=False): # pragma: no cover
def __init__(self, **kwargs):
"""
Initializes the CommentNode instance and sets its instance variables.
@ -199,22 +202,23 @@ class DirectiveNode(ParserNode):
variable for this DirectiveNode should be None, and it should be inserted to the
beginning of root BlockNode children sequence.
DirectiveNode objects should have the following attributes:
DirectiveNode objects should have the following attributes in addition to
the ones described in ParserNode:
# True if this DirectiveNode is enabled and False if it is inside of an
# inactive conditional block.
# inactive conditional block. Default: True
enabled: bool
# Name, or key of the configuration directive
# Name, or key of the configuration directive. Required.
name: str
# Tuple of parameters of this ParserNode object, excluding whitespaces.
# Default: ()
parameters: Tuple[str, ...]
"""
# pylint: disable=too-many-arguments, super-init-not-called
def __init__(self, name, parameters=(), ancestor=None, filepath="",
dirty=False, enabled=True): # pragma: no cover
def __init__(self, **kwargs):
"""
Initializes the DirectiveNode instance and sets its instance variables.
@ -277,37 +281,34 @@ class BlockNode(ParserNode):
The applicable parameters are dependent on the underlying configuration language
and its grammar.
BlockNode objects should have the following attributes:
BlockNode objects should have the following attributes in addition to
the ones described in ParserNode:
# True if this BlockNode is enabled and False if it is inside of an
# inactive conditional block. If a BlockNode contains an unmatched
# conditional statement, it should itself be flagged as enabled as it's
# parsed, but its children should be flagged as disabled.
# parsed, but its children should be flagged as disabled. Default: True
enabled: bool
# Name, or key of the configuration directive. If the BlockNode is the root
# configuration node, the name should be None.
# configuration node, the name should be None. Required.
name: Optional[str]
# Tuple of parameters of this ParserNode object, excluding whitespaces.
# Default: ()
parameters: Tuple[str, ...]
# Tuple of ParserNode objects that are the children for this node. The order
# of the children is the same s that of the parsed configuration block.
children: Tuple[ParserNode, ...]
"""
# pylint: disable=too-many-arguments, super-init-not-called
@abc.abstractmethod
def __init__(self, name="", parameters=(), children=(), ancestor=None,
filepath="", dirty=False, enabled=True): # pragma: no cover
def __init__(self, **kwargs):
"""
Initializes the BlockNode instance and sets its instance variables.
:param name: Name or key of the BlockNode object, or None for root
configuration node.
:param tuple parameters: Tuple of str parameters for this BlockNode.
:param tuple children: Tuple of ParserNode children for this BlockNode.
:param ancestor: BlockNode ancestor for this BlockNode, or None for root
configuration node.
:param str filepath: Filesystem path for the file where this BlockNode does

View file

@ -0,0 +1,74 @@
"""ParserNode utils"""
def validate_kwargs(kwargs, required_names):
"""
Ensures that the kwargs dict has all the expected values.
:param dict kwargs: Dictionary of keyword arguments to validate.
:param list required_names: List of required parameter names.
"""
validated_kwargs = dict()
for name in required_names:
try:
validated_kwargs[name] = kwargs.pop(name)
except KeyError:
raise TypeError("Required keyword argument: {} undefined.".format(name))
# Raise exception if unknown key word arguments are found.
if kwargs:
unknown = ", ".join(kwargs.keys())
raise TypeError("Unknown keyword argument(s): {}".format(unknown))
return validated_kwargs
def parsernode_kwargs(kwargs):
"""
Validates keyword arguments for ParserNode.
:param dict kwargs: Keyword argument dictionary to validate.
:returns: Tuple of validated and prepared arguments.
"""
kwargs.setdefault("dirty", False)
kwargs = validate_kwargs(kwargs, ["ancestor", "dirty", "filepath"])
return kwargs["ancestor"], kwargs["dirty"], kwargs["filepath"]
def commentnode_kwargs(kwargs):
"""
Validates keyword arguments for CommentNode and sets the default values for
optional kwargs.
:param dict kwargs: Keyword argument dictionary to validate.
:returns: Tuple of validated and prepared arguments and the remaining kwargs.
"""
kwargs.setdefault("dirty", False)
kwargs = validate_kwargs(kwargs, ["ancestor", "dirty", "filepath", "comment"])
comment = kwargs.pop("comment")
return comment, kwargs
def node_kwargs(kwargs):
"""
Validates keyword arguments for DirectiveNode and BlockNode and sets the
default values for optional kwargs.
:param dict kwargs: Keyword argument dictionary to validate.
:returns: Tuple of validated and prepared arguments.
"""
kwargs.setdefault("dirty", False)
kwargs.setdefault("enabled", True)
kwargs.setdefault("parameters", ())
kwargs = validate_kwargs(kwargs, ["ancestor", "dirty", "filepath", "name",
"parameters", "enabled"])
name = kwargs.pop("name")
parameters = kwargs.pop("parameters")
enabled = kwargs.pop("enabled")
return name, parameters, enabled, kwargs

View file

@ -5,22 +5,21 @@ import unittest
from acme.magic_typing import Optional, Tuple # pylint: disable=unused-import, no-name-in-module
from certbot_apache import interfaces
from certbot_apache import parsernode_util as util
class DummyParserNode(interfaces.ParserNode):
""" A dummy class implementing ParserNode interface """
ancestor = None
dirty = False
filepath = None
def __init__(self, ancestor=None, filepath=str(), dirty=False):
def __init__(self, **kwargs):
"""
Initializes the ParserNode instance.
"""
super(DummyParserNode, self).__init__(ancestor, filepath, dirty)
ancestor, dirty, filepath = util.parsernode_kwargs(kwargs)
self.ancestor = ancestor
self.dirty = dirty
self.filepath = filepath
super(DummyParserNode, self).__init__(**kwargs)
def save(self, msg): # pragma: no cover
"""Save"""
@ -30,31 +29,30 @@ class DummyParserNode(interfaces.ParserNode):
class DummyCommentNode(DummyParserNode):
""" A dummy class implementing CommentNode interface """
def __init__(self, comment, ancestor, filepath, dirty=False):
def __init__(self, **kwargs):
"""
Initializes the CommentNode instance and sets its instance variables.
"""
super(DummyCommentNode, self).__init__(ancestor, filepath, dirty)
comment, kwargs = util.commentnode_kwargs(kwargs)
self.comment = comment
super(DummyCommentNode, self).__init__(**kwargs)
class DummyDirectiveNode(DummyParserNode):
""" A dummy class implementing DirectiveNode interface """
parameters = tuple() # type: Tuple[str, ...]
enabled = True
name = ""
# pylint: disable=too-many-arguments
def __init__(self, name, parameters=(), ancestor=None, filepath="",
dirty=False, enabled=True):
def __init__(self, **kwargs):
"""
Initializes the DirectiveNode instance and sets its instance variables.
"""
super(DummyDirectiveNode, self).__init__(ancestor, filepath, dirty)
name, parameters, enabled, kwargs = util.node_kwargs(kwargs)
self.name = name
self.parameters = parameters
self.enabled = enabled
super(DummyDirectiveNode, self).__init__(**kwargs)
def set_parameters(self, parameters): # pragma: no cover
"""Set parameters"""
pass
@ -62,23 +60,19 @@ class DummyDirectiveNode(DummyParserNode):
class DummyBlockNode(DummyParserNode):
""" A dummy class implementing BlockNode interface """
parameters = tuple() # type: Tuple[str, ...]
children = tuple() # type: Tuple[interfaces.ParserNode, ...]
enabled = True
name = ""
# pylint: disable=too-many-arguments
def __init__(self, name="", parameters=(), children=(), ancestor=None,
filepath="", dirty=False, enabled=True):
def __init__(self, **kwargs):
"""
Initializes the BlockNode instance and sets its instance variables.
"""
super(DummyBlockNode, self).__init__(ancestor, filepath, dirty)
name, parameters, enabled, kwargs = util.node_kwargs(kwargs)
self.name = name
self.parameters = parameters
self.children = children
self.enabled = enabled
super(DummyBlockNode, self).__init__(**kwargs)
def add_child_block(self, name, parameters=None, position=None): # pragma: no cover
"""Add child block"""
pass
@ -124,9 +118,45 @@ class ParserNodeTest(unittest.TestCase):
"""Dummy placeholder test case for ParserNode interfaces"""
def test_dummy(self):
dummyblock = DummyBlockNode()
dummydirective = DummyDirectiveNode("Name")
dummycomment = DummyCommentNode("Comment", dummyblock, "/some/file")
dummyblock = DummyBlockNode(
name=None,
parameters=(),
ancestor=None,
dirty=False,
filepath="/some/random/path"
)
dummydirective = DummyDirectiveNode(
name="Name",
ancestor=None,
filepath="/another/path"
)
dummycomment = DummyCommentNode(
comment="Comment",
ancestor=dummyblock,
filepath="/some/file"
)
def test_unknown_parameter(self):
params = {
"comment": "x",
"ancestor": None,
"dirty": False,
"filepath": "/tmp",
"unknown": "x"
}
self.assertRaises(TypeError, DummyCommentNode, **params)
params["name"] = "unnamed"
params.pop("comment")
self.assertRaises(TypeError, DummyDirectiveNode, **params)
self.assertRaises(TypeError, DummyBlockNode, **params)
def test_missing_required(self):
params = {
"ancestor": None,
"dirty": False,
"filepath": "/tmp",
}
self.assertRaises(TypeError, DummyCommentNode, **params)
if __name__ == "__main__":