summaryrefslogtreecommitdiff
path: root/cliapp/app.py
diff options
context:
space:
mode:
Diffstat (limited to 'cliapp/app.py')
-rw-r--r--cliapp/app.py234
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())