]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Refactor tornado.options to make it testable.
authorBen Darnell <ben@bendarnell.com>
Mon, 7 May 2012 05:46:53 +0000 (22:46 -0700)
committerBen Darnell <ben@bendarnell.com>
Mon, 7 May 2012 05:46:53 +0000 (22:46 -0700)
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.

tornado/options.py
tornado/test/options_test.py [new file with mode: 0644]
tornado/test/runtests.py
website/sphinx/options.rst

index 4e23684f19f93357bb5faa6e424354fd50d59b63..1763e8d26b658fd49ace4f5ad343a350d456df1a 100644 (file)
@@ -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 (file)
index 0000000..dabc2c9
--- /dev/null
@@ -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)
index 91f8b25dae4484f45da6b5d23d79e4ff2f4f6458..fdedd4382e89e5f2823c48a76d9c38585290f08b 100755 (executable)
@@ -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',
index a201bc2c8a432c54044fd700e783c6a91fc61a74..026b37864a6d502950db24c29e3c9881332ad0ab 100644 (file)
@@ -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