[tor-commits] [stem/master] Caching /help responses

atagar at torproject.org atagar at torproject.org
Tue May 6 01:21:13 UTC 2014


commit 48c016d9d65345bf8ad82eb57b8ec950e52ed145
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Apr 19 23:11:32 2014 -0700

    Caching /help responses
    
    Determining our /help output on construction rather than on demand. I'm a
    little on the fencepost about this, but this includes some minor output and
    code improvements so opting for it for the moment.
---
 stem/interpretor/commands.py |  224 +++++++++++++++++++++---------------------
 1 file changed, 111 insertions(+), 113 deletions(-)

diff --git a/stem/interpretor/commands.py b/stem/interpretor/commands.py
index 3a75f6c..f43e0d1 100644
--- a/stem/interpretor/commands.py
+++ b/stem/interpretor/commands.py
@@ -16,146 +16,144 @@ BOLD_OUTPUT_FORMAT = (Color.BLUE, Attr.BOLD)
 ERROR_FORMAT = (Attr.BOLD, Color.RED)
 
 
-class ControlInterpretor(object):
-  """
-  Handles issuing requests and providing nicely formed responses, with support
-  for special irc style subcommands.
+ at uses_settings
+def help_output(controller, config):
   """
+  Provides the output for our /help commands.
 
-  def __init__(self, controller):
-    self.controller = controller
-    self.received_events = []
-
-  def register_event(self, event):
-    """
-    Adds the event to our buffer so it'll be in '/events' output.
-    """
+  :param stem.Controller controller: tor control connection
+  :param stem.util.conf.Config config: interpretor configuration
 
-    self.received_events.append(event)
+  :returns: **dict** mapping arguments to their help output
+  """
 
-  @uses_settings
-  def do_help(self, arg, config):
-    """
-    Performs the '/help' operation, giving usage information for the given
-    argument or a general summary if there wasn't one.
-    """
+  result = {}
+  usage_info = config.get('help.usage', {})
 
-    arg = arg.upper()
-    usage_info = config.get('help.usage', {})
+  general_help = ''
 
-    # If there's multiple arguments then just take the first. This is
-    # particularly likely if they're trying to query a full command (for
-    # instance "/help GETINFO version")
+  for line in msg('help.general').splitlines():
+    cmd_start = line.find(' - ')
 
-    arg = arg.split(' ')[0]
+    if cmd_start != -1:
+      general_help += format(line[:cmd_start], *BOLD_OUTPUT_FORMAT)
+      general_help += format(line[cmd_start:] + '\n', *OUTPUT_FORMAT)
+    else:
+      general_help += format(line + '\n', *BOLD_OUTPUT_FORMAT)
 
-    # strip slash if someone enters an interpretor command (ex. "/help /help")
+  result[''] = general_help
 
-    if arg.startswith('/'):
-      arg = arg[1:]
+  for arg, usage in usage_info.items():
+    description = config.get('help.description.%s' % arg.lower(), '')
 
-    output = ''
+    output = format(usage + '\n', *BOLD_OUTPUT_FORMAT)
 
-    if not arg:
-      # provides the general help with everything bolded except descriptions
+    for line in description.splitlines():
+      output += format('  ' + line + '\n', *OUTPUT_FORMAT)
 
-      for line in msg('help.general').splitlines():
-        cmd_start = line.find(' - ')
+    output += '\n'
 
-        if cmd_start != -1:
-          output += format(line[:cmd_start], *BOLD_OUTPUT_FORMAT)
-          output += format(line[cmd_start:] + '\n', *OUTPUT_FORMAT)
-        else:
-          output += format(line + '\n', *BOLD_OUTPUT_FORMAT)
-    elif arg in usage_info:
-      # Provides information for the tor or interpretor argument. This bolds
-      # the usage information and indents the description after it.
+    if arg == 'GETINFO':
+      results = controller.get_info('info/names', None)
 
-      usage = usage_info[arg]
-      description = config.get('help.description.%s' % arg.lower(), '')
+      if results:
+        for line in results.splitlines():
+          if ' -- ' in line:
+            opt, summary = line.split(' -- ', 1)
 
-      output = format(usage + '\n', *BOLD_OUTPUT_FORMAT)
+            output += format("%-33s" % opt, *BOLD_OUTPUT_FORMAT)
+            output += format(" - %s\n" % summary, *OUTPUT_FORMAT)
+    elif arg == 'GETCONF':
+      results = controller.get_info('config/names', None)
 
-      for line in description.splitlines():
-        output += format('  ' + line + '\n', *OUTPUT_FORMAT)
+      if results:
+        options = [opt.split(' ', 1)[0] for opt in results.splitlines()]
 
-      if arg == 'GETINFO':
-        # if this is the GETINFO option then also list the valid options
+        for i in range(0, len(options), 2):
+          line = ''
 
-        info_options = self.controller.get_info('info/names', None)
+          for entry in options[i:i + 1]:
+            line += '%-42s' % entry
 
-        if info_options:
-          for line in info_options.splitlines():
-            line_match = re.match("^(.+) -- (.+)$", line)
+          output += format(line + '\n', *OUTPUT_FORMAT)
+    elif arg == 'SIGNAL':
+      signal_options = config.get('help.signal.options', {})
 
-            if line_match:
-              opt, description = line_match.groups()
+      for signal, summary in signal_options.items():
+        output += format('%-15s' % signal, *BOLD_OUTPUT_FORMAT)
+        output += format(' - %s\n' % summary, *OUTPUT_FORMAT)
+    elif arg == 'SETEVENTS':
+      results = controller.get_info('events/names', None)
 
-              output += format("%-33s" % opt, *BOLD_OUTPUT_FORMAT)
-              output += format(" - %s\n" % description, *OUTPUT_FORMAT)
-      elif arg == 'GETCONF':
-        # lists all of the configuration options
-        # TODO: integrate tor man page output when stem supports that
+      if results:
+        entries = results.split()
 
-        conf_options = self.controller.get_info('config/names', None)
+        # displays four columns of 20 characters
 
-        if conf_options:
-          conf_entries = [opt.split(' ', 1)[0] for opt in conf_options.split('\n')]
+        for i in range(0, len(entries), 4):
+          line = ''
 
-          # displays two columns of 42 characters
+          for entry in entries[i:i + 4]:
+            line += '%-20s' % entry
 
-          for i in range(0, len(conf_entries), 2):
-            line_entries = conf_entries[i:i + 2]
+          output += format(line + '\n', *OUTPUT_FORMAT)
+    elif arg == 'USEFEATURE':
+      results = controller.get_info('features/names', None)
 
-            line_content = ''
+      if results:
+        output += format(results + '\n', *OUTPUT_FORMAT)
+    elif arg in ('LOADCONF', 'POSTDESCRIPTOR'):
+      # gives a warning that this option isn't yet implemented
+      output += format(msg('msg.multiline_unimplemented_notice') + '\n', *ERROR_FORMAT)
 
-            for entry in line_entries:
-              line_content += '%-42s' % entry
+    result[arg] = output
 
-            output += format(line_content + '\n', *OUTPUT_FORMAT)
+  return result
 
-          output += format("For more information use '/help [CONFIG OPTION]'.", *BOLD_OUTPUT_FORMAT)
-      elif arg == 'SIGNAL':
-        # lists descriptions for all of the signals
 
-        descriptions = config.get('help.signal.options', {})
+class ControlInterpretor(object):
+  """
+  Handles issuing requests and providing nicely formed responses, with support
+  for special irc style subcommands.
+  """
 
-        for signal, description in descriptions.items():
-          output += format('%-15s' % signal, *BOLD_OUTPUT_FORMAT)
-          output += format(' - %s\n' % description, *OUTPUT_FORMAT)
-      elif arg == 'SETEVENTS':
-        # lists all of the event types
+  def __init__(self, controller):
+    self._controller = controller
+    self._received_events = []
+    self._help_output = help_output(controller)
 
-        event_options = self.controller.get_info('events/names', None)
+  def register_event(self, event):
+    """
+    Adds the event to our buffer so it'll be in '/events' output.
+    """
 
-        if event_options:
-          event_entries = event_options.split()
+    self._received_events.append(event)
 
-          # displays four columns of 20 characters
+  @uses_settings
+  def do_help(self, arg, config):
+    """
+    Performs the '/help' operation, giving usage information for the given
+    argument or a general summary if there wasn't one.
+    """
 
-          for i in range(0, len(event_entries), 4):
-            line_entries = event_entries[i:i + 4]
+    arg = arg.upper()
+    usage_info = config.get('help.usage', {})
 
-            line_content = ''
+    # If there's multiple arguments then just take the first. This is
+    # particularly likely if they're trying to query a full command (for
+    # instance "/help GETINFO version")
 
-            for entry in line_entries:
-              line_content += '%-20s' % entry
+    arg = arg.split(' ')[0]
 
-            output += format(line_content + '\n', *OUTPUT_FORMAT)
-      elif arg == 'USEFEATURE':
-        # lists the feature options
+    # strip slash if someone enters an interpretor command (ex. "/help /help")
 
-        feature_options = self.controller.get_info('features/names', None)
+    if arg.startswith('/'):
+      arg = arg[1:]
 
-        if feature_options:
-          output += format(feature_options + '\n', *OUTPUT_FORMAT)
-      elif arg in ('LOADCONF', 'POSTDESCRIPTOR'):
-        # gives a warning that this option isn't yet implemented
-        output += format('\n' + msg('msg.multiline_unimplemented_notice') + '\n', *ERROR_FORMAT)
+    if arg in self._help_output:
+      return self._help_output[arg]
     else:
-      output += format("No help information available for '%s'..." % arg, *ERROR_FORMAT)
-
-    return output
+      return format("No help information available for '%s'..." % arg, *ERROR_FORMAT)
 
   def do_events(self, arg):
     """
@@ -164,7 +162,7 @@ class ControlInterpretor(object):
     all buffered events.
     """
 
-    events = self.received_events
+    events = self._received_events
     event_types = arg.upper().split()
 
     if event_types:
@@ -187,14 +185,14 @@ class ControlInterpretor(object):
     if not arg:
       # uses our fingerprint if we're a relay, otherwise gives an error
 
-      fingerprint = self.controller.get_info('fingerprint', None)
+      fingerprint = self._controller.get_info('fingerprint', None)
 
       if not fingerprint:
         output += format("We aren't a relay, no information to provide", *ERROR_FORMAT)
     elif stem.util.tor_tools.is_valid_fingerprint(arg):
       fingerprint = arg
     elif stem.util.tor_tools.is_valid_nickname(arg):
-      desc = self.controller.get_network_status(arg, None)
+      desc = self._controller.get_network_status(arg, None)
 
       if desc:
         fingerprint = desc.fingerprint
@@ -217,7 +215,7 @@ class ControlInterpretor(object):
 
       matches = {}
 
-      for desc in self.controller.get_network_statuses():
+      for desc in self._controller.get_network_statuses():
         if desc.address == address:
           if not port or desc.or_port == port:
             matches[desc.or_port] = desc.fingerprint
@@ -235,9 +233,9 @@ class ControlInterpretor(object):
       return format("'%s' isn't a fingerprint, nickname, or IP address" % arg, *ERROR_FORMAT)
 
     if fingerprint:
-      micro_desc = self.controller.get_microdescriptor(fingerprint, None)
-      server_desc = self.controller.get_server_descriptor(fingerprint, None)
-      ns_desc = self.controller.get_network_status(fingerprint, None)
+      micro_desc = self._controller.get_microdescriptor(fingerprint, None)
+      server_desc = self._controller.get_server_descriptor(fingerprint, None)
+      ns_desc = self._controller.get_network_status(fingerprint, None)
 
       # We'll mostly rely on the router status entry. Either the server
       # descriptor or microdescriptor will be missing, so we'll treat them as
@@ -246,7 +244,7 @@ class ControlInterpretor(object):
       if not ns_desc:
         return format("Unable to find consensus information for %s" % fingerprint, *ERROR_FORMAT)
 
-      locale = self.controller.get_info('ip-to-country/%s' % ns_desc.address, None)
+      locale = self._controller.get_info('ip-to-country/%s' % ns_desc.address, None)
       locale_label = ' (%s)' % locale if locale else ''
 
       if server_desc:
@@ -307,7 +305,7 @@ class ControlInterpretor(object):
     :raises: **stem.SocketClosed** if the control connection has been severed
     """
 
-    if not self.controller.is_alive():
+    if not self._controller.is_alive():
       raise stem.SocketClosed()
 
     command = command.strip()
@@ -347,7 +345,7 @@ class ControlInterpretor(object):
 
       if cmd == 'GETINFO':
         try:
-          response = self.controller.get_info(arg.split())
+          response = self._controller.get_info(arg.split())
           output = format('\n'.join(response.values()), *OUTPUT_FORMAT)
         except stem.ControllerError as exc:
           output = format(str(exc), *ERROR_FORMAT)
@@ -387,20 +385,20 @@ class ControlInterpretor(object):
 
         try:
           is_reset = cmd == 'RESETCONF'
-          self.controller.set_options(param_list, is_reset)
+          self._controller.set_options(param_list, is_reset)
         except stem.ControllerError as exc:
           output = format(str(exc), *ERROR_FORMAT)
       elif cmd == 'SETEVENTS':
         try:
           # first discontinue listening to prior events
 
-          self.controller.remove_event_listener(self.register_event)
+          self._controller.remove_event_listener(self.register_event)
 
           # attach listeners for the given group of events
 
           if arg:
             events = arg.split()
-            self.controller.add_event_listener(self.register_event, *events)
+            self._controller.add_event_listener(self.register_event, *events)
             output = format('Listing for %s events\n' % ', '.join(events), *OUTPUT_FORMAT)
           else:
             output = format('Disabled event listening\n', *OUTPUT_FORMAT)
@@ -411,7 +409,7 @@ class ControlInterpretor(object):
         output = format(msg('msg.multiline_unimplemented_notice'), *ERROR_FORMAT)
       else:
         try:
-          response = self.controller.msg(command)
+          response = self._controller.msg(command)
 
           if cmd == 'QUIT':
             raise stem.SocketClosed()





More information about the tor-commits mailing list