class CLIEngine(object): """A base class for Command Line Inteface facing tools. :class:`CLIEnigne` provides the basic structure to set up a simple command-line interface, based on the :mod:`argparse` framework. The only required implementation below is the :meth:`do` method. All other settings are optional. :param str prefix_chars: Sets the prefix characters to command line arguments. by default this is set to "-", reqiuring that all command line argumetns begin with "-". At the end of initialization, the configuration (:attr:`config`) and :attr:`parser` will be availabe for use. """ # pylint: disable= too-many-instance-attributes @property def description(self): """The textual description of the command line interface, set as the description of the parser from :mod:`argparse`.""" return self.__doc__ debug = __debug__ """Whether this tool will show stack traces.""" epilog = "" """The text that comes at the end of the :mod:`argparse` help text.""" defaultcfg = "Config.yml" """The name of the default configuration file to be loaded. If set to ``False``, no configuration will occur.""" supercfg = [] """This is a list of tuples which represent configurations that should be loaded before the default configuration is loaded. Each tuple contains the module name and filename pair that should be passed to :func:`~pkg_resources.resource_filename`. To specify a super-configuration in the current directory, use ``__main__`` as the module name.""" def __init__(self, prefix_chars=str("-"), conflict_handler='error'): super(CLIEngine, self).__init__() self._log = getLogger(self.__module__) self._parser = ArgumentParser( prefix_chars = prefix_chars, add_help = False, formatter_class = RawDescriptionHelpFormatter, description = self.description, epilog = self.epilog, conflict_handler = conflict_handler) self._home = os.environ["HOME"] self.config = StructuredConfiguration() self._opts = None self._rargs = None self.__help_action = None self.exitcode = 0 self._hasinit = False self._hasargs = False self._hasvars = True config = ConfigurationProperty() @property def parser(self): """:class:`argparse.ArgumentParser` instance for this engine.""" if getattr(self, '_hasvars', False): return self._parser else: raise AttributeError("Parser has not yet been initialized!") @property def opts(self): """Command Line Options, as paresed, for this engine""" return self._opts @property def log(self): """This engine's logger""" return self._log def init(self): """Initialization after the parser has been created.""" self._hasinit = True if self.defaultcfg: self._add_configfile_args() self._add_configure_args() if self.debug: self._add_debug_args() def arguments(self, *args): r"""Parse the given arguments. If no arguments are given, parses the known arguments on the command line. Generally this should parse all arguments except ``-h`` for help, which should be parsed last after the help text has been fully assembled. The full help text can be set to the ``self.parser.epilog`` attribute. :param \*args: The arguments to be parsed. Similar to taking the command line components and doing ``" -h --config test.yml".split()``. Same signature as would be used for :meth:`argparse.ArgumentParser.parse_args()` """ if not self._hasinit: self.init() self._opts, self._rargs = self.parser.parse_known_args(*args) self._hasargs = True def before_configure(self): """Actions to be run before configuration. This method can be overwritten to provide custom actions to be taken before the engine gets configured. """ pass def configure(self): """Configure the command line engine from a series of YAML files. The configuration loads (starting with a blank configuration): 1. The :attr:`module` configuration file named for :attr:`defaultcfg` 2. The command line specified file from the user's home folder ``~/config.yml`` 3. The command line specified file from the working directory. If the third file is not found, and the user specified a new name for the configuration file, then the user is warned that no configuration file could be found. This way the usre is only warned about a missing configuration file if they requested a file specifically (and so intended to use a customized file). """ cfg = getattr(self.opts, 'config', self.defaultcfg) self.config.configure(module=self.__module__, defaultcfg=self.defaultcfg, cfg=cfg,supercfg=self.supercfg) self.config.parse_literals(*getattr(self.opts, 'configure', [])) def after_configure(self): """Actions to be run after configuration. This method can be overwritten to provide custom actions to be taken before the engine gets configured.""" pass def parse(self): """Parse the command line arguments. This function uses the arguments passed in through :meth:`arguments`, adds the `-h` option, and calls the parser to understand and act on the arguments. Arguments are then stored in the :attr:`opts` attribute. This method also calls :meth:`configure_logging` to set up the logger if it is ready to go. .. note:: Calling :meth:`configure_logging` allows :meth:`configure` to change the logging configuration values before the logger is configured. """ self._add_help() self._opts = self.parser.parse_args(self._rargs, self._opts) self.configure_logging() @abc.abstractmethod def do(self): # pylint: disable= invalid-name """This method should handle the main operations for the command line tool. The user should overwrite this method in Engine subclasses for thier own use. The :exc:`KeyboardInterrupt` or :exc:`SystemExit` errors will be caught by :meth:`kill`""" pass def kill(self): """This function should forcibly kill any subprocesses. It is called when :meth:`do` raises a :exc:`KeyboardInterrupt` or :exc:`SystemExit` to ensure that any tasks can be finalized before the system exits. Errors raised here are not caught.""" pass def run(self): """This method is used to run the command line engine in the expected order. This method should be called to run the engine from another program.""" if not self._hasinit: self.init() if not self._hasargs: warn("Implied Command-line mode", UserWarning) self.arguments() self.before_configure() self.configure() self.after_configure() self.parse() try: self.do() except SystemExit as exc: if not getattr(exc, 'code', 0): self.kill() if self.debug: raise self.exitcode = getattr(exc, 'code', self.exitcode) except KeyboardInterrupt as exc: self.kill() if self.debug: raise return self.exitcode @classmethod def script(cls): """The class method for using this module as a script entry point. This method ensures that the engine runs correctly on the command line, and is cleaned up at the end.""" engine = cls() engine.arguments() return engine.run() def _remove_help(self): """Remove the ``-h, --help`` argument from the parser. .. Warning:: This method uses a swizzle to access protected parser attributes and remove the argument as best as possible. It may break! """ # pylint: disable= protected-access for option_string in self.__help_action.option_strings: del self._parser._option_string_actions[option_string] self._parser._remove_action(self.__help_action) def _add_help(self): """Add the ``-h, --help`` argument.""" self.__help_action = self.parser.add_argument('-h', '--help', action='help', help="Display this help text") def _add_configfile_args(self,*args): """Add a parser command line argument for changing configuration files. :arguments: The set of arguments to be passed to :meth:`~argparse.ArgumentParser.add_argument`. """ if len(args) == 0: args = ("--config",) self.parser.add_argument(*args, dest='config', action='store', metavar='file.yml', default=self.defaultcfg, help="Set configuration file. By default, load %(file)s and" " ~/%(file)s if it exists." % dict(file=self.defaultcfg)) self.parser.register('action', 'config', bind_configuration_action(self.config)) def _add_configure_args(self,*args): """Add a parser command line argument for literal configuration items. See :meth:`~pyshell.config.DottedConfiguration.parse_literals`. :arguments: The set of arguments to be passed to :meth:`~argparse.ArgumentParser.add_argument`. """ if len(args) == 0: args = ("--configure",) self.parser.add_argument(*args, dest='configure', action='append', metavar='Item.Key=value',default=[], help="Set configuration value. The value is parsed as a" " python literal.") def _add_debug_args(self, *args): """Add debugging arguments.""" if len(args) == 0: args = ('--ipdb',) self.parser.add_argument(*args, dest='debug', action=ipydbAction, help="enable the ipython debugger.") def configure_logging(self): """Configure the logging system using the configuration underneath ``config["logging"]`` as a dictionary configuration for the :mod:`logging` module.""" configure_logging(self.config)
class CLIEngine(object): """A base class for Command Line Inteface facing tools. :class:`CLIEnigne` provides the basic structure to set up a simple command-line interface, based on the :mod:`argparse` framework. The only required implementation below is the :meth:`do` method. All other settings are optional. :param str prefix_chars: Sets the prefix characters to command line arguments. by default this is set to "-", reqiuring that all command line argumetns begin with "-". At the end of initialization, the configuration (:attr:`config`) and :attr:`parser` will be availabe for use. """ # pylint: disable= too-many-instance-attributes @property def description(self): """The textual description of the command line interface, set as the description of the parser from :mod:`argparse`.""" return self.__doc__ debug = __debug__ """Whether this tool will show stack traces.""" epilog = "" """The text that comes at the end of the :mod:`argparse` help text.""" defaultcfg = "Config.yml" """The name of the default configuration file to be loaded. If set to ``False``, no configuration will occur.""" supercfg = [] """This is a list of tuples which represent configurations that should be loaded before the default configuration is loaded. Each tuple contains the module name and filename pair that should be passed to :func:`~pkg_resources.resource_filename`. To specify a super-configuration in the current directory, use ``__main__`` as the module name.""" def __init__(self, prefix_chars=str("-"), conflict_handler='error'): super(CLIEngine, self).__init__() self._log = getLogger(self.__module__) self._parser = ArgumentParser( prefix_chars=prefix_chars, add_help=False, formatter_class=RawDescriptionHelpFormatter, description=self.description, epilog=self.epilog, conflict_handler=conflict_handler) self._home = os.environ["HOME"] self.config = StructuredConfiguration() self._opts = None self._rargs = None self.__help_action = None self.exitcode = 0 self._hasinit = False self._hasargs = False self._hasvars = True config = ConfigurationProperty() @property def parser(self): """:class:`argparse.ArgumentParser` instance for this engine.""" if getattr(self, '_hasvars', False): return self._parser else: raise AttributeError("Parser has not yet been initialized!") @property def opts(self): """Command Line Options, as paresed, for this engine""" return self._opts @property def log(self): """This engine's logger""" return self._log def init(self): """Initialization after the parser has been created.""" self._hasinit = True if self.defaultcfg: self._add_configfile_args() self._add_configure_args() if self.debug: self._add_debug_args() def arguments(self, *args): r"""Parse the given arguments. If no arguments are given, parses the known arguments on the command line. Generally this should parse all arguments except ``-h`` for help, which should be parsed last after the help text has been fully assembled. The full help text can be set to the ``self.parser.epilog`` attribute. :param \*args: The arguments to be parsed. Similar to taking the command line components and doing ``" -h --config test.yml".split()``. Same signature as would be used for :meth:`argparse.ArgumentParser.parse_args()` """ if not self._hasinit: self.init() self._opts, self._rargs = self.parser.parse_known_args(*args) self._hasargs = True def before_configure(self): """Actions to be run before configuration. This method can be overwritten to provide custom actions to be taken before the engine gets configured. """ pass def configure(self): """Configure the command line engine from a series of YAML files. The configuration loads (starting with a blank configuration): 1. The :attr:`module` configuration file named for :attr:`defaultcfg` 2. The command line specified file from the user's home folder ``~/config.yml`` 3. The command line specified file from the working directory. If the third file is not found, and the user specified a new name for the configuration file, then the user is warned that no configuration file could be found. This way the usre is only warned about a missing configuration file if they requested a file specifically (and so intended to use a customized file). """ cfg = getattr(self.opts, 'config', self.defaultcfg) self.config.configure(module=self.__module__, defaultcfg=self.defaultcfg, cfg=cfg, supercfg=self.supercfg) self.config.parse_literals(*getattr(self.opts, 'configure', [])) def after_configure(self): """Actions to be run after configuration. This method can be overwritten to provide custom actions to be taken before the engine gets configured.""" pass def parse(self): """Parse the command line arguments. This function uses the arguments passed in through :meth:`arguments`, adds the `-h` option, and calls the parser to understand and act on the arguments. Arguments are then stored in the :attr:`opts` attribute. This method also calls :meth:`configure_logging` to set up the logger if it is ready to go. .. note:: Calling :meth:`configure_logging` allows :meth:`configure` to change the logging configuration values before the logger is configured. """ self._add_help() self._opts = self.parser.parse_args(self._rargs, self._opts) self.configure_logging() @abc.abstractmethod def do(self): # pylint: disable= invalid-name """This method should handle the main operations for the command line tool. The user should overwrite this method in Engine subclasses for thier own use. The :exc:`KeyboardInterrupt` or :exc:`SystemExit` errors will be caught by :meth:`kill`""" pass def kill(self): """This function should forcibly kill any subprocesses. It is called when :meth:`do` raises a :exc:`KeyboardInterrupt` or :exc:`SystemExit` to ensure that any tasks can be finalized before the system exits. Errors raised here are not caught.""" pass def run(self): """This method is used to run the command line engine in the expected order. This method should be called to run the engine from another program.""" if not self._hasinit: self.init() if not self._hasargs: warn("Implied Command-line mode", UserWarning) self.arguments() self.before_configure() self.configure() self.after_configure() self.parse() try: self.do() except SystemExit as exc: if not getattr(exc, 'code', 0): self.kill() if self.debug: raise self.exitcode = getattr(exc, 'code', self.exitcode) except KeyboardInterrupt as exc: self.kill() if self.debug: raise return self.exitcode @classmethod def script(cls): """The class method for using this module as a script entry point. This method ensures that the engine runs correctly on the command line, and is cleaned up at the end.""" engine = cls() engine.arguments() return engine.run() def _remove_help(self): """Remove the ``-h, --help`` argument from the parser. .. Warning:: This method uses a swizzle to access protected parser attributes and remove the argument as best as possible. It may break! """ # pylint: disable= protected-access for option_string in self.__help_action.option_strings: del self._parser._option_string_actions[option_string] self._parser._remove_action(self.__help_action) def _add_help(self): """Add the ``-h, --help`` argument.""" self.__help_action = self.parser.add_argument( '-h', '--help', action='help', help="Display this help text") def _add_configfile_args(self, *args): """Add a parser command line argument for changing configuration files. :arguments: The set of arguments to be passed to :meth:`~argparse.ArgumentParser.add_argument`. """ if len(args) == 0: args = ("--config", ) self.parser.add_argument( *args, dest='config', action='store', metavar='file.yml', default=self.defaultcfg, help="Set configuration file. By default, load %(file)s and" " ~/%(file)s if it exists." % dict(file=self.defaultcfg)) self.parser.register('action', 'config', bind_configuration_action(self.config)) def _add_configure_args(self, *args): """Add a parser command line argument for literal configuration items. See :meth:`~pyshell.config.DottedConfiguration.parse_literals`. :arguments: The set of arguments to be passed to :meth:`~argparse.ArgumentParser.add_argument`. """ if len(args) == 0: args = ("--configure", ) self.parser.add_argument( *args, dest='configure', action='append', metavar='Item.Key=value', default=[], help="Set configuration value. The value is parsed as a" " python literal.") def _add_debug_args(self, *args): """Add debugging arguments.""" if len(args) == 0: args = ('--ipdb', ) self.parser.add_argument(*args, dest='debug', action=ipydbAction, help="enable the ipython debugger.") def configure_logging(self): """Configure the logging system using the configuration underneath ``config["logging"]`` as a dictionary configuration for the :mod:`logging` module.""" configure_logging(self.config)