options.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Copyright 2009 Facebook
00004 #
00005 # Licensed under the Apache License, Version 2.0 (the "License"); you may
00006 # not use this file except in compliance with the License. You may obtain
00007 # a copy of the License at
00008 #
00009 #     http://www.apache.org/licenses/LICENSE-2.0
00010 #
00011 # Unless required by applicable law or agreed to in writing, software
00012 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
00013 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
00014 # License for the specific language governing permissions and limitations
00015 # under the License.
00016 
00017 """A command line parsing module that lets modules define their own options.
00018 
00019 Each module defines its own options, e.g.::
00020 
00021     from tornado.options import define, options
00022 
00023     define("mysql_host", default="127.0.0.1:3306", help="Main user DB")
00024     define("memcache_hosts", default="127.0.0.1:11011", multiple=True,
00025            help="Main user memcache servers")
00026 
00027     def connect():
00028         db = database.Connection(options.mysql_host)
00029         ...
00030 
00031 The main() method of your application does not need to be aware of all of
00032 the options used throughout your program; they are all automatically loaded
00033 when the modules are loaded. Your main() method can parse the command line
00034 or parse a config file with::
00035 
00036     import tornado.options
00037     tornado.options.parse_config_file("/etc/server.conf")
00038     tornado.options.parse_command_line()
00039 
00040 Command line formats are what you would expect ("--myoption=myvalue").
00041 Config files are just Python files. Global names become options, e.g.::
00042 
00043     myoption = "myvalue"
00044     myotheroption = "myothervalue"
00045 
00046 We support datetimes, timedeltas, ints, and floats (just pass a 'type'
00047 kwarg to define). We also accept multi-value options. See the documentation
00048 for define() below.
00049 """
00050 
00051 from __future__ import absolute_import, division, with_statement
00052 
00053 import datetime
00054 import logging
00055 import logging.handlers
00056 import re
00057 import sys
00058 import os
00059 import time
00060 import textwrap
00061 
00062 from tornado.escape import _unicode
00063 
00064 # For pretty log messages, if available
00065 try:
00066     import curses
00067 except ImportError:
00068     curses = None
00069 
00070 
00071 class Error(Exception):
00072     """Exception raised by errors in the options module."""
00073     pass
00074 
00075 
00076 class _Options(dict):
00077     """A collection of options, a dictionary with object-like access.
00078 
00079     Normally accessed via static functions in the `tornado.options` module,
00080     which reference a global instance.
00081     """
00082     def __getattr__(self, name):
00083         if isinstance(self.get(name), _Option):
00084             return self[name].value()
00085         raise AttributeError("Unrecognized option %r" % name)
00086 
00087     def __setattr__(self, name, value):
00088         if isinstance(self.get(name), _Option):
00089             return self[name].set(value)
00090         raise AttributeError("Unrecognized option %r" % name)
00091 
00092     def define(self, name, default=None, type=None, help=None, metavar=None,
00093                multiple=False, group=None):
00094         if name in self:
00095             raise Error("Option %r already defined in %s", name,
00096                         self[name].file_name)
00097         frame = sys._getframe(0)
00098         options_file = frame.f_code.co_filename
00099         file_name = frame.f_back.f_code.co_filename
00100         if file_name == options_file:
00101             file_name = ""
00102         if type is None:
00103             if not multiple and default is not None:
00104                 type = default.__class__
00105             else:
00106                 type = str
00107         if group:
00108             group_name = group
00109         else:
00110             group_name = file_name
00111         self[name] = _Option(name, file_name=file_name, default=default,
00112                              type=type, help=help, metavar=metavar,
00113                              multiple=multiple, group_name=group_name)
00114 
00115     def parse_command_line(self, args=None):
00116         if args is None:
00117             args = sys.argv
00118         remaining = []
00119         for i in xrange(1, len(args)):
00120             # All things after the last option are command line arguments
00121             if not args[i].startswith("-"):
00122                 remaining = args[i:]
00123                 break
00124             if args[i] == "--":
00125                 remaining = args[i + 1:]
00126                 break
00127             arg = args[i].lstrip("-")
00128             name, equals, value = arg.partition("=")
00129             name = name.replace('-', '_')
00130             if not name in self:
00131                 print_help()
00132                 raise Error('Unrecognized command line option: %r' % name)
00133             option = self[name]
00134             if not equals:
00135                 if option.type == bool:
00136                     value = "true"
00137                 else:
00138                     raise Error('Option %r requires a value' % name)
00139             option.parse(value)
00140         if self.help:
00141             print_help()
00142             sys.exit(0)
00143 
00144         # Set up log level and pretty console logging by default
00145         if self.logging != 'none':
00146             logging.getLogger().setLevel(getattr(logging, self.logging.upper()))
00147             enable_pretty_logging()
00148 
00149         return remaining
00150 
00151     def parse_config_file(self, path):
00152         config = {}
00153         execfile(path, config, config)
00154         for name in config:
00155             if name in self:
00156                 self[name].set(config[name])
00157 
00158     def print_help(self, file=sys.stdout):
00159         """Prints all the command line options to stdout."""
00160         print >> file, "Usage: %s [OPTIONS]" % sys.argv[0]
00161         print >> file, "\nOptions:\n"
00162         by_group = {}
00163         for option in self.itervalues():
00164             by_group.setdefault(option.group_name, []).append(option)
00165 
00166         for filename, o in sorted(by_group.items()):
00167             if filename:
00168                 print >> file, "\n%s options:\n" % os.path.normpath(filename)
00169             o.sort(key=lambda option: option.name)
00170             for option in o:
00171                 prefix = option.name
00172                 if option.metavar:
00173                     prefix += "=" + option.metavar
00174                 description = option.help or ""
00175                 if option.default is not None and option.default != '':
00176                     description += " (default %s)" % option.default
00177                 lines = textwrap.wrap(description, 79 - 35)
00178                 if len(prefix) > 30 or len(lines) == 0:
00179                     lines.insert(0, '')
00180                 print >> file, "  --%-30s %s" % (prefix, lines[0])
00181                 for line in lines[1:]:
00182                     print >> file, "%-34s %s" % (' ', line)
00183         print >> file
00184 
00185 
00186 class _Option(object):
00187     def __init__(self, name, default=None, type=basestring, help=None, metavar=None,
00188                  multiple=False, file_name=None, group_name=None):
00189         if default is None and multiple:
00190             default = []
00191         self.name = name
00192         self.type = type
00193         self.help = help
00194         self.metavar = metavar
00195         self.multiple = multiple
00196         self.file_name = file_name
00197         self.group_name = group_name
00198         self.default = default
00199         self._value = None
00200 
00201     def value(self):
00202         return self.default if self._value is None else self._value
00203 
00204     def parse(self, value):
00205         _parse = {
00206             datetime.datetime: self._parse_datetime,
00207             datetime.timedelta: self._parse_timedelta,
00208             bool: self._parse_bool,
00209             basestring: self._parse_string,
00210         }.get(self.type, self.type)
00211         if self.multiple:
00212             self._value = []
00213             for part in value.split(","):
00214                 if self.type in (int, long):
00215                     # allow ranges of the form X:Y (inclusive at both ends)
00216                     lo, _, hi = part.partition(":")
00217                     lo = _parse(lo)
00218                     hi = _parse(hi) if hi else lo
00219                     self._value.extend(range(lo, hi + 1))
00220                 else:
00221                     self._value.append(_parse(part))
00222         else:
00223             self._value = _parse(value)
00224         return self.value()
00225 
00226     def set(self, value):
00227         if self.multiple:
00228             if not isinstance(value, list):
00229                 raise Error("Option %r is required to be a list of %s" %
00230                             (self.name, self.type.__name__))
00231             for item in value:
00232                 if item != None and not isinstance(item, self.type):
00233                     raise Error("Option %r is required to be a list of %s" %
00234                                 (self.name, self.type.__name__))
00235         else:
00236             if value != None and not isinstance(value, self.type):
00237                 raise Error("Option %r is required to be a %s (%s given)" %
00238                             (self.name, self.type.__name__, type(value)))
00239         self._value = value
00240 
00241     # Supported date/time formats in our options
00242     _DATETIME_FORMATS = [
00243         "%a %b %d %H:%M:%S %Y",
00244         "%Y-%m-%d %H:%M:%S",
00245         "%Y-%m-%d %H:%M",
00246         "%Y-%m-%dT%H:%M",
00247         "%Y%m%d %H:%M:%S",
00248         "%Y%m%d %H:%M",
00249         "%Y-%m-%d",
00250         "%Y%m%d",
00251         "%H:%M:%S",
00252         "%H:%M",
00253     ]
00254 
00255     def _parse_datetime(self, value):
00256         for format in self._DATETIME_FORMATS:
00257             try:
00258                 return datetime.datetime.strptime(value, format)
00259             except ValueError:
00260                 pass
00261         raise Error('Unrecognized date/time format: %r' % value)
00262 
00263     _TIMEDELTA_ABBREVS = [
00264         ('hours', ['h']),
00265         ('minutes', ['m', 'min']),
00266         ('seconds', ['s', 'sec']),
00267         ('milliseconds', ['ms']),
00268         ('microseconds', ['us']),
00269         ('days', ['d']),
00270         ('weeks', ['w']),
00271     ]
00272 
00273     _TIMEDELTA_ABBREV_DICT = dict(
00274         (abbrev, full) for full, abbrevs in _TIMEDELTA_ABBREVS
00275         for abbrev in abbrevs)
00276 
00277     _FLOAT_PATTERN = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?'
00278 
00279     _TIMEDELTA_PATTERN = re.compile(
00280         r'\s*(%s)\s*(\w*)\s*' % _FLOAT_PATTERN, re.IGNORECASE)
00281 
00282     def _parse_timedelta(self, value):
00283         try:
00284             sum = datetime.timedelta()
00285             start = 0
00286             while start < len(value):
00287                 m = self._TIMEDELTA_PATTERN.match(value, start)
00288                 if not m:
00289                     raise Exception()
00290                 num = float(m.group(1))
00291                 units = m.group(2) or 'seconds'
00292                 units = self._TIMEDELTA_ABBREV_DICT.get(units, units)
00293                 sum += datetime.timedelta(**{units: num})
00294                 start = m.end()
00295             return sum
00296         except Exception:
00297             raise
00298 
00299     def _parse_bool(self, value):
00300         return value.lower() not in ("false", "0", "f")
00301 
00302     def _parse_string(self, value):
00303         return _unicode(value)
00304 
00305 
00306 options = _Options()
00307 """Global options dictionary.
00308 
00309 Supports both attribute-style and dict-style access.
00310 """
00311 
00312 
00313 def define(name, default=None, type=None, help=None, metavar=None,
00314            multiple=False, group=None):
00315     """Defines a new command line option.
00316 
00317     If type is given (one of str, float, int, datetime, or timedelta)
00318     or can be inferred from the default, we parse the command line
00319     arguments based on the given type. If multiple is True, we accept
00320     comma-separated values, and the option value is always a list.
00321 
00322     For multi-value integers, we also accept the syntax x:y, which
00323     turns into range(x, y) - very useful for long integer ranges.
00324 
00325     help and metavar are used to construct the automatically generated
00326     command line help string. The help message is formatted like::
00327 
00328        --name=METAVAR      help string
00329 
00330     group is used to group the defined options in logical groups. By default,
00331     command line options are grouped by the defined file.
00332 
00333     Command line option names must be unique globally. They can be parsed
00334     from the command line with parse_command_line() or parsed from a
00335     config file with parse_config_file.
00336     """
00337     return options.define(name, default=default, type=type, help=help,
00338                           metavar=metavar, multiple=multiple, group=group)
00339 
00340 
00341 def parse_command_line(args=None):
00342     """Parses all options given on the command line (defaults to sys.argv).
00343 
00344     Note that args[0] is ignored since it is the program name in sys.argv.
00345 
00346     We return a list of all arguments that are not parsed as options.
00347     """
00348     return options.parse_command_line(args)
00349 
00350 
00351 def parse_config_file(path):
00352     """Parses and loads the Python config file at the given path."""
00353     return options.parse_config_file(path)
00354 
00355 
00356 def print_help(file=sys.stdout):
00357     """Prints all the command line options to stdout."""
00358     return options.print_help(file)
00359 
00360 
00361 def enable_pretty_logging(options=options):
00362     """Turns on formatted logging output as configured.
00363 
00364     This is called automatically by `parse_command_line`.
00365     """
00366     root_logger = logging.getLogger()
00367     if options.log_file_prefix:
00368         channel = logging.handlers.RotatingFileHandler(
00369             filename=options.log_file_prefix,
00370             maxBytes=options.log_file_max_size,
00371             backupCount=options.log_file_num_backups)
00372         channel.setFormatter(_LogFormatter(color=False))
00373         root_logger.addHandler(channel)
00374 
00375     if (options.log_to_stderr or
00376         (options.log_to_stderr is None and not root_logger.handlers)):
00377         # Set up color if we are in a tty and curses is installed
00378         color = False
00379         if curses and sys.stderr.isatty():
00380             try:
00381                 curses.setupterm()
00382                 if curses.tigetnum("colors") > 0:
00383                     color = True
00384             except Exception:
00385                 pass
00386         channel = logging.StreamHandler()
00387         channel.setFormatter(_LogFormatter(color=color))
00388         root_logger.addHandler(channel)
00389 
00390 
00391 class _LogFormatter(logging.Formatter):
00392     def __init__(self, color, *args, **kwargs):
00393         logging.Formatter.__init__(self, *args, **kwargs)
00394         self._color = color
00395         if color:
00396             # The curses module has some str/bytes confusion in
00397             # python3.  Until version 3.2.3, most methods return
00398             # bytes, but only accept strings.  In addition, we want to
00399             # output these strings with the logging module, which
00400             # works with unicode strings.  The explicit calls to
00401             # unicode() below are harmless in python2 but will do the
00402             # right conversion in python 3.
00403             fg_color = (curses.tigetstr("setaf") or
00404                         curses.tigetstr("setf") or "")
00405             if (3, 0) < sys.version_info < (3, 2, 3):
00406                 fg_color = unicode(fg_color, "ascii")
00407             self._colors = {
00408                 logging.DEBUG: unicode(curses.tparm(fg_color, 4),  # Blue
00409                                        "ascii"),
00410                 logging.INFO: unicode(curses.tparm(fg_color, 2),  # Green
00411                                       "ascii"),
00412                 logging.WARNING: unicode(curses.tparm(fg_color, 3),  # Yellow
00413                                          "ascii"),
00414                 logging.ERROR: unicode(curses.tparm(fg_color, 1),  # Red
00415                                        "ascii"),
00416             }
00417             self._normal = unicode(curses.tigetstr("sgr0"), "ascii")
00418 
00419     def format(self, record):
00420         try:
00421             record.message = record.getMessage()
00422         except Exception, e:
00423             record.message = "Bad message (%r): %r" % (e, record.__dict__)
00424         record.asctime = time.strftime(
00425             "%y%m%d %H:%M:%S", self.converter(record.created))
00426         prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % \
00427             record.__dict__
00428         if self._color:
00429             prefix = (self._colors.get(record.levelno, self._normal) +
00430                       prefix + self._normal)
00431         formatted = prefix + " " + record.message
00432         if record.exc_info:
00433             if not record.exc_text:
00434                 record.exc_text = self.formatException(record.exc_info)
00435         if record.exc_text:
00436             formatted = formatted.rstrip() + "\n" + record.exc_text
00437         return formatted.replace("\n", "\n    ")
00438 
00439 
00440 # Default options
00441 define("help", type=bool, help="show this help information")
00442 define("logging", default="info",
00443        help=("Set the Python log level. If 'none', tornado won't touch the "
00444              "logging configuration."),
00445        metavar="debug|info|warning|error|none")
00446 define("log_to_stderr", type=bool, default=None,
00447        help=("Send log output to stderr (colorized if possible). "
00448              "By default use stderr if --log_file_prefix is not set and "
00449              "no other logging is configured."))
00450 define("log_file_prefix", type=str, default=None, metavar="PATH",
00451        help=("Path prefix for log files. "
00452              "Note that if you are running multiple tornado processes, "
00453              "log_file_prefix must be different for each of them (e.g. "
00454              "include the port number)"))
00455 define("log_file_max_size", type=int, default=100 * 1000 * 1000,
00456        help="max size of log files before rollover")
00457 define("log_file_num_backups", type=int, default=10,
00458        help="number of log files to keep")


roswww
Author(s): Jonathan Mace
autogenerated on Thu Jan 2 2014 11:53:30