00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
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
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
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
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
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
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
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
00397
00398
00399
00400
00401
00402
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),
00409 "ascii"),
00410 logging.INFO: unicode(curses.tparm(fg_color, 2),
00411 "ascii"),
00412 logging.WARNING: unicode(curses.tparm(fg_color, 3),
00413 "ascii"),
00414 logging.ERROR: unicode(curses.tparm(fg_color, 1),
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
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")