594 lines
16 KiB
Python
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()
|