diff options
Diffstat (limited to 'cliapp/app.py')
-rw-r--r-- | cliapp/app.py | 234 |
1 files changed, 176 insertions, 58 deletions
diff --git a/cliapp/app.py b/cliapp/app.py index 9e9e28b..3c07310 100644 --- a/cliapp/app.py +++ b/cliapp/app.py @@ -24,7 +24,9 @@ import os import StringIO import sys import traceback +import time import platform +import textwrap import cliapp @@ -99,6 +101,8 @@ class Application(object): self.cmd_synopsis = {} self.subcommands = {} + self.subcommand_aliases = {} + self.hidden_subcommands = set() for method_name in self._subcommand_methodnames(): cmd = self._unnormalize_cmd(method_name) self.subcommands[cmd] = getattr(self, method_name) @@ -110,6 +114,13 @@ class Application(object): self.plugin_subdir = 'plugins' + # For meliae memory dumps. + self.memory_dump_counter = 0 + self.last_memory_dump = 0 + + # For process duration. + self._started = os.times()[-1] + def add_settings(self): '''Add application specific settings.''' @@ -143,12 +154,10 @@ class Application(object): return ''.join(x.upper() if x in ok else '_' for x in basename) def _set_process_name(self): # pragma: no cover - if platform.system() == 'Linux': - try: - with open('/proc/self/comm', 'w', 0) as f: - f.write(self.settings.progname[:15]) - except IOError, e: - logging.warning(str(e)) + comm = '/proc/self/comm' + if platform.system() == 'Linux' and os.path.exists(comm): + with open('/proc/self/comm', 'w', 0) as f: + f.write(self.settings.progname[:15]) def _run(self, args=None, stderr=sys.stderr, log=logging.critical): try: @@ -163,6 +172,8 @@ class Application(object): # config file settings. self.setup() self.enable_plugins() + if self.subcommands: + self.add_default_subcommands() args = sys.argv[1:] if args is None else args self.parse_args(args, configs_only=True) self.settings.load_configs() @@ -215,7 +226,22 @@ class Application(object): logging.info('%s version %s ends normally' % (self.settings.progname, self.settings.version)) - def add_subcommand(self, name, func, arg_synopsis=None): + def compute_setting_values(self, settings): + '''Compute setting values after configs and options are parsed. + + You can override this method to implement a default value for + a setting that is dependent on another setting. For example, + you might have settings "url" and "protocol", where protocol + gets set based on the schema of the url, unless explicitly + set by the user. So if the user sets just the url, to + "http://www.example.com/", the protocol would be set to + "http". If the user sets both url and protocol, the protocol + does not get modified by compute_setting_values. + + ''' + + def add_subcommand( + self, name, func, arg_synopsis=None, aliases=None, hidden=False): '''Add a subcommand. Normally, subcommands are defined by add ``cmd_foo`` methods @@ -231,6 +257,43 @@ class Application(object): if name not in self.subcommands: self.subcommands[name] = func self.cmd_synopsis[name] = arg_synopsis + self.subcommand_aliases[name] = aliases or [] + if hidden: # pragma: no cover + self.hidden_subcommands.add(name) + + def add_default_subcommands(self): + if 'help' not in self.subcommands: + self.add_subcommand('help', self.help) + if 'help-all' not in self.subcommands: + self.add_subcommand('help-all', self.help_all) + + def _help_helper(self, args, show_all): # pragma: no cover + try: + width = int(os.environ.get('COLUMNS', '78')) + except ValueError: + width = 78 + + fmt = cliapp.TextFormat(width=width) + + if args: + usage = self._format_usage_for(args[0]) + description = fmt.format(self._format_subcommand_help(args[0])) + text = '%s\n\n%s' % (usage, description) + else: + usage = self._format_usage(all=show_all) + description = fmt.format(self._format_description(all=show_all)) + text = '%s\n\n%s' % (usage, description) + + text = self.settings.progname.join(text.split('%prog')) + self.output.write(text) + + def help(self, args): # pragma: no cover + '''Print help.''' + self._help_helper(args, False) + + def help_all(self, args): # pragma: no cover + '''Print help, including hidden subcommands.''' + self._help_helper(args, True) def _subcommand_methodnames(self): return [x @@ -245,63 +308,57 @@ class Application(object): assert method.startswith('cmd_') return method[len('cmd_'):].replace('_', '-') - def _format_usage(self): + def _format_usage(self, all=False): '''Format usage, possibly also subcommands, if any.''' if self.subcommands: lines = [] prefix = 'Usage:' for cmd in sorted(self.subcommands.keys()): - args = self.cmd_synopsis.get(cmd, '') or '' - lines.append('%s %%prog [options] %s %s' % (prefix, cmd, args)) - prefix = ' ' * len(prefix) + if all or cmd not in self.hidden_subcommands: + args = self.cmd_synopsis.get(cmd, '') or '' + lines.append( + '%s %%prog [options] %s %s' % (prefix, cmd, args)) + prefix = ' ' * len(prefix) return '\n'.join(lines) else: return None - def _format_description(self): + def _format_usage_for(self, cmd): # pragma: no cover + args = self.cmd_synopsis.get(cmd, '') or '' + return 'Usage: %%prog [options] %s %s' % (cmd, args) + + def _format_description(self, all=False): '''Format OptionParser description, with subcommand support.''' if self.subcommands: - paras = [] + summaries = [] for cmd in sorted(self.subcommands.keys()): - paras.append(self._format_subcommand_description(cmd)) - cmd_desc = '\n\n'.join(paras) - return '%s\n\n%s' % (self._description or '', cmd_desc) + if all or cmd not in self.hidden_subcommands: + summaries.append(self._format_subcommand_summary(cmd)) + cmd_desc = ''.join(summaries) + return '%s\n%s' % (self._description or '', cmd_desc) else: return self._description - def _format_subcommand_description(self, cmd): # pragma: no cover - - def remove_empties(lines): - while lines and not lines[0].strip(): - del lines[0] - - def split_para(lines): - para = [] - while lines and lines[0].strip(): - para.append(lines[0].strip()) - del lines[0] - return para - - indent = ' ' * 4 + def _format_subcommand_summary(self, cmd): # pragma: no cover method = self.subcommands[cmd] doc = method.__doc__ or '' lines = doc.splitlines() - remove_empties(lines) if lines: - heading = '* %s -- %s' % (cmd, lines[0]) - result = [heading] - del lines[0] - remove_empties(lines) - while lines: - result.append('') - para_lines = split_para(lines) - para_text = ' '.join(para_lines) - result.append(para_text) - remove_empties(lines) - return '\n'.join(result) + summary = lines[0].strip() else: - return '* %s' % cmd - + summary = '' + return '* %%prog %s: %s\n' % (cmd, summary) + + def _format_subcommand_help(self, cmd): # pragma: no cover + method = self.subcommands[cmd] + doc = method.__doc__ or '' + t = doc.split('\n', 1) + if len(t) == 1: + return doc + else: + first, rest = t + return first + '\n' + textwrap.dedent(rest) + def setup_logging(self): # pragma: no cover '''Set up logging.''' @@ -317,11 +374,7 @@ class Application(object): level = levels.get(level_name, logging.INFO) if self.settings['log'] == 'syslog': - handler = logging.handlers.SysLogHandler(address='/dev/log') - progname = '%%'.join(self.settings.progname.split('%')) - fmt = progname + ": %(levelname)s %(message)s" - formatter = logging.Formatter(fmt) - handler.setFormatter(formatter) + handler = self.setup_logging_handler_for_syslog() elif self.settings['log'] and self.settings['log'] != 'none': handler = LogHandler( self.settings['log'], @@ -334,7 +387,7 @@ class Application(object): formatter = logging.Formatter(fmt, datefmt) handler.setFormatter(formatter) else: - handler = logging.FileHandler('/dev/null') + handler = self.setup_logging_handler_to_none() # reduce amount of pointless I/O level = logging.FATAL @@ -342,6 +395,47 @@ class Application(object): logger.addHandler(handler) logger.setLevel(level) + def setup_logging_handler_for_syslog(self): # pragma: no cover + '''Setup a logging.Handler for logging to syslog.''' + + handler = logging.handlers.SysLogHandler(address='/dev/log') + progname = '%%'.join(self.settings.progname.split('%')) + fmt = progname + ": %(levelname)s %(message)s" + formatter = logging.Formatter(fmt) + handler.setFormatter(formatter) + + return handler + + def setup_logging_handler_to_none(self): # pragma: no cover + '''Setup a logging.Handler that does not log anything anywhere.''' + + handler = logging.FileHandler('/dev/null') + return handler + + def setup_logging_handler_to_file(self): # pragma: no cover + '''Setup a logging.Handler for logging to a named file.''' + + handler = LogHandler( + self.settings['log'], + perms=int(self.settings['log-mode'], 8), + maxBytes=self.settings['log-max'], + backupCount=self.settings['log-keep'], + delay=False) + fmt = self.setup_logging_format() + datefmt = self.setup_logging_timestamp() + formatter = logging.Formatter(fmt, datefmt) + handler.setFormatter(formatter) + + return handler + + def setup_logging_format(self): # pragma: no cover + '''Return format string for log messages.''' + return '%(asctime)s %(levelname)s %(message)s' + + def setup_logging_timestamp(self): # pragma: no cover + '''Return timestamp format string for log message.''' + return '%Y-%m-%d %H:%M:%S' + def log_config(self): logging.info('%s version %s starts' % (self.settings.progname, self.settings.version)) @@ -353,6 +447,7 @@ class Application(object): f = StringIO.StringIO() cp.write(f) logging.debug('Config:\n%s' % f.getvalue()) + logging.debug('Python version: %s' % sys.version) def app_directory(self): '''Return the directory where the application class is defined. @@ -390,9 +485,10 @@ class Application(object): ''' - return self.settings.parse_args(args, configs_only=configs_only, - arg_synopsis=self.arg_synopsis, - cmd_synopsis=self.cmd_synopsis) + return self.settings.parse_args( + args, configs_only=configs_only, arg_synopsis=self.arg_synopsis, + cmd_synopsis=self.cmd_synopsis, + compute_setting_values=self.compute_setting_values) def setup(self): '''Prepare for process_args. @@ -427,11 +523,18 @@ class Application(object): if self.subcommands: if not args: raise SystemExit('must give subcommand') - if args[0] in self.subcommands: - method = self.subcommands[args[0]] - method(args[1:]) - else: - raise SystemExit('unknown subcommand %s' % args[0]) + + cmd = args[0] + if cmd not in self.subcommands: + for name in self.subcommand_aliases: + if cmd in self.subcommand_aliases[name]: + cmd = name + break + else: + raise SystemExit('unknown subcommand %s' % args[0]) + + method = self.subcommands[cmd] + method(args[1:]) else: self.process_inputs(args) @@ -523,9 +626,24 @@ class Application(object): ''' kind = self.settings['dump-memory-profile'] + interval = self.settings['memory-dump-interval'] if kind == 'none': return + + now = time.time() + if self.last_memory_dump + interval > now: + return + self.last_memory_dump = now + + # Log wall clock and CPU times for self, children. + utime, stime, cutime, cstime, elapsed_time = os.times() + duration = elapsed_time - self._started + logging.debug('process duration: %s s' % duration) + logging.debug('CPU time, in process: %s s' % utime) + logging.debug('CPU time, in system: %s s' % stime) + logging.debug('CPU time, in children: %s s' % cutime) + logging.debug('CPU time, in system for children: %s s' % cstime) logging.debug('dumping memory profiling data: %s' % msg) logging.debug('VmRSS: %s KiB' % self._vmrss()) |