Files
controller-wisetopdataserver/python/biopro/util/cli_base.py
T
2021-12-20 14:52:55 +08:00

594 lines
16 KiB
Python

"""Application Command Line Interface. The basic function and class provide for cli module.
"""
import abc
from typing import Any, Optional, Tuple, Union, List, Type, TypeVar, NoReturn, Callable
class MainForceReturn(RuntimeError):
"""interrupt user for break the CliMain.main function."""
pass
def cli_command(*command: str, value: Union[None, str, Tuple[str, ...]] = None):
"""**decorator** mark the method as a sub command handle.
code example for general usage ::
RETURN_TYPE = Union[None, CliSubCommandMain, Type[CliSubCommandMain]]
# in class definition
@cli_command('command')
def _command(self, command: str, arguments: List[str]) -> RETURN_TYPE:
'''command document'''
pass
code example for repl main ::
# in class definition
@cli_command('command', value='VALUE')
def _command(self, command: str, arguments: List[str]):
'''command document'''
pass
:param command: command name
:param value: command argument name, only used in help document generating.
"""
def _inner(f):
handle: Optional[CliHandle] = getattr(f, '_cli_handle_', None)
if not isinstance(handle, CliHandleDisable):
help_section = handle.help_section if handle is not None else None
f._cli_handle_ = h = CliHandleCommand(command, value, help_section, f.__doc__)
h.handle = f
return f
return _inner
def cli_flags(*opt: str, force_return=False):
"""**decorator** mark the method as a cli flag handle.
code example ::
# in class definition
@cli_flags('-f', '--flag')
def _flag(self, opt: str) -> None:
'''flag document'''
pass
:param opt: flag names
:param force_return: return from the argument parsing.
:raises ValueError: if *opt* not startswith ``-`` or ``--``
"""
def _inner(f):
handle: Optional[CliHandle] = getattr(f, '_cli_handle_', None)
if not isinstance(handle, CliHandleDisable):
help_section = handle.help_section if handle is not None else None
f._cli_handle_ = h = CliHandleFlag(opt, force_return, help_section, f.__doc__)
h.handle = f
return f
return _inner
def cli_options(*opt: str, value='VALUE', optional=False):
"""**decorator** mark the method as a cli options handle.
code example ::
# in class definition
@cli_options('-o', '--options', value='VALUE')
def _options(self, opt: str, value: str) -> None:
'''options document'''
pass
@cli_options('-O', '--optional', value='VALUE', optional=True)
def _optional(self, opt: str, value: Optional[str]) -> None:
'''optional document'''
pass
# accept all un-recognized options
@cli_options('--')
def _other(self, opt: str, value: Optional[str]) -> None:
'''other document'''
pass
:param opt: option name
:param value: value text, only used in help document generating
:param optional: is *value* optional
:raises ValueError: if *opt* not startswith ``-`` or ``--``
"""
def _inner(f):
handle: Optional[CliHandle] = getattr(f, '_cli_handle_', None)
if not isinstance(handle, CliHandleDisable):
help_section = handle.help_section if handle is not None else None
f._cli_handle_ = h = CliHandleOption(opt, value, optional, help_section, f.__doc__)
h.handle = f
return f
return _inner
def cli_arguments(pos: Union[int, str], value: str = 'ARGS', left_all=False):
"""**decorator** mark the method as a cli argument handle.
code example ::
# in class definition
@cli_arguments(0, value='FILE')
def _file(self, pos: int, value: str) -> None:
'''argument document'''
pass
# optional argument
@cli_arguments('?', value='FILE')
def _optional_file(self, pos: int, value: str) -> None:
'''argument document'''
pass
# one or more arguments
@cli_arguments('+', value='FILE')
def _one_or_more_file(self, pos: int, value: str) -> None:
'''argument document'''
pass
# zero or more arguments
@cli_arguments('*', value='FILE')
def _zero_or_more_file(self, pos: int, value: str) -> None:
'''argument document'''
pass
:param pos: argument position
:param value: argument text, only used in help document generating.
:param left_all: arguments behind this argument consider arguments instead of options or flags.
:raises ValueError: *pos* not a number and not one of ``?+*``
"""
def _inner(f):
handle: Optional[CliHandle] = getattr(f, '_cli_handle_', None)
if not isinstance(handle, CliHandleDisable):
help_section = handle.help_section if handle is not None else None
f._cli_handle_ = h = CliHandleArgument(pos, value, left_all, help_section, f.__doc__)
h.handle = f
return f
return _inner
def cli_help_section(section: str):
"""**decorator** mark a method in a special help section
code example ::
# in class definition
@cli_options('-o', '--options', value='VALUE')
@cli_help_section('options group 1')
def _options(self, opt: str, value: str) -> None:
'''options document'''
pass
:param section:
:return:
"""
def _inner(f):
handle: Optional[CliHandle] = getattr(f, '_cli_handle_', None)
if not isinstance(handle, CliHandleDisable):
if handle is None:
f._cli_handle_ = h = CliHandle(section, f.__doc__)
h.handle = f
else:
handle.handle = f
handle.help_section = section
return f
return _inner
def cli_disable(f=None):
"""**decorator** disable this cli handle
code example ::
# in class definition
@cli_disable
@cli_options('-o', '--options', value='VALUE')
def _options(self, opt: str, value: str) -> None:
'''options document'''
pass
could be used in those form ::
@cli_disable
@cli_disable()
:param f: function
:return:
"""
def _inner(_f):
_f._cli_handle_ = CliHandleDisable()
return _f
if f is None:
return _inner
else:
return _inner(f)
C = TypeVar('C', bound='CliOptions')
class CliOptions:
"""cli option class. used to compose with other different options by CliMain"""
__slots__ = ()
@classmethod
def get_options(cls: Type[C], *options: 'CliOptions') -> C:
"""get first options which is subclass of *cls*. If not found, create one.
:param options: cli option instances
:return: instance of this *cls*.
"""
for opt in options:
# noinspection PyTypeHints
if isinstance(opt, cls):
return opt
else:
return cls()
class CliHandle:
"""cli handle base class"""
__slots__ = ('handle', 'help_section', 'help_doc', '_target')
def __init__(self,
help_section: Optional[str],
help_doc: Optional[str] = None):
self.handle = None
'''function instance'''
self.help_section: Optional[str] = help_section
'''help section title'''
self.help_doc = help_doc
'''help document content'''
self._target = None
def bind(self, target: Any) -> 'CliHandle':
"""bind handle with *target* as first parameter.
:param target: first parameter
:return: new instance with *target* bound
"""
raise RuntimeError()
class CliHandleDisable(CliHandle):
"""disabled handle. used by :func:`cli_disable`"""
def __init__(self):
super().__init__('')
class CliHandleFlag(CliHandle):
"""cli flag handle, used by :func:`cli_flags`"""
__slots__ = 'flags', 'force_return'
def __init__(self,
flags: Tuple[str, ...],
force_return=False,
help_section: Optional[str] = None,
help_doc: Optional[str] = None):
for o in flags:
if not o.startswith('-'):
raise ValueError('illegal options : ' + o)
elif o == '-' or o == '--':
raise ValueError('illegal options : ' + o)
super().__init__(help_section, help_doc)
self.flags = flags
'''flag list'''
self.force_return = force_return
'''is this flag force return the main()?'''
def bind(self, target: Any) -> 'CliHandleFlag':
ret = CliHandleFlag(self.flags,
self.force_return,
self.help_section,
self.help_doc)
ret.handle = self.handle
ret._target = target
return ret
def invoke(self, opt: str):
"""invoke this handle
:param opt: token as flag
"""
try:
self.handle(self._target, opt)
except (MainForceReturn, SystemExit, KeyboardInterrupt):
raise
except BaseException as e:
raise RuntimeError(opt + ' : ' + ' '.join(e.args)) from e
class CliHandleOption(CliHandle):
"""cli option handle. used by :func:`cli_options`"""
__slots__ = 'options', 'value_text', 'optional'
def __init__(self,
options: Union[str, Tuple[str, ...]],
value_text: str = 'VALUE',
optional=False,
help_section: Optional[str] = None,
help_doc: Optional[str] = None):
wild_options = False
if isinstance(options, str):
options = (options,)
for o in options:
if not o.startswith('-'):
raise ValueError('illegal options : ' + o)
elif o == '-':
raise ValueError('illegal options : ' + o)
elif o == '--':
wild_options = True
if wild_options and len(options) > 1:
raise ValueError('wild options cannot corporate with other options')
super().__init__(help_section, help_doc)
self.options = options
'''options list'''
self.value_text = value_text
'''value present text'''
self.optional = optional or wild_options
'''is this option optional?'''
def bind(self, target: Any) -> 'CliHandleOption':
ret = CliHandleOption(self.options,
self.value_text,
self.optional,
self.help_section,
self.help_doc)
ret.handle = self.handle
ret._target = target
return ret
def invoke(self, opt: str, value: Optional[str]):
"""invoke this handle
:param opt: token as option flag
:param value: token as option
"""
try:
self.handle(self._target, opt, value)
except (MainForceReturn, SystemExit, KeyboardInterrupt):
raise
except ValueError as e:
raise ValueError(opt + '=' + value + ' : ' + ' '.join(e.args)) from e
except BaseException as e:
raise RuntimeError(opt + ' : ' + ' '.join(e.args)) from e
class CliHandleArgument(CliHandle):
"""cli argument handle. used by :func:`cli_arguments`"""
HELP_SECTION = 'ARGUMENTS'
__slots__ = 'position', 'value_text', 'left_all'
def __init__(self,
position: Union[int, str],
value_text: str = 'ARG',
left_all=False,
help_section: Optional[str] = None,
help_doc: Optional[str] = None):
if isinstance(position, str):
if position not in "?+*" and len(position) != 1:
raise ValueError('illegal pos symbol : ' + position)
super().__init__(help_section if help_section is not None else self.HELP_SECTION, help_doc)
self.position = position
'''argument position'''
self.value_text = value_text
'''value present text'''
self.left_all = left_all
'''does this argument will collect all the left argument?'''
def bind(self, target: Any) -> 'CliHandleArgument':
ret = CliHandleArgument(self.position,
self.value_text,
self.left_all,
self.help_section,
self.help_doc)
ret.handle = self.handle
ret._target = target
return ret
def invoke(self, pos: int, value: str):
"""invoke this handle
:param pos: token position
:param value: token as argument value
"""
try:
self.handle(self._target, pos, value)
except (MainForceReturn, SystemExit, KeyboardInterrupt):
raise
except ValueError as e:
raise ValueError(self.value_text + ' at ' + str(pos) + ' = ' + value + ' : ' + ' '.join(e.args)) from e
except BaseException as e:
raise RuntimeError(self.value_text + ' at ' + str(pos) + ' = ' + value + ' : ' + ' '.join(e.args)) from e
class CliHandleCommand(CliHandle):
"""cli command handle. used by :func:`cli_commands`"""
HELP_SECTION = 'COMMANDS'
__slots__ = 'command', 'usage_line'
def __init__(self,
command: Tuple[str, ...],
usage_line: Union[None, str, Tuple[str, ...], List[str]] = None,
help_section: Optional[str] = None,
help_doc: Optional[str] = None):
super().__init__(help_section if help_section is not None else self.HELP_SECTION, help_doc)
self.command = command
'''command text'''
self.usage_line: Optional[Tuple[str, ...]] = None
'''custom usage line'''
if isinstance(usage_line, str):
self.usage_line = (usage_line,)
elif isinstance(usage_line, (tuple, list)):
self.usage_line = tuple(usage_line)
def bind(self, target: Any) -> 'CliHandleCommand':
ret = CliHandleCommand(self.command,
self.usage_line,
self.help_section,
self.help_doc)
ret.handle = self.handle
ret._target = target
return ret
def invoke(self, command: str, argv: List[str]) -> Optional['AbstractCliMain']:
"""invoke the handle
:param command: token as command
:param argv: arguments
:return: None or instance of :class:`AbstractCliMain`
"""
try:
main = self.handle(self._target, command, argv)
except (MainForceReturn, SystemExit, KeyboardInterrupt):
raise
except ValueError as e:
raise ValueError('sub-command : ' + command + ' : ' + str(e)) from e
except BaseException as e:
raise RuntimeError('sub-command : ' + command + ' : ' + str(e)) from e
else:
if main is None:
return None
elif isinstance(main, type):
# initialize CliSubCommandMain
main = main(command)
if not isinstance(main, AbstractCliMain):
raise RuntimeError('not a CliSubCommandMain : ' + type(main).__class__)
return main
class AbstractCliMain(metaclass=abc.ABCMeta):
"""abstract cli main."""
@abc.abstractmethod
def main(self, argv: List[str] = None) -> None:
"""main function of the Main class.
:param argv: options, get from sys.argv if None.
"""
pass
@abc.abstractmethod
def parsing(self,
argv: List[str],
parsing_sub_command=True,
on_unknown_argument: Callable[[int, str], bool] = None) -> Optional[List[str]]:
"""parsing arguments.
:param argv: argument list.
:param parsing_sub_command: take first argument as sub-command
:param on_unknown_argument: error handle, pass unknown option or argument,
pass for return True, raise error for return False
:return: remind arguments
:raises ValueError: unknown options, arguments or sub-command
"""
pass
@abc.abstractmethod
def run(self) -> None:
"""handle after parsing phase"""
pass
def force_return(self) -> NoReturn:
"""force return main()
:raises MainForceReturn: always
"""
raise MainForceReturn()