"""
Functions and classes to assist in standard processing of command line
arguments.
Command Line Guidelines
=======================
Form of Options
---------------
Long Options
~~~~~~~~~~~~
Long options should use single-dash, not double dash. That is, use -verbose,
not --verbose. (Note that this means that clustering of short options is not
available. It also means that spaces are required between short options and
values - i.e. -lquiet is not equivalent to -l quiet as it is in the default
ArgumentParser.)
Boolean Options
~~~~~~~~~~~~~~~
For boolean options, prefer either -log vs <nothing> or -log vs. -nolog to
anything that requires the specification of some true/false value to the
option.
Copyright Schrodinger, LLC. All rights reserved.
"""
import argparse
import enum
import sys
import warnings
from typing import Callable
from typing import List
from typing import Optional
from schrodinger.infra import exception_handler
from schrodinger.Qt import QtCore
# backward compatibility
from .singledashoptionparser import SingleDashOptionParser # noqa:F401
from .singledashoptionparser import _get_option_container
from .singledashoptionparser import version_string
[docs]class Options(enum.Enum):
DEBUG = enum.auto()
DEBUGGER = enum.auto()
DRIVERHOST = enum.auto()
HOST = enum.auto()
HOSTLIST = enum.auto()
HOSTWITHTHREADS = enum.auto()
HOSTWITHSUBJOBS = enum.auto()
JOBNAME = enum.auto()
LOCAL = enum.auto()
NICE = enum.auto()
NJOBS = enum.auto()
NSTRUCTS = enum.auto()
NOJOBID = enum.auto()
NOLAUNCH = enum.auto()
NOLOCAL = enum.auto()
OPLSDIR = enum.auto()
RETRIES = enum.auto()
RESTART = enum.auto()
SAVE = enum.auto()
STRICT = enum.auto()
SUBHOST = enum.auto()
TMPDIR = enum.auto()
VIEWNAME = enum.auto()
WAIT = enum.auto()
# Command-line flags
FLAG_HOST = '-HOST'
FLAG_SUBHOST = '-SUBHOST'
FLAG_LIC = '-lic'
# jlaunch.pl options:
FLAG_NODELIST = '-schrodinger_nodelist'
[docs]def main_wrapper(main: Callable, *args, exit_on_return: bool = True, **kwargs):
"""
Wrap 'main' functions for improved user interaction. This should be used
for functions invoked via __main__.
This function will call the provided 'main' function while installing an
exception handler that saves tracebacks to a file (unless running in a
development environment). KeyboardInterrupt and BrokenPipeError exceptions
are handled silently and simply exit with code 1.
:param main: The function that should be called.
:param exit_on_return: when this is set to True (default), this function
will explicitly invoke a system exit once main returns.
:param args: Additional positional arguments will be passed to the provided
main function when it is called.
"""
# Install the appropriate exception handler
exception_handler.set_exception_handler()
try:
try:
rc = main(*args, **kwargs)
if exit_on_return:
sys.exit(rc)
except (KeyboardInterrupt, BrokenPipeError):
sys.exit(1)
finally:
# wait for all QtConcurrent threads to exit JOBCON-6003
# avoids QWaitCondition: Destroyed while threads are still waiting
QtCore.QThreadPool.globalInstance().waitForDone()
# Let all other exceptions bubble on up.
return rc
[docs]def print_version(version_source: str = ""):
"""
Print the script version and exit.
:param version_source: The string to search for a Revision tag.
"""
print(version_string(version_source))
sys.exit(0)
def _check_exclusive_options(options: List[Options]):
"""
Takes user options and raises a ValueError if exclusive options are passed.
:param options: Options user will add to an ArgumentParser
:raise TypeError: If the options are conflicting
"""
exclusive_options = {
Options.HOST, Options.HOSTLIST, Options.HOSTWITHSUBJOBS,
Options.HOSTWITHTHREADS
}
present_options = set(options)
bad_options = exclusive_options.intersection(present_options)
if len(bad_options) > 1:
raise TypeError(
f"Options {[x.name for x in bad_options]} are mutually exclusive")
[docs]def add_jobcontrol_options(parser: argparse.ArgumentParser,
options: Optional[List[Options]] = None,
group_options: bool = True):
"""
Adds common Job Control options to an argparse.ArgumentParser instance.
The options are specified as a list of module enums.
Note that HOST, TMPDIR and SAVE are 'top-level' options and are not
passed down from the top-level script (i.e. $SCHRODINGER/run). These are
available so the options appear in the command line help. There are
functions in schrodinger.job.jobcontrol that get HOST information, and
environment variables can be queried for TMPDIR and SAVE if needed.
Example usage::
parser = argparse.ArgumentParser()
cmdline.add_jobcontrol_options(parser)
args = parser.parse_args()
:param parser: a parser instance
:param options: List of module enums that indicate what options to
add to the parser.
:param group_options: If True, options are added in a group in help
under the header 'Job Control Options'.
"""
if not options:
options = (Options.HOST,)
opt_group_header = 'Job Control Options'
opt_container, add_option = _get_option_container(
parser, opt_group_header if group_options else None)
_check_exclusive_options(options)
# HOST and HOSTLIST are mutually exclusive. Only HOSTLIST is added
# if both appear in the list of options.
if Options.HOST in options or Options.HOSTLIST in options:
if Options.HOSTLIST in options:
warnings.warn(
"HOSTLIST option is deprecated. Use HOST option instead.",
DeprecationWarning,
stacklevel=3)
add_option(FLAG_HOST,
dest="host",
default='localhost',
metavar='<hostname>',
help="""Run job remotely on the indicated host entry.""")
elif Options.HOSTWITHSUBJOBS in options:
add_option(
FLAG_HOST,
dest="host",
default='localhost',
metavar='<hostname>[:<n>]',
help=(
"Run job remotely on the indicated host entry with up to "
"<n> simultaneous subjobs. If <n> is not specified, the "
"default number of processors from the host entry is used if "
"submitting to a queueing system, or a single processor for a "
"host entry without a queue (localhost)."))
elif Options.HOSTWITHTHREADS in options:
add_option(
FLAG_HOST,
dest="host",
default='localhost',
metavar='<hostname>[:<n>]',
help=(
"Run job remotely on the indicated host entry with up to "
"<n> parallel threads. If <n> is not specified, the default "
"number of processors from the host entry is used if submitting "
"to a queueing system, or a thread for a host entry without a "
"queue (localhost)."))
if Options.WAIT in options:
add_option('-WAIT',
dest="wait",
default=False,
action='store_true',
help="""Do not return a prompt until the job completes.""")
if Options.LOCAL in options:
add_option('-LOCAL',
dest="local",
default=False,
action='store_true',
help=("Do not use a temporary directory for job files. "
"Keep files in the current directory."))
if Options.NOLOCAL in options:
add_option('-NOLOCAL',
dest="nolocal",
default=False,
action='store_true',
help="""Use a temporary directory for job files.""")
if Options.DEBUG in options:
add_option(
'-D', # ev98558 - support -D as a short option for -DEBUG.
'-DEBUG',
dest="debug",
default=False,
action='store_true',
help="""Show details of Job Control operation.""")
if Options.DEBUGGER in options:
add_option('-DEBUGGER',
dest="debugger",
help=("The name of the debugger application used to execute "
"a job using this debugger."))
if Options.TMPDIR in options:
add_option('-TMPDIR',
dest="tmpdir",
help=("The name of the directory used to store files "
"temporarily during a job."))
if Options.SAVE in options:
add_option(
'-SAVE',
dest="save",
default=False,
action='store_true',
help="""Return zip archive of job directory at job completion.""")
if Options.NOJOBID in options:
add_option('-NOJOBID',
dest="nojobid",
default=False,
action='store_true',
help="""Run the job directly, without Job Control layer.""")
if Options.VIEWNAME in options:
add_option(
'-VIEWNAME',
dest="viewname",
default=False,
metavar='<viewname>',
help="""Specifies viewname used in job filtering in maestro.""")
if Options.OPLSDIR in options:
add_option(
'-OPLSDIR',
dest="oplsdir",
help="""Specifies directory for custom forcefield parameters.""")
if Options.JOBNAME in options:
add_option('-JOBNAME', help="""Provide an explicit name for the job.""")
[docs]def add_standard_options(
parser: argparse.ArgumentParser,
options: Optional[List[Options]] = None,
group_options: bool = True,
default_retries: Optional[int] = None,
):
"""
Adds standard options to an argparse.ArgumentParser instance.
The options are specified as a list of module enums. Accepted values are
NJOBS, NSTRUCTS, STRICT, RETRIES, NOLAUNCH, and RESTART.
Please note that NJOBS and NSTRUCTS options are mutual exclusive.
i.e. njobs = total_structures / structures_per_job
User can either provide njobs or structures_per_job; so one option is
affecting the other option (in short, in specifying one option it can set
the proper value for the other option), hence NJOBS and NSTRUCTS options
are mutual exclusive.
Example usage::
parser = argparse.ArgumentParser()
cmdline.add_standard_options(parser)
args = cmdline.parse_standard_options(parser, args)
:param parser: a parser instance
:param options: List of module enums that indicate what options to
add to the parser.
:param group_options: If True then the added options are added as a group
in the help message under the header 'Standard
Options'.
:param default_retries: The default number of retries to use (only
applies is RETRIES is included in options)
"""
if not options:
options = [
Options.NJOBS, Options.NSTRUCTS, Options.STRICT, Options.RETRIES,
Options.NOLAUNCH, Options.RESTART
]
validation_positive_int_error = "Please specify positive integer number."
validation_duplication_error_msg = (
"Please note -NJOBS and -NSTRUCTS options are mutually exclusive\n"
"We can derive the value of -NJOBS from -NSTRUCTS and vice versa.\n"
"Please specify only one option.")
def validate_callback(option, opt_str, value, parser):
if value < 0:
parser.error(validation_positive_int_error)
setattr(parser.values, option.dest, value)
if hasattr(parser.values, 'njobs') and hasattr(parser.values,
'nstructs'):
if parser.values.njobs and parser.values.nstructs:
parser.error(validation_duplication_error_msg)
class validate_n(argparse.Action):
def __call__(self, parser, namespace, value, option_string=None):
if value < 0:
parser.error(validation_positive_int_error)
setattr(namespace, self.dest, value)
if hasattr(namespace, 'njobs') and hasattr(namespace, 'nstructs'):
if namespace.njobs and namespace.nstructs:
parser.error(validation_duplication_error_msg)
opt_group_header = 'Standard Options'
opt_container, add_option = _get_option_container(
parser, opt_group_header if group_options else None)
if isinstance(parser, argparse.ArgumentParser):
validate_kwargs = {'type': int, 'action': validate_n}
retries_type = int
else:
validate_kwargs = {
'type': "int",
'action': "callback",
'callback': validate_callback
}
retries_type = 'int'
if Options.NJOBS in options:
add_option('-NJOBS',
dest="njobs",
help="Divide the overall job into NJOBS subjobs.",
**validate_kwargs)
if Options.NSTRUCTS in options:
add_option(
'-NSTRUCTS',
dest="nstructs",
help=("Divide the overall job into subjobs with no more than "
"NSTRUCTS structures."),
**validate_kwargs)
# As per the 'CommandLineStandards' twiki, user is advised to use either
# -HOST and -SUBHOST OR -HOST and -DRIVERHOST pair.
if Options.DRIVERHOST in options and Options.SUBHOST not in options:
add_option(
'-DRIVERHOST',
dest="driverhost",
metavar='<hostname>',
help=("Run the driver on the specified host. The subjobs are run "
"on the hosts specified with %s." % FLAG_HOST))
elif Options.SUBHOST in options:
mvar = ('<hostname> or\n %s <hostname:nproc> or\n '
'%s "hostname1:nproc1 ... hostnameN:nprocN"' %
(FLAG_SUBHOST, FLAG_SUBHOST))
if isinstance(parser, argparse.ArgumentParser):
# ArgumentParser cannot accept METAVER strings with \n in them but
# the SingleDashOptionParser can.
mvar = mvar.replace('\n ', "")
add_option(
FLAG_SUBHOST,
dest="subhost",
metavar=mvar,
help=("Run the subjobs on the specified hosts. The driver is run "
"on the host specified with %s." % FLAG_HOST))
if Options.STRICT in options:
add_option('-STRICT',
dest="strict",
default=False,
action='store_true',
help="""Terminate the job if any subjob dies.""")
if Options.RETRIES in options:
if default_retries is not None:
default_retries_help = " (Default: %d)" % default_retries
else:
default_retries_help = ""
add_option('-RETRIES',
dest="retries",
default=default_retries,
type=retries_type,
help=("If a subjob fails for any reason, it will be retried "
"RETRIES times." + default_retries_help))
if Options.NOLAUNCH in options:
add_option('-NOLAUNCH',
dest="nolaunch",
default=False,
action='store_true',
help="""Set up subjob inputs, but don't run the jobs.""")
if Options.RESTART in options:
add_option(
'-RESTART',
dest="restart",
default=False,
action='store_true',
help=("Restart a previously failed job, utilizing any already "
"completed subjobs."))
[docs]def create_argument_parser(*args, **kwargs):
"""
Prefer use of argparse.ArgumentParser directly.
Factory function to get an argparse.ArgumentParser with standard help
and version added in a single dash fashion.
Takes the same arguments as argparse.ArgumentParser.
:type version_source: str
:keyword version_source: A string containing a CVS Revision string like
$Revision: 1.26 $. If not specified, it uses the
Schrodinger python version number.
"""
# Pop the version_source keyword argument from the dictionary, as
# it's not a valid argument for the superclass.
version_source = version_string(kwargs.pop("version_source", ""))
kwargs['add_help'] = False
parser = argparse.ArgumentParser(*args, **kwargs)
parser.add_argument('-v',
'-version',
action='version',
version=str(version_source),
help="Show the program's version and exit.")
parser.add_argument("-h",
'-help',
action='help',
help="Show this help message and exit.")
return parser
# Legacy definitions from pre-enum days
DEBUG = Options.DEBUG
DEBUGGER = Options.DEBUGGER
DRIVERHOST = Options.DRIVERHOST
HOST = Options.HOST
HOSTLIST = Options.HOSTLIST
JOBNAME = Options.JOBNAME
LOCAL = Options.LOCAL
NICE = Options.NICE
NJOBS = Options.NJOBS
NOJOBID = Options.NOJOBID
NOLAUNCH = Options.NOLAUNCH
NOLOCAL = Options.NOLOCAL
NSTRUCTS = Options.NSTRUCTS
OPLSDIR = Options.OPLSDIR
SAVE = Options.SAVE
SUBHOST = Options.SUBHOST
STRICT = Options.STRICT
RESTART = Options.RESTART
RETRIES = Options.RETRIES
TMPDIR = Options.TMPDIR
VIEWNAME = Options.VIEWNAME
WAIT = Options.WAIT