Source code for schrodinger.utils.cmdline

"""
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
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