481 lines
15 KiB
Python
481 lines
15 KiB
Python
# Copyright 2017 The Abseil Authors.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Generic entry point for Abseil Python applications.
|
|
|
|
To use this module, define a ``main`` function with a single ``argv`` argument
|
|
and call ``app.run(main)``. For example::
|
|
|
|
def main(argv):
|
|
if len(argv) > 1:
|
|
raise app.UsageError('Too many command-line arguments.')
|
|
|
|
if __name__ == '__main__':
|
|
app.run(main)
|
|
"""
|
|
|
|
import collections
|
|
import errno
|
|
import os
|
|
import pdb
|
|
import sys
|
|
import textwrap
|
|
import traceback
|
|
|
|
from absl import command_name
|
|
from absl import flags
|
|
from absl import logging
|
|
|
|
try:
|
|
import faulthandler
|
|
except ImportError:
|
|
faulthandler = None
|
|
|
|
FLAGS = flags.FLAGS
|
|
|
|
flags.DEFINE_boolean('run_with_pdb', False, 'Set to true for PDB debug mode')
|
|
flags.DEFINE_boolean('pdb_post_mortem', False,
|
|
'Set to true to handle uncaught exceptions with PDB '
|
|
'post mortem.')
|
|
flags.DEFINE_alias('pdb', 'pdb_post_mortem')
|
|
flags.DEFINE_boolean('run_with_profiling', False,
|
|
'Set to true for profiling the script. '
|
|
'Execution will be slower, and the output format might '
|
|
'change over time.')
|
|
flags.DEFINE_string('profile_file', None,
|
|
'Dump profile information to a file (for python -m '
|
|
'pstats). Implies --run_with_profiling.')
|
|
flags.DEFINE_boolean('use_cprofile_for_profiling', True,
|
|
'Use cProfile instead of the profile module for '
|
|
'profiling. This has no effect unless '
|
|
'--run_with_profiling is set.')
|
|
flags.DEFINE_boolean('only_check_args', False,
|
|
'Set to true to validate args and exit.',
|
|
allow_hide_cpp=True)
|
|
|
|
|
|
# If main() exits via an abnormal exception, call into these
|
|
# handlers before exiting.
|
|
EXCEPTION_HANDLERS = []
|
|
|
|
|
|
class Error(Exception):
|
|
pass
|
|
|
|
|
|
class UsageError(Error):
|
|
"""Exception raised when the arguments supplied by the user are invalid.
|
|
|
|
Raise this when the arguments supplied are invalid from the point of
|
|
view of the application. For example when two mutually exclusive
|
|
flags have been supplied or when there are not enough non-flag
|
|
arguments. It is distinct from flags.Error which covers the lower
|
|
level of parsing and validating individual flags.
|
|
"""
|
|
|
|
def __init__(self, message, exitcode=1):
|
|
super(UsageError, self).__init__(message)
|
|
self.exitcode = exitcode
|
|
|
|
|
|
class HelpFlag(flags.BooleanFlag):
|
|
"""Special boolean flag that displays usage and raises SystemExit."""
|
|
NAME = 'help'
|
|
SHORT_NAME = '?'
|
|
|
|
def __init__(self):
|
|
super(HelpFlag, self).__init__(
|
|
self.NAME, False, 'show this help',
|
|
short_name=self.SHORT_NAME, allow_hide_cpp=True)
|
|
|
|
def parse(self, arg):
|
|
if self._parse(arg):
|
|
usage(shorthelp=True, writeto_stdout=True)
|
|
# Advertise --helpfull on stdout, since usage() was on stdout.
|
|
print()
|
|
print('Try --helpfull to get a list of all flags.')
|
|
sys.exit(1)
|
|
|
|
|
|
class HelpshortFlag(HelpFlag):
|
|
"""--helpshort is an alias for --help."""
|
|
NAME = 'helpshort'
|
|
SHORT_NAME = None
|
|
|
|
|
|
class HelpfullFlag(flags.BooleanFlag):
|
|
"""Display help for flags in the main module and all dependent modules."""
|
|
|
|
def __init__(self):
|
|
super(HelpfullFlag, self).__init__(
|
|
'helpfull', False, 'show full help', allow_hide_cpp=True)
|
|
|
|
def parse(self, arg):
|
|
if self._parse(arg):
|
|
usage(writeto_stdout=True)
|
|
sys.exit(1)
|
|
|
|
|
|
class HelpXMLFlag(flags.BooleanFlag):
|
|
"""Similar to HelpfullFlag, but generates output in XML format."""
|
|
|
|
def __init__(self):
|
|
super(HelpXMLFlag, self).__init__(
|
|
'helpxml', False, 'like --helpfull, but generates XML output',
|
|
allow_hide_cpp=True)
|
|
|
|
def parse(self, arg):
|
|
if self._parse(arg):
|
|
flags.FLAGS.write_help_in_xml_format(sys.stdout)
|
|
sys.exit(1)
|
|
|
|
|
|
def parse_flags_with_usage(args):
|
|
"""Tries to parse the flags, print usage, and exit if unparsable.
|
|
|
|
Args:
|
|
args: [str], a non-empty list of the command line arguments including
|
|
program name.
|
|
|
|
Returns:
|
|
[str], a non-empty list of remaining command line arguments after parsing
|
|
flags, including program name.
|
|
"""
|
|
try:
|
|
return FLAGS(args)
|
|
except flags.Error as error:
|
|
message = str(error)
|
|
if '\n' in message:
|
|
final_message = 'FATAL Flags parsing error:\n%s\n' % textwrap.indent(
|
|
message, ' ')
|
|
else:
|
|
final_message = 'FATAL Flags parsing error: %s\n' % message
|
|
sys.stderr.write(final_message)
|
|
sys.stderr.write('Pass --helpshort or --helpfull to see help on flags.\n')
|
|
sys.exit(1)
|
|
|
|
|
|
_define_help_flags_called = False
|
|
|
|
|
|
def define_help_flags():
|
|
"""Registers help flags. Idempotent."""
|
|
# Use a global to ensure idempotence.
|
|
global _define_help_flags_called
|
|
|
|
if not _define_help_flags_called:
|
|
flags.DEFINE_flag(HelpFlag())
|
|
flags.DEFINE_flag(HelpshortFlag()) # alias for --help
|
|
flags.DEFINE_flag(HelpfullFlag())
|
|
flags.DEFINE_flag(HelpXMLFlag())
|
|
_define_help_flags_called = True
|
|
|
|
|
|
def _register_and_parse_flags_with_usage(
|
|
argv=None,
|
|
flags_parser=parse_flags_with_usage,
|
|
):
|
|
"""Registers help flags, parses arguments and shows usage if appropriate.
|
|
|
|
This also calls sys.exit(0) if flag --only_check_args is True.
|
|
|
|
Args:
|
|
argv: [str], a non-empty list of the command line arguments including
|
|
program name, sys.argv is used if None.
|
|
flags_parser: Callable[[List[Text]], Any], the function used to parse flags.
|
|
The return value of this function is passed to `main` untouched.
|
|
It must guarantee FLAGS is parsed after this function is called.
|
|
|
|
Returns:
|
|
The return value of `flags_parser`. When using the default `flags_parser`,
|
|
it returns the following:
|
|
[str], a non-empty list of remaining command line arguments after parsing
|
|
flags, including program name.
|
|
|
|
Raises:
|
|
Error: Raised when flags_parser is called, but FLAGS is not parsed.
|
|
SystemError: Raised when it's called more than once.
|
|
"""
|
|
if _register_and_parse_flags_with_usage.done:
|
|
raise SystemError('Flag registration can be done only once.')
|
|
|
|
define_help_flags()
|
|
|
|
original_argv = sys.argv if argv is None else argv
|
|
args_to_main = flags_parser(original_argv)
|
|
if not FLAGS.is_parsed():
|
|
raise Error('FLAGS must be parsed after flags_parser is called.')
|
|
|
|
# Exit when told so.
|
|
if FLAGS.only_check_args:
|
|
sys.exit(0)
|
|
# Immediately after flags are parsed, bump verbosity to INFO if the flag has
|
|
# not been set.
|
|
if FLAGS['verbosity'].using_default_value:
|
|
FLAGS.verbosity = 0
|
|
_register_and_parse_flags_with_usage.done = True
|
|
|
|
return args_to_main
|
|
|
|
_register_and_parse_flags_with_usage.done = False
|
|
|
|
|
|
def _run_main(main, argv):
|
|
"""Calls main, optionally with pdb or profiler."""
|
|
if FLAGS.run_with_pdb:
|
|
sys.exit(pdb.runcall(main, argv))
|
|
elif FLAGS.run_with_profiling or FLAGS.profile_file:
|
|
# Avoid import overhead since most apps (including performance-sensitive
|
|
# ones) won't be run with profiling.
|
|
# pylint: disable=g-import-not-at-top
|
|
import atexit
|
|
if FLAGS.use_cprofile_for_profiling:
|
|
import cProfile as profile
|
|
else:
|
|
import profile
|
|
profiler = profile.Profile()
|
|
if FLAGS.profile_file:
|
|
atexit.register(profiler.dump_stats, FLAGS.profile_file)
|
|
else:
|
|
atexit.register(profiler.print_stats)
|
|
sys.exit(profiler.runcall(main, argv))
|
|
else:
|
|
sys.exit(main(argv))
|
|
|
|
|
|
def _call_exception_handlers(exception):
|
|
"""Calls any installed exception handlers."""
|
|
for handler in EXCEPTION_HANDLERS:
|
|
try:
|
|
if handler.wants(exception):
|
|
handler.handle(exception)
|
|
except: # pylint: disable=bare-except
|
|
try:
|
|
# We don't want to stop for exceptions in the exception handlers but
|
|
# we shouldn't hide them either.
|
|
logging.error(traceback.format_exc())
|
|
except: # pylint: disable=bare-except
|
|
# In case even the logging statement fails, ignore.
|
|
pass
|
|
|
|
|
|
def run(
|
|
main,
|
|
argv=None,
|
|
flags_parser=parse_flags_with_usage,
|
|
):
|
|
"""Begins executing the program.
|
|
|
|
Args:
|
|
main: The main function to execute. It takes an single argument "argv",
|
|
which is a list of command line arguments with parsed flags removed.
|
|
The return value is passed to `sys.exit`, and so for example
|
|
a return value of 0 or None results in a successful termination, whereas
|
|
a return value of 1 results in abnormal termination.
|
|
For more details, see https://docs.python.org/3/library/sys#sys.exit
|
|
argv: A non-empty list of the command line arguments including program name,
|
|
sys.argv is used if None.
|
|
flags_parser: Callable[[List[Text]], Any], the function used to parse flags.
|
|
The return value of this function is passed to `main` untouched.
|
|
It must guarantee FLAGS is parsed after this function is called.
|
|
Should be passed as a keyword-only arg which will become mandatory in a
|
|
future release.
|
|
- Parses command line flags with the flag module.
|
|
- If there are any errors, prints usage().
|
|
- Calls main() with the remaining arguments.
|
|
- If main() raises a UsageError, prints usage and the error message.
|
|
"""
|
|
try:
|
|
args = _run_init(
|
|
sys.argv if argv is None else argv,
|
|
flags_parser,
|
|
)
|
|
while _init_callbacks:
|
|
callback = _init_callbacks.popleft()
|
|
callback()
|
|
try:
|
|
_run_main(main, args)
|
|
except UsageError as error:
|
|
usage(shorthelp=True, detailed_error=error, exitcode=error.exitcode)
|
|
except:
|
|
exc = sys.exc_info()[1]
|
|
# Don't try to post-mortem debug successful SystemExits, since those
|
|
# mean there wasn't actually an error. In particular, the test framework
|
|
# raises SystemExit(False) even if all tests passed.
|
|
if isinstance(exc, SystemExit) and not exc.code:
|
|
raise
|
|
|
|
# Check the tty so that we don't hang waiting for input in an
|
|
# non-interactive scenario.
|
|
if FLAGS.pdb_post_mortem and sys.stdout.isatty():
|
|
traceback.print_exc()
|
|
print()
|
|
print(' *** Entering post-mortem debugging ***')
|
|
print()
|
|
pdb.post_mortem()
|
|
raise
|
|
except Exception as e:
|
|
_call_exception_handlers(e)
|
|
raise
|
|
|
|
# Callbacks which have been deferred until after _run_init has been called.
|
|
_init_callbacks = collections.deque()
|
|
|
|
|
|
def call_after_init(callback):
|
|
"""Calls the given callback only once ABSL has finished initialization.
|
|
|
|
If ABSL has already finished initialization when ``call_after_init`` is
|
|
called then the callback is executed immediately, otherwise `callback` is
|
|
stored to be executed after ``app.run`` has finished initializing (aka. just
|
|
before the main function is called).
|
|
|
|
If called after ``app.run``, this is equivalent to calling ``callback()`` in
|
|
the caller thread. If called before ``app.run``, callbacks are run
|
|
sequentially (in an undefined order) in the same thread as ``app.run``.
|
|
|
|
Args:
|
|
callback: a callable to be called once ABSL has finished initialization.
|
|
This may be immediate if initialization has already finished. It
|
|
takes no arguments and returns nothing.
|
|
"""
|
|
if _run_init.done:
|
|
callback()
|
|
else:
|
|
_init_callbacks.append(callback)
|
|
|
|
|
|
def _run_init(
|
|
argv,
|
|
flags_parser,
|
|
):
|
|
"""Does one-time initialization and re-parses flags on rerun."""
|
|
if _run_init.done:
|
|
return flags_parser(argv)
|
|
command_name.make_process_name_useful()
|
|
# Set up absl logging handler.
|
|
logging.use_absl_handler()
|
|
args = _register_and_parse_flags_with_usage(
|
|
argv=argv,
|
|
flags_parser=flags_parser,
|
|
)
|
|
if faulthandler:
|
|
try:
|
|
faulthandler.enable()
|
|
except Exception: # pylint: disable=broad-except
|
|
# Some tests verify stderr output very closely, so don't print anything.
|
|
# Disabled faulthandler is a low-impact error.
|
|
pass
|
|
_run_init.done = True
|
|
return args
|
|
|
|
|
|
_run_init.done = False
|
|
|
|
|
|
def usage(shorthelp=False, writeto_stdout=False, detailed_error=None,
|
|
exitcode=None):
|
|
"""Writes __main__'s docstring to stderr with some help text.
|
|
|
|
Args:
|
|
shorthelp: bool, if True, prints only flags from the main module,
|
|
rather than all flags.
|
|
writeto_stdout: bool, if True, writes help message to stdout,
|
|
rather than to stderr.
|
|
detailed_error: str, additional detail about why usage info was presented.
|
|
exitcode: optional integer, if set, exits with this status code after
|
|
writing help.
|
|
"""
|
|
if writeto_stdout:
|
|
stdfile = sys.stdout
|
|
else:
|
|
stdfile = sys.stderr
|
|
|
|
doc = sys.modules['__main__'].__doc__
|
|
if not doc:
|
|
doc = '\nUSAGE: %s [flags]\n' % sys.argv[0]
|
|
doc = flags.text_wrap(doc, indent=' ', firstline_indent='')
|
|
else:
|
|
# Replace all '%s' with sys.argv[0], and all '%%' with '%'.
|
|
num_specifiers = doc.count('%') - 2 * doc.count('%%')
|
|
try:
|
|
doc %= (sys.argv[0],) * num_specifiers
|
|
except (OverflowError, TypeError, ValueError):
|
|
# Just display the docstring as-is.
|
|
pass
|
|
if shorthelp:
|
|
flag_str = FLAGS.main_module_help()
|
|
else:
|
|
flag_str = FLAGS.get_help()
|
|
try:
|
|
stdfile.write(doc)
|
|
if flag_str:
|
|
stdfile.write('\nflags:\n')
|
|
stdfile.write(flag_str)
|
|
stdfile.write('\n')
|
|
if detailed_error is not None:
|
|
stdfile.write('\n%s\n' % detailed_error)
|
|
except IOError as e:
|
|
# We avoid printing a huge backtrace if we get EPIPE, because
|
|
# "foo.par --help | less" is a frequent use case.
|
|
if e.errno != errno.EPIPE:
|
|
raise
|
|
if exitcode is not None:
|
|
sys.exit(exitcode)
|
|
|
|
|
|
class ExceptionHandler(object):
|
|
"""Base exception handler from which other may inherit."""
|
|
|
|
def wants(self, exc):
|
|
"""Returns whether this handler wants to handle the exception or not.
|
|
|
|
This base class returns True for all exceptions by default. Override in
|
|
subclass if it wants to be more selective.
|
|
|
|
Args:
|
|
exc: Exception, the current exception.
|
|
"""
|
|
del exc # Unused.
|
|
return True
|
|
|
|
def handle(self, exc):
|
|
"""Do something with the current exception.
|
|
|
|
Args:
|
|
exc: Exception, the current exception
|
|
|
|
This method must be overridden.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
def install_exception_handler(handler):
|
|
"""Installs an exception handler.
|
|
|
|
Args:
|
|
handler: ExceptionHandler, the exception handler to install.
|
|
|
|
Raises:
|
|
TypeError: Raised when the handler was not of the correct type.
|
|
|
|
All installed exception handlers will be called if main() exits via
|
|
an abnormal exception, i.e. not one of SystemExit, KeyboardInterrupt,
|
|
FlagsError or UsageError.
|
|
"""
|
|
if not isinstance(handler, ExceptionHandler):
|
|
raise TypeError('handler of type %s does not inherit from ExceptionHandler'
|
|
% type(handler))
|
|
EXCEPTION_HANDLERS.append(handler)
|