Refactor tornado.options to make it testable.
Now most code is in methods of the _Options class, and it is possible to create isolated instances of that class. The test is pretty rudimentary, but it's a start.
This commit is contained in:
parent
625c6e052f
commit
a548ab670b
|
@ -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",
|
||||
|
|
|
@ -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)
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue