diff --git a/tornado/options.py b/tornado/options.py index 4e23684f..1763e8d2 100644 --- a/tornado/options.py +++ b/tornado/options.py @@ -68,138 +68,17 @@ except ImportError: curses = None -def define(name, default=None, type=None, help=None, metavar=None, - multiple=False, group=None): - """Defines a new command line option. - - If type is given (one of str, float, int, datetime, or timedelta) - or can be inferred from the default, we parse the command line - arguments based on the given type. If multiple is True, we accept - comma-separated values, and the option value is always a list. - - For multi-value integers, we also accept the syntax x:y, which - turns into range(x, y) - very useful for long integer ranges. - - help and metavar are used to construct the automatically generated - command line help string. The help message is formatted like:: - - --name=METAVAR help string - - group is used to group the defined options in logical groups. By default, - command line options are grouped by the defined file. - - Command line option names must be unique globally. They can be parsed - from the command line with parse_command_line() or parsed from a - config file with parse_config_file. - """ - if name in options: - raise Error("Option %r already defined in %s", name, - options[name].file_name) - frame = sys._getframe(0) - options_file = frame.f_code.co_filename - file_name = frame.f_back.f_code.co_filename - if file_name == options_file: - file_name = "" - if type is None: - if not multiple and default is not None: - type = default.__class__ - else: - type = str - if group: - group_name = group - else: - group_name = file_name - options[name] = _Option(name, file_name=file_name, default=default, - type=type, help=help, metavar=metavar, - multiple=multiple, group_name=group_name) - - -def parse_command_line(args=None): - """Parses all options given on the command line. - - We return all command line arguments that are not options as a list. - """ - if args is None: - args = sys.argv - remaining = [] - for i in xrange(1, len(args)): - # All things after the last option are command line arguments - if not args[i].startswith("-"): - remaining = args[i:] - break - if args[i] == "--": - remaining = args[i + 1:] - break - arg = args[i].lstrip("-") - name, equals, value = arg.partition("=") - name = name.replace('-', '_') - if not name in options: - print_help() - raise Error('Unrecognized command line option: %r' % name) - option = options[name] - if not equals: - if option.type == bool: - value = "true" - else: - raise Error('Option %r requires a value' % name) - option.parse(value) - if options.help: - print_help() - sys.exit(0) - - # Set up log level and pretty console logging by default - if options.logging != 'none': - logging.getLogger().setLevel(getattr(logging, options.logging.upper())) - enable_pretty_logging() - - return remaining - - -def parse_config_file(path): - """Parses and loads the Python config file at the given path.""" - config = {} - execfile(path, config, config) - for name in config: - if name in options: - options[name].set(config[name]) - - -def print_help(file=sys.stdout): - """Prints all the command line options to stdout.""" - print >> file, "Usage: %s [OPTIONS]" % sys.argv[0] - print >> file, "\nOptions:\n" - by_group = {} - for option in options.itervalues(): - by_group.setdefault(option.group_name, []).append(option) - - for filename, o in sorted(by_group.items()): - if filename: - print >> file, "\n%s options:\n" % os.path.normpath(filename) - o.sort(key=lambda option: option.name) - for option in o: - prefix = option.name - if option.metavar: - prefix += "=" + option.metavar - description = option.help or "" - if option.default is not None and option.default != '': - description += " (default %s)" % option.default - lines = textwrap.wrap(description, 79 - 35) - if len(prefix) > 30 or len(lines) == 0: - lines.insert(0, '') - print >> file, " --%-30s %s" % (prefix, lines[0]) - for line in lines[1:]: - print >> file, "%-34s %s" % (' ', line) - print >> file +class Error(Exception): + """Exception raised by errors in the options module.""" + pass class _Options(dict): - """Our global program options, a dictionary with object-like access.""" - @classmethod - def instance(cls): - if not hasattr(cls, "_instance"): - cls._instance = cls() - return cls._instance + """A collection of options, a dictionary with object-like access. + Normally accessed via static functions in the `tornado.options` module, + which reference a global instance. + """ def __getattr__(self, name): if isinstance(self.get(name), _Option): return self[name].value() @@ -210,6 +89,99 @@ class _Options(dict): return self[name].set(value) raise AttributeError("Unrecognized option %r" % name) + def define(self, name, default=None, type=None, help=None, metavar=None, + multiple=False, group=None): + if name in self: + raise Error("Option %r already defined in %s", name, + self[name].file_name) + frame = sys._getframe(0) + options_file = frame.f_code.co_filename + file_name = frame.f_back.f_code.co_filename + if file_name == options_file: + file_name = "" + if type is None: + if not multiple and default is not None: + type = default.__class__ + else: + type = str + if group: + group_name = group + else: + group_name = file_name + self[name] = _Option(name, file_name=file_name, default=default, + type=type, help=help, metavar=metavar, + multiple=multiple, group_name=group_name) + + def parse_command_line(self, args=None): + if args is None: + args = sys.argv + remaining = [] + for i in xrange(1, len(args)): + # All things after the last option are command line arguments + if not args[i].startswith("-"): + remaining = args[i:] + break + if args[i] == "--": + remaining = args[i + 1:] + break + arg = args[i].lstrip("-") + name, equals, value = arg.partition("=") + name = name.replace('-', '_') + if not name in self: + print_help() + raise Error('Unrecognized command line option: %r' % name) + option = self[name] + if not equals: + if option.type == bool: + value = "true" + else: + raise Error('Option %r requires a value' % name) + option.parse(value) + if self.help: + print_help() + sys.exit(0) + + # Set up log level and pretty console logging by default + if self.logging != 'none': + logging.getLogger().setLevel(getattr(logging, self.logging.upper())) + enable_pretty_logging() + + return remaining + + def parse_config_file(self, path): + config = {} + execfile(path, config, config) + for name in config: + if name in self: + self[name].set(config[name]) + + def print_help(self, file=sys.stdout): + """Prints all the command line options to stdout.""" + print >> file, "Usage: %s [OPTIONS]" % sys.argv[0] + print >> file, "\nOptions:\n" + by_group = {} + for option in self.itervalues(): + by_group.setdefault(option.group_name, []).append(option) + + for filename, o in sorted(by_group.items()): + if filename: + print >> file, "\n%s options:\n" % os.path.normpath(filename) + o.sort(key=lambda option: option.name) + for option in o: + prefix = option.name + if option.metavar: + prefix += "=" + option.metavar + description = option.help or "" + if option.default is not None and option.default != '': + description += " (default %s)" % option.default + lines = textwrap.wrap(description, 79 - 35) + if len(prefix) > 30 or len(lines) == 0: + lines.insert(0, '') + print >> file, " --%-30s %s" % (prefix, lines[0]) + for line in lines[1:]: + print >> file, "%-34s %s" % (' ', line) + print >> file + class _Option(object): def __init__(self, name, default=None, type=basestring, help=None, metavar=None, @@ -331,12 +303,63 @@ class _Option(object): return _unicode(value) -class Error(Exception): - """Exception raised by errors in the options module.""" - pass + +options = _Options() +"""Global options dictionary. + +Supports both attribute-style and dict-style access. +""" -def enable_pretty_logging(): +def define(name, default=None, type=None, help=None, metavar=None, + multiple=False, group=None): + """Defines a new command line option. + + If type is given (one of str, float, int, datetime, or timedelta) + or can be inferred from the default, we parse the command line + arguments based on the given type. If multiple is True, we accept + comma-separated values, and the option value is always a list. + + For multi-value integers, we also accept the syntax x:y, which + turns into range(x, y) - very useful for long integer ranges. + + help and metavar are used to construct the automatically generated + command line help string. The help message is formatted like:: + + --name=METAVAR help string + + group is used to group the defined options in logical groups. By default, + command line options are grouped by the defined file. + + Command line option names must be unique globally. They can be parsed + from the command line with parse_command_line() or parsed from a + config file with parse_config_file. + """ + return options.define(name, default=default, type=type, help=help, + metavar=metavar, multiple=multiple, group=group) + + +def parse_command_line(args=None): + """Parses all options given on the command line (defaults to sys.argv). + + Note that args[0] is ignored since it is the program name in sys.argv. + + We return a list of all arguments that are not parsed as options. + """ + return options.parse_command_line(args) + + +def parse_config_file(path): + """Parses and loads the Python config file at the given path.""" + return options.parse_config_file(path) + + +def print_help(file=sys.stdout): + """Prints all the command line options to stdout.""" + return options.print_help(file) + + +def enable_pretty_logging(options=options): """Turns on formatted logging output as configured. This is called automatically by `parse_command_line`. @@ -415,9 +438,6 @@ class _LogFormatter(logging.Formatter): return formatted.replace("\n", "\n ") -options = _Options.instance() - - # Default options define("help", type=bool, help="show this help information") define("logging", default="info", diff --git a/tornado/test/options_test.py b/tornado/test/options_test.py new file mode 100644 index 00000000..dabc2c9c --- /dev/null +++ b/tornado/test/options_test.py @@ -0,0 +1,17 @@ +import unittest + +from tornado.options import _Options + +class OptionsTest(unittest.TestCase): + def setUp(self): + self.options = _Options() + define = self.options.define + # these are currently required + define("logging", default="none") + define("help", default=False) + + define("port", default=80) + + def test_parse_command_line(self): + self.options.parse_command_line(["main.py", "--port=443"]) + self.assertEqual(self.options.port, 443) diff --git a/tornado/test/runtests.py b/tornado/test/runtests.py index 91f8b25d..fdedd438 100755 --- a/tornado/test/runtests.py +++ b/tornado/test/runtests.py @@ -17,6 +17,7 @@ TEST_MODULES = [ 'tornado.test.import_test', 'tornado.test.ioloop_test', 'tornado.test.iostream_test', + 'tornado.test.options_test', 'tornado.test.process_test', 'tornado.test.simple_httpclient_test', 'tornado.test.stack_context_test', diff --git a/website/sphinx/options.rst b/website/sphinx/options.rst index a201bc2c..026b3786 100644 --- a/website/sphinx/options.rst +++ b/website/sphinx/options.rst @@ -2,4 +2,16 @@ ============================================ .. automodule:: tornado.options - :members: + + .. autofunction:: define + + .. py:data:: options + + Global options dictionary. Supports both attribute-style and + dict-style access. + + .. autofunction:: parse_command_line + .. autofunction:: parse_config_file + .. autofunction:: print_help(file=sys.stdout) + .. autofunction:: enable_pretty_logging() + .. autoexception:: Error