commit 48c016d9d65345bf8ad82eb57b8ec950e52ed145
Author: Damian Johnson <atagar(a)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.
+@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()