commit b8959dfe154180018071a07d7ad8eceb68a4c516 Author: Ravi Chandra Padmala neenaoffline@gmail.com Date: Sun Jun 17 18:05:54 2012 +0530
Various modifications to GETCONF parsing
* Change the way entries are stored. Now only stores one value unless the caller explicitly mentions that he want to retrieve multiple values using the 'multiple' argument.
* Stop checking if the received configuration options are the ones that were requested. (The HiddenService options do this)
* Minor documentation fixes. --- stem/control.py | 35 +++++++++++++++------------------ stem/response/__init__.py | 5 ++- stem/response/getconf.py | 40 +++++++++++++++++++------------------- stem/response/getinfo.py | 2 +- stem/socket.py | 16 +++++++------- test/integ/control/controller.py | 12 +++++----- test/unit/response/getconf.py | 15 ++++++------- 7 files changed, 61 insertions(+), 64 deletions(-)
diff --git a/stem/control.py b/stem/control.py index a616b5c..d14d723 100644 --- a/stem/control.py +++ b/stem/control.py @@ -453,7 +453,9 @@ class Controller(BaseController): * dict with the param => response mapping if our param was a list * default if one was provided and our call failed
- :raises: :class:`stem.socket.ControllerError` if the call fails, and we weren't provided a default response + :raises: + :class:`stem.socket.ControllerError` if the call fails, and we weren't provided a default response + :class:`stem.socket.InvalidArguments` if the 'param' requested was invalid """
# TODO: add caching? @@ -530,8 +532,8 @@ class Controller(BaseController): """
return stem.connection.get_protocolinfo(self) - - def get_conf(self, param, default = None): + + def get_conf(self, param, default = UNDEFINED, multiple = False): """ Queries the control socket for the values of given configuration options. If provided a default then that's returned as if the GETCONF option is undefined @@ -540,17 +542,21 @@ class Controller(BaseController):
:param str,list param: GETCONF option or options to be queried :param object default: response if the query fails + :param bool multiple: if True, the value(s) provided are lists of all returned values, + otherwise this just provides the first value
:returns: Response depends upon how we were called as follows...
- * str with the response if our param was a str - * dict with the param => response mapping if our param was a list + * str with the response if our param was a str and multiple was False + * dict with the param (str) => response (str) mapping if our param was a list and multiple was False + * list with the response strings if our param was a str and multiple was True + * dict with the param (str) => response (list) mapping if our param was a list and multiple was True * default if one was provided and our call failed
:raises: :class:`stem.socket.ControllerError` if the call fails, and we weren't provided a default response - :class:`stem.socket.InvalidArguments` if configuration options requested was invalid + :class:`stem.socket.InvalidArguments` if the configuration options requested were invalid """
if isinstance(param, str): @@ -561,23 +567,14 @@ class Controller(BaseController):
try: response = self.msg("GETCONF %s" % " ".join(param)) - stem.response.convert("GETCONF", response) - - # error if we got back different parameters than we requested - requested_params = set(param) - reply_params = set(response.entries.keys()) - - if requested_params != reply_params: - requested_label = ", ".join(requested_params) - reply_label = ", ".join(reply_params) - - raise stem.socket.ProtocolError("GETCONF reply doesn't match the parameters that we requested. Queried '%s' but got '%s'." % (requested_label, reply_label)) + stem.response.convert("GETCONF", response, multiple = multiple)
if is_multiple: return response.entries else: - return response.entries[param[0]] + try: return response.entries[param[0]] + except KeyError: raise stem.socket.InvalidRequest("Received empty string") except stem.socket.ControllerError, exc: - if default is None: raise exc + if default is UNDEFINED: raise exc else: return default
diff --git a/stem/response/__init__.py b/stem/response/__init__.py index 13c65b3..590e4c2 100644 --- a/stem/response/__init__.py +++ b/stem/response/__init__.py @@ -42,7 +42,7 @@ KEY_ARG = re.compile("^(\S+)=") CONTROL_ESCAPES = {r"\": "\", r""": """, r"'": "'", r"\r": "\r", r"\n": "\n", r"\t": "\t"}
-def convert(response_type, message): +def convert(response_type, message, **kwargs): """ Converts a ControlMessage into a particular kind of tor response. This does an in-place conversion of the message from being a ControlMessage to a @@ -57,6 +57,7 @@ def convert(response_type, message):
:param str response_type: type of tor response to convert to :param stem.response.ControlMessage message: message to be converted + :param kwargs: optional keyword arguments to be passed to the parser method
:raises: * :class:`stem.socket.ProtocolError` the message isn't a proper response of that type @@ -84,7 +85,7 @@ def convert(response_type, message): else: raise TypeError("Unsupported response type: %s" % response_type)
message.__class__ = response_class - message._parse_message() + message._parse_message(**kwargs)
class ControlMessage: """ diff --git a/stem/response/getconf.py b/stem/response/getconf.py index fd2256c..9ab15c7 100644 --- a/stem/response/getconf.py +++ b/stem/response/getconf.py @@ -1,14 +1,6 @@ import stem.socket import stem.response
-def _split_line(line): - if line.is_next_mapping(quoted = False): - return line.split("=", 1) # TODO: make this part of the ControlLine? - elif line.is_next_mapping(quoted = True): - return line.pop_mapping(True).items()[0] - else: - return (line.pop(), None) - class GetConfResponse(stem.response.ControlMessage): """ Reply for a GETCONF query. @@ -18,7 +10,12 @@ class GetConfResponse(stem.response.ControlMessage): of strings) """
- def _parse_message(self): + def _parse_message(self, multiple = False): + """ + :param bool multiple: + if True stores each value in a list, otherwise stores only the first + values as a sting + """ # Example: # 250-CookieAuthentication=0 # 250-ControlPort=9100 @@ -27,7 +24,7 @@ class GetConfResponse(stem.response.ControlMessage):
self.entries = {} remaining_lines = list(self) - + if self.content() == [("250", " ", "OK")]: return
if not self.is_ok(): @@ -35,7 +32,7 @@ class GetConfResponse(stem.response.ControlMessage): for code, _, line in self.content(): if code == "552" and line.startswith("Unrecognized configuration key "") and line.endswith("""): unrecognized_keywords.append(line[32:-1]) - + if unrecognized_keywords: raise stem.socket.InvalidArguments("552", "GETCONF request contained unrecognized keywords: %s\n" \ % ', '.join(unrecognized_keywords), unrecognized_keywords) @@ -44,15 +41,18 @@ class GetConfResponse(stem.response.ControlMessage):
while remaining_lines: line = remaining_lines.pop(0) - - key, value = _split_line(line) + + if line.is_next_mapping(quoted = False): + key, value = line.split("=", 1) # TODO: make this part of the ControlLine? + elif line.is_next_mapping(quoted = True): + key, value = line.pop_mapping(True).items()[0] + else: + key, value = (line.pop(), None) + entry = self.entries.get(key, None) - - if type(entry) == str and entry != value: - self.entries[key] = [entry] - self.entries[key].append(value) - elif type(entry) == list and not value in entry: - self.entries[key].append(value) + + if multiple: + self.entries.setdefault(key, []).append(value) else: - self.entries[key] = value + self.entries.setdefault(key, value)
diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py index f467769..a2cce57 100644 --- a/stem/response/getinfo.py +++ b/stem/response/getinfo.py @@ -29,7 +29,7 @@ class GetInfoResponse(stem.response.ControlMessage): for code, _, line in self.content(): if code == '552' and line.startswith("Unrecognized key "") and line.endswith("""): unrecognized_keywords.append(line[18:-1]) - + if unrecognized_keywords: raise stem.socket.InvalidArguments("552", "GETINFO request contained unrecognized keywords: %s\n" \ % ', '.join(unrecognized_keywords), unrecognized_keywords) diff --git a/stem/socket.py b/stem/socket.py index 9e1f5b7..6972929 100644 --- a/stem/socket.py +++ b/stem/socket.py @@ -29,7 +29,7 @@ as instances of the :class:`stem.response.ControlMessage` class. ControllerError - Base exception raised when using the controller. |- ProtocolError - Malformed socket data. |- InvalidRequest - Invalid request. - +- InvalidArguments - Invalid request parameters. + | +- InvalidArguments - Invalid request parameters. +- SocketError - Communication with the socket failed. +- SocketClosed - Socket has been shut down. """ @@ -553,7 +553,7 @@ class ProtocolError(ControllerError): class InvalidRequest(ControllerError): """ Base Exception class for invalid requests - + :var str code: The error code returned by Tor (if applicable) :var str message: The error message returned by Tor (if applicable) or a human readable error message @@ -562,11 +562,11 @@ class InvalidRequest(ControllerError): def __init__(self, code = None, message = None): """ Initializes an InvalidRequest object. - + :param str code: The error code returned by Tor (if applicable) :param str message: The error message returned by Tor (if applicable) or a human readable error message - + :returns: object of InvalidRequest class """
@@ -576,22 +576,22 @@ class InvalidRequest(ControllerError): class InvalidArguments(InvalidRequest): """ Exception class for invalid requests which contain invalid arguments. - + :var str code: The error code returned by Tor (if applicable) :var str message: The error message returned by Tor (if applicable) or a human readable error message :var list arguments: a list of arguments which were invalid """ - + def __init__(self, code = None, message = None, arguments = None): """ Initializes an InvalidArguments object. - + :param str code: The error code returned by Tor (if applicable) :param str message: The error message returned by Tor (if applicable) or a human readable error message :param list arguments: a list of arguments which were invalid - + :returns: object of InvalidArguments class """
diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py index 2441eac..db7941c 100644 --- a/test/integ/control/controller.py +++ b/test/integ/control/controller.py @@ -147,18 +147,18 @@ class TestController(unittest.TestCase):
socket = runner.get_tor_socket() if isinstance(socket, stem.socket.ControlPort): - socket = str(socket.get_port()) + connection_value = str(socket.get_port()) config_key = "ControlPort" elif isinstance(socket, stem.socket.ControlSocketFile): - socket = str(socket.get_socket_path()) + connection_value = str(socket.get_socket_path()) config_key = "ControlSocket"
- self.assertEqual(socket, controller.get_conf(config_key)) - self.assertEqual(socket, controller.get_conf(config_key, "la-di-dah")) + self.assertEqual(connection_value, controller.get_conf(config_key)) + self.assertEqual(connection_value, controller.get_conf(config_key, "la-di-dah"))
# succeessful batch query
- expected = {config_key: socket} + expected = {config_key: connection_value} self.assertEqual(expected, controller.get_conf([config_key])) self.assertEqual(expected, controller.get_conf([config_key], "la-di-dah"))
@@ -173,7 +173,7 @@ class TestController(unittest.TestCase):
# empty input
- self.assertRaises(stem.socket.ControllerError, controller.get_conf, "") + self.assertRaises(stem.socket.InvalidRequest, controller.get_conf, "") self.assertEqual("la-di-dah", controller.get_conf("", "la-di-dah"))
self.assertEqual({}, controller.get_conf([])) diff --git a/test/unit/response/getconf.py b/test/unit/response/getconf.py index a9f1cc8..e0020fb 100644 --- a/test/unit/response/getconf.py +++ b/test/unit/response/getconf.py @@ -24,12 +24,11 @@ MULTIVALUE_RESPONSE = """\ 250-ControlPort=9100 250-ExitPolicy=accept 34.3.4.5 250-ExitPolicy=accept 3.4.53.3 -250-ExitPolicy=reject 23.245.54.3 -250-ExitPolicy=accept 34.3.4.5 250-ExitPolicy=accept 3.4.53.3 250 ExitPolicy=reject 23.245.54.3"""
-UNRECOGNIZED_KEY_RESPONSE = "552 Unrecognized configuration key "yellowbrickroad"" +UNRECOGNIZED_KEY_RESPONSE = '''552-Unrecognized configuration key "brickroad" +552 Unrecognized configuration key "submarine"'''
INVALID_RESPONSE = """\ 123-FOO @@ -82,11 +81,11 @@ class TestGetConfResponse(unittest.TestCase): """
control_message = mocking.get_message(MULTIVALUE_RESPONSE) - stem.response.convert("GETCONF", control_message) + stem.response.convert("GETCONF", control_message, multiple = True)
expected = { - "ControlPort": "9100", - "ExitPolicy": ["accept 34.3.4.5", "accept 3.4.53.3", "reject 23.245.54.3"] + "ControlPort": ["9100"], + "ExitPolicy": ["accept 34.3.4.5", "accept 3.4.53.3", "accept 3.4.53.3", "reject 23.245.54.3"] }
self.assertEqual(expected, control_message.entries) @@ -100,9 +99,9 @@ class TestGetConfResponse(unittest.TestCase): self.assertRaises(stem.socket.InvalidArguments, stem.response.convert, "GETCONF", control_message)
try: - stem.response.convert("GETCONF", control_message) + stem.response.convert("GETCONF", control_message, multiple = True) except stem.socket.InvalidArguments, exc: - self.assertEqual(exc.arguments, ["yellowbrickroad"]) + self.assertEqual(exc.arguments, ["brickroad", "submarine"])
def test_invalid_content(self): """