diff --git a/certbot-apache/certbot_apache/interfaces.py b/certbot-apache/certbot_apache/interfaces.py index 08a749cd2..4dd3c3732 100644 --- a/certbot-apache/certbot_apache/interfaces.py +++ b/certbot-apache/certbot_apache/interfaces.py @@ -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 diff --git a/certbot-apache/certbot_apache/parsernode_util.py b/certbot-apache/certbot_apache/parsernode_util.py new file mode 100644 index 000000000..fecc635ce --- /dev/null +++ b/certbot-apache/certbot_apache/parsernode_util.py @@ -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 diff --git a/certbot-apache/certbot_apache/tests/parsernode_test.py b/certbot-apache/certbot_apache/tests/parsernode_test.py index 705dc61a1..08d0f4c3b 100644 --- a/certbot-apache/certbot_apache/tests/parsernode_test.py +++ b/certbot-apache/certbot_apache/tests/parsernode_test.py @@ -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__":