#!/usr/bin/env python
# -*- encoding: utf-8 -*-
# ---------------------------------------------------------------------
# xoutil.cli
# ---------------------------------------------------------------------
# Copyright (c) 2015-2017 Merchise and Contributors
# Copyright (c) 2013, 2014 Merchise Autrement and Contributors
# All rights reserved.
#
# This is free software; you can redistribute it and/or modify it under
# the terms of the LICENCE attached in the distribution package.
#
# Created on 3 mai 2013
'''Define tools for command-line interface (CLI) applications.
CLi is a means of interaction with a computer program where the user (or
client) issues commands to the program in the form of successive lines of text
(command lines).
.. versionadded:: 1.4.1
'''
from __future__ import (division as _py3_division,
print_function as _py3_print,
unicode_literals as _py3_unicode,
absolute_import as _py3_abs_import)
from abc import abstractmethod, ABCMeta
from xoutil.eight.meta import metaclass
from xoutil.objects import classproperty
from .tools import command_name, program_name
class RegistryDescriptor(object):
'''Define a mechanism to automatically obtain all registered commands.'''
__slots__ = [str('cache')]
def __init__(self):
self.cache = {}
def __get__(self, instance, owner):
if instance is None and owner is Command:
if not self.cache:
self._settle_cache(Command)
assert self.cache.pop(command_name(Command), None) is None
return self.cache
else:
if instance:
from xoutil.eight import typeof
obj = 'Instance %s of class %s' % (id(instance),
typeof(instance).__name__)
else:
obj = 'Class %s' % owner.__name__
msg = 'Only allowed in class "Command"; used invalidly from "%s"!'
raise AttributeError(msg % obj)
def _settle_cache(self, source, recursed=set()):
# TODO: Convert check based in argument "recursed" in a decorator
name = source.__name__
if name not in recursed:
recursed.add(name)
sub_commands = source.__subclasses__()
sub_commands.extend(getattr(source, '_abc_registry', ()))
cmds = getattr(source, '__commands__', None)
if cmds:
sub_commands.extend(cmds())
if sub_commands:
for cmd in sub_commands:
self._settle_cache(cmd, recursed=recursed)
else: # Only branch commands are OK to execute
self.cache[command_name(source)] = source
else:
raise ValueError('Reused class name "%s"!' % name)
[docs]class Command(metaclass(ABCMeta)):
'''A command base registry.
There are several methods to register new commands:
* Inheriting from this class
* Using the ABC mechanism of `register` virtual subclasses.
* Registering a class with the method "__commands__" defined.
If the method "__commands__" is used, it must be a class or static method.
Command names are calculated as class names in lower case inserting a
hyphen before each new capital letter. For example "MyCommand" will be
used as "my-command".
Each command could include its own argument parser, but it isn't used
automatically, all arguments will be passed as a single parameter to
:meth:`run` removing the command when obtained from "sys.argv".
'''
__default_command__ = None
def __str__(self):
return command_name(type(self))
def __repr__(self):
return '<command: %s>' % command_name(type(self))
@classproperty
def registry(cls):
'''Obtain all registered commands.'''
if cls is Command:
name = '__registry__'
res = getattr(cls, name, {})
if not res:
cls._settle_cache(res, Command)
assert res.pop(command_name(Command), None) is None
cls._check_help(res)
setattr(cls, name, res)
return res
else:
msg = ('Invalid class "%s" for use this property, only allowed in '
'"Command"!')
raise TypeError(msg % cls.__name__)
# = RegistryDescriptor()
@abstractmethod
[docs] def run(self, args=None):
'''Must return a valid value for "sys.exit"'''
if args is None:
args = []
raise NotImplementedError
@classmethod
[docs] def set_default_command(cls, cmd=None):
'''A default command can be defined for call it when no one is
specified.
A command is detected when its name appears as the first command-line
argument.
To specify a default command, use this method with the command as a
string (the command name) or the command class.
If the command is specified, then the calling class is the selected
one.
For example::
>>> Command.set_default_command('server') # doctest: +SKIP
>>> Server.set_default_command() # doctest: +SKIP
>>> Command.set_default_command(Server) # doctest: +SKIP
'''
if cls is Command:
if cmd is not None:
from xoutil.eight import string_types as text
name = cmd if isinstance(cmd, text) else command_name(cmd)
else:
raise ValueError('missing command specification!')
else:
if cmd is None:
name = command_name(cls)
else:
msg = 'redundant command specification: "%s" and "%s"!'
raise ValueError(msg % (cls, cmd))
Command.__default_command__ = name
@staticmethod
def _settle_cache(target, source, recursed=None):
'''`target` is a mapping to store result commands'''
import sys
if recursed is None:
recursed = set()
# TODO: Convert check based in argument "recursed" in a decorator
from xoutil.names import nameof
name = nameof(source, inner=True, full=True)
if name not in recursed:
recursed.add(name)
sub_commands = type.__subclasses__(source)
sub_commands.extend(getattr(source, '_abc_registry', ()))
cmds = getattr(source, '__commands__', None)
if cmds:
from collections import Iterable
if not isinstance(cmds, Iterable):
cmds = cmds()
sub_commands.extend(cmds)
if sub_commands:
for cmd in sub_commands:
Command._settle_cache(target, cmd, recursed=recursed)
else: # Only branch commands are OK to execute
if sys.version_info < (3, 0):
from types import MethodType as ValidMethodType
else:
from types import FunctionType as ValidMethodType
assert isinstance(source.run, ValidMethodType), \
'Invalid type %r for source %r' % (type(source.run).__name__, source)
target[command_name(source)] = source
else:
raise ValueError('Reused class "%s"!' % name)
@staticmethod
def _check_help(target):
'''Check that correct help command is present.'''
name = HELP_NAME
hlp = target[name]
if hlp is not Help and not getattr(hlp, '__overwrite__', False):
target[name] = Help
[docs]class Help(Command):
'''Show all commands
Define the class attribute `__order__` to sort commands in special command
"help".
Commands could define its help in the first line of a sequence of
documentations until found:
- command class,
- "run" method,
- definition module.
This command could not be overwritten unless using the class attribute:
__override__ = True
'''
__order__ = -9999
@classmethod
[docs] def get_arg_parser(cls):
'''This is an example on how to build local argument parser.
Use class method "get
'''
res = getattr(cls, '_arg_parser')
if not res:
from argparse import ArgumentParser
res = ArgumentParser()
cls._arg_parser = res
return res
def run(self, args=[]):
print('The most commonly used "%s" commands are:' % program_name())
cmds = Command.registry
ordered = [(getattr(cmds[cmd], '__order__', 0), cmd) for cmd in cmds]
ordered.sort()
max_len = len(max(ordered, key=lambda x: len(x[1]))[1])
for _, cmd in ordered:
cmd_class = cmds[cmd]
doc = self._strip_doc(cmd_class.__doc__)
if not doc:
doc = self._strip_doc(cmd_class.run.__doc__)
if not doc:
import sys
mod_name = cmd_class.__module__
module = sys.modules.get(mod_name, None)
if module:
doc = self._strip_doc(module.__doc__)
doc = '"%s"' % (doc if doc else mod_name)
else:
doc = '"%s"' % mod_name
head = ' '*3 + cmd + ' '*(2 + max_len - len(cmd))
print(head, doc)
@staticmethod
def _strip_doc(doc):
if doc:
doc = str('%s' % doc).strip()
return str(doc.strip().split('\n')[0].strip('''.'" \t\n\r'''))
else:
return ''
HELP_NAME = command_name(Help)
# TODO: Create "xoutil.config" here
del RegistryDescriptor
del abstractmethod, ABCMeta
del metaclass, classproperty