[tor-commits] [stem/master] Behave smartly with context-sensitive config keys

atagar at torproject.org atagar at torproject.org
Wed Jul 4 21:34:20 UTC 2012


commit 9e40210c83c2af32fecb08e4ba4ea71833ce8050
Author: Ravi Chandra Padmala <neenaoffline at 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"])
   





More information about the tor-commits mailing list