commit 9e40210c83c2af32fecb08e4ba4ea71833ce8050 Author: Ravi Chandra Padmala neenaoffline@gmail.com Date: Thu Jun 21 03:39:34 2012 +0530
Behave smartly with context-sensitive config keys --- stem/control.py | 108 +++++++++++++++++++++++++++++--------- stem/response/getconf.py | 19 ++----- test/integ/control/controller.py | 25 +++++---- test/unit/response/getconf.py | 14 +++--- 4 files changed, 108 insertions(+), 58 deletions(-)
diff --git a/stem/control.py b/stem/control.py index 5ce6780..45e4f10 100644 --- a/stem/control.py +++ b/stem/control.py @@ -404,6 +404,14 @@ class Controller(BaseController): BaseController and provides a more user friendly API for library users. """
+ _mapped_config_keys = { + "HiddenServiceDir": "HiddenServiceOptions", + "HiddenServicePort": "HiddenServiceOptions", + "HiddenServiceVersion": "HiddenServiceOptions", + "HiddenServiceAuthorizeClient": "HiddenServiceOptions", + "HiddenServiceOptions": "HiddenServiceOptions" + } + def from_port(control_addr = "127.0.0.1", control_port = 9051): """ Constructs a ControlPort based Controller. @@ -535,12 +543,12 @@ class Controller(BaseController):
def get_conf(self, param, default = UNDEFINED, multiple = False): """ - Queries the control socket for the values of given configuration options. If + Queries the control socket for the value of a given configuration option. If provided a default then that's returned as if the GETCONF option is undefined or if the call fails for any reason (invalid configuration option, error response, control port closed, initiated, etc).
- :param str,list param: GETCONF option or options to be queried + :param str param: GETCONF option 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 @@ -548,39 +556,89 @@ class Controller(BaseController): :returns: Response depends upon how we were called as follows...
- * 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 + * str with the response if multiple was False + * list with the response strings 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 the configuration options requested were invalid + :class:`stem.socket.InvalidArguments` if the configuration option requested was invalid + """ + + try: + if param == "": raise stem.socket.InvalidRequest("Received empty parameter") + + # automagically change the requested parameter if it's context sensitive + # and cannot be returned on it's own. + if param.lower() in self._mapped_config_keys.keys(): + return self.get_conf_map(self._mapped_config_keys[param], default, multiple)[param] + + response = self.msg("GETCONF %s" % param) + stem.response.convert("GETCONF", response) + + # error if we got back different parameters than we requested + if response.entries.keys()[0].lower() != param.lower(): + raise stem.socket.ProtocolError("GETCONF reply doesn't match the parameters that we requested. Queried '%s' but got '%s'." % (param, response.entries.keys()[0])) + + if not multiple: + return response.entries[param][0] + return response.entries[param] + + except stem.socket.ControllerError, exc: + if default is UNDEFINED: raise exc + else: return default + + def get_conf_map(self, param, default = UNDEFINED, multiple = False): + """ + Queries the control socket for the values of given configuration options and + provides a mapping of the keys to the values. If provided a default then + that's returned as if the GETCONF option is undefined or if the call fails + for any reason (invalid configuration option, error response, control port + closed, initiated, etc). + + :param str,list param: GETCONF option(s) 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... + + * dict with param (str) => response mappings (str) if multiple was False + * dict with param (str) => response mappings (list) if multiple was True + * dict with param (str) => default mappings if a default value 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 the configuration option requested was invalid """
if isinstance(param, str): - is_multiple = False param = [param] - else: - is_multiple = True
try: - response = self.msg("GETCONF %s" % " ".join(param)) - stem.response.convert("GETCONF", response, multiple = multiple) + if param == [""] or param == []: + raise stem.socket.InvalidRequest("Received empty parameter")
- if is_multiple: - return response.entries - else: - try: return response.entries[param[0]] - except KeyError: raise stem.socket.InvalidRequest("Received empty string") + response = self.msg("GETCONF %s" % ' '.join(param)) + stem.response.convert("GETCONF", response) + + requested_params = set(map(lambda x: x.lower(), param)) + reply_params = set(map(lambda x: x.lower(), response.entries.keys())) + + # if none of the requested parameters are context sensitive and if the + # parameters received don't match the parameters requested + if not set(self._mapped_config_keys.values()) & requested_params and 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)) + + if not multiple: + return dict([(entry[0], entry[1][0]) for entry in response.entries.items()]) + return response.entries + except stem.socket.ControllerError, exc: - if default is UNDEFINED: raise exc - elif is_multiple: - if default != UNDEFINED: - return dict([(p, default) for p in param]) - else: - return dict([(p, None) for p in param]) - else: - return default + if default != UNDEFINED: return dict([(p, default) for p in param]) + else: raise exc
diff --git a/stem/response/getconf.py b/stem/response/getconf.py index 9ab15c7..1527e39 100644 --- a/stem/response/getconf.py +++ b/stem/response/getconf.py @@ -5,17 +5,10 @@ class GetConfResponse(stem.response.ControlMessage): """ Reply for a GETCONF query.
- :var dict entries: - mapping between the queried options (string) and their values (string/list - of strings) + :var dict entries: mapping between the queried options (string) and their values (list of strings) """
- 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 - """ + def _parse_message(self): # Example: # 250-CookieAuthentication=0 # 250-ControlPort=9100 @@ -34,8 +27,9 @@ class GetConfResponse(stem.response.ControlMessage): unrecognized_keywords.append(line[32:-1])
if unrecognized_keywords: - raise stem.socket.InvalidArguments("552", "GETCONF request contained unrecognized keywords: %s\n" \ + exc = stem.socket.InvalidArguments("552", "GETCONF request contained unrecognized keywords: %s" \ % ', '.join(unrecognized_keywords), unrecognized_keywords) + raise exc else: raise stem.socket.ProtocolError("GETCONF response contained a non-OK status code:\n%s" % self)
@@ -51,8 +45,5 @@ class GetConfResponse(stem.response.ControlMessage):
entry = self.entries.get(key, None)
- if multiple: - self.entries.setdefault(key, []).append(value) - else: - self.entries.setdefault(key, value) + self.entries.setdefault(key, []).append(value)
diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py index 38f6f10..f9db10e 100644 --- a/test/integ/control/controller.py +++ b/test/integ/control/controller.py @@ -159,24 +159,23 @@ class TestController(unittest.TestCase): # succeessful batch query
expected = {config_key: connection_value} - self.assertEqual(expected, controller.get_conf([config_key])) - self.assertEqual(expected, controller.get_conf([config_key], "la-di-dah")) + self.assertEqual(expected, controller.get_conf_map([config_key])) + self.assertEqual(expected, controller.get_conf_map([config_key], "la-di-dah"))
getconf_params = set(["ControlPort", "DirPort", "DataDirectory"]) - self.assertEqual(getconf_params, set(controller.get_conf(["ControlPort", + self.assertEqual(getconf_params, set(controller.get_conf_map(["ControlPort", "DirPort", "DataDirectory"])))
# non-existant option(s)
- self.assertRaises(stem.socket.InvalidRequest, controller.get_conf, "blarg") - self.assertEqual("la-di-dah", controller.get_conf("blarg", "la-di-dah")) - self.assertRaises(stem.socket.InvalidRequest, controller.get_conf, "blarg") + self.assertRaises(stem.socket.InvalidArguments, controller.get_conf, "blarg") self.assertEqual("la-di-dah", controller.get_conf("blarg", "la-di-dah")) + self.assertRaises(stem.socket.InvalidArguments, controller.get_conf_map, "blarg") + self.assertEqual({"blarg": "la-di-dah"}, controller.get_conf_map("blarg", "la-di-dah"))
- self.assertRaises(stem.socket.InvalidRequest, controller.get_conf, - ["blarg", "huadf"], multiple = True) + self.assertRaises(stem.socket.InvalidRequest, controller.get_conf_map, ["blarg", "huadf"], multiple = True) self.assertEqual({"erfusdj": "la-di-dah", "afiafj": "la-di-dah"}, - controller.get_conf(["erfusdj", "afiafj"], "la-di-dah", multiple = True)) + controller.get_conf_map(["erfusdj", "afiafj"], "la-di-dah", multiple = True))
# multivalue configuration keys
@@ -187,8 +186,10 @@ class TestController(unittest.TestCase): # empty input
self.assertRaises(stem.socket.InvalidRequest, controller.get_conf, "") - self.assertEqual("la-di-dah", controller.get_conf("", "la-di-dah")) + self.assertRaises(stem.socket.InvalidRequest, controller.get_conf_map, []) + self.assertRaises(stem.socket.InvalidRequest, controller.get_conf_map, "")
- self.assertEqual({}, controller.get_conf([])) - self.assertEqual({}, controller.get_conf([], {})) + self.assertEqual("la-di-dah", controller.get_conf("", "la-di-dah")) + self.assertEqual({"": "la-di-dah"}, controller.get_conf_map("", "la-di-dah")) + self.assertEqual({}, controller.get_conf_map([], "la-di-dah"))
diff --git a/test/unit/response/getconf.py b/test/unit/response/getconf.py index e0020fb..58e80d6 100644 --- a/test/unit/response/getconf.py +++ b/test/unit/response/getconf.py @@ -56,7 +56,7 @@ class TestGetConfResponse(unittest.TestCase):
control_message = mocking.get_message(SINGLE_RESPONSE) stem.response.convert("GETCONF", control_message) - self.assertEqual({"DataDirectory": "/home/neena/.tor"}, control_message.entries) + self.assertEqual({"DataDirectory": ["/home/neena/.tor"]}, control_message.entries)
def test_batch_response(self): """ @@ -67,10 +67,10 @@ class TestGetConfResponse(unittest.TestCase): stem.response.convert("GETCONF", control_message)
expected = { - "CookieAuthentication": "0", - "ControlPort": "9100", - "DataDirectory": "/tmp/fake dir", - "DirPort": None, + "CookieAuthentication": ["0"], + "ControlPort": ["9100"], + "DataDirectory": ["/tmp/fake dir"], + "DirPort": [None], }
self.assertEqual(expected, control_message.entries) @@ -81,7 +81,7 @@ class TestGetConfResponse(unittest.TestCase): """
control_message = mocking.get_message(MULTIVALUE_RESPONSE) - stem.response.convert("GETCONF", control_message, multiple = True) + stem.response.convert("GETCONF", control_message)
expected = { "ControlPort": ["9100"], @@ -99,7 +99,7 @@ class TestGetConfResponse(unittest.TestCase): self.assertRaises(stem.socket.InvalidArguments, stem.response.convert, "GETCONF", control_message)
try: - stem.response.convert("GETCONF", control_message, multiple = True) + stem.response.convert("GETCONF", control_message) except stem.socket.InvalidArguments, exc: self.assertEqual(exc.arguments, ["brickroad", "submarine"])