jp: --output option:

- new --output option can be added wich use_output. use_output can be True (in which case it wild default to C.OUTPUT_TEXT), or any of C.OUTPUT_TYPES (currently text, list and dict)
    - output change the output format mainly to make command result parsing more easy, but it can also be use to add fancy effects (like coloration)
    - outputs are added with plugins in the same way as commands (import of both is done in the same method)
    - in the command class, output need to be declared with use_output=C.OUTPUT_xxx, then the data only need to be processed with self.output(data)
    - outputs can have description (not used yet)
    - use_xxx argument handling has been refactored (minor refactoring) to be more generic
    - first outputs are "default", "json" (pretty printed) and "json_raw" (compact json)
    - the first command to use them is "profile list"
This commit is contained in:
Goffi 2016-04-23 23:10:03 +02:00
commit b8e62dd69c
5 changed files with 150 additions and 32 deletions

View file

@ -39,6 +39,7 @@ import sat_frontends.jp
from sat_frontends.jp.constants import Const as C
import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI
import shlex
from collections import OrderedDict
if sys.version_info < (2, 7, 3):
# XXX: shlex.split only handle unicode since python 2.7.3
@ -116,6 +117,11 @@ class Jp(object):
self._progress_id = None # TODO: manage several progress ids
self.quit_on_progress_end = True
# outputs
self._outputs = {}
for type_ in C.OUTPUT_TYPES:
self._outputs[type_] = OrderedDict()
@property
def version(self):
return self.bridge.getVersion()
@ -179,6 +185,9 @@ class Jp(object):
else:
print msg
def output(self, type_, name, data):
self._outputs[type_][name]['callback'](data)
def addOnQuitCallback(self, callback, *args, **kwargs):
"""Add a callback which will be called on quit command
@ -191,6 +200,14 @@ class Jp(object):
finally:
callbacks_list.append((callback, args, kwargs))
def getOutputChoices(self, output_type):
"""Return valid output filters for output_type
@param output_type: True for default,
else can be any registered type
"""
return self._outputs[output_type].keys()
def _make_parents(self):
self.parents = {}
@ -220,32 +237,45 @@ class Jp(object):
def add_parser_options(self):
self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT}))
def import_commands(self):
""" Automaticaly import commands to jp
looks from modules names cmd_*.py in jp path and import them
def register_output(self, type_, name, callback, description=""):
if type_ not in C.OUTPUT_TYPES:
log.error(u"Invalid output type {}".format(type_))
return
self._outputs[type_][name] = {'callback': callback,
'description': description
}
def import_plugins(self):
"""Automaticaly import commands and outputs in jp
looks from modules names cmd_*.py in jp path and import them
"""
path = os.path.dirname(sat_frontends.jp.__file__)
modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, "cmd_*.py"))))
for module_name in modules:
module = import_module("sat_frontends.jp."+module_name)
try:
self.import_command_module(module)
except ImportError:
continue
# XXX: outputs must be imported before commands as they are used for arguments
for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), (C.PLUGIN_CMD, 'cmd_*.py')):
modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, pattern))))
for module_name in modules:
module = import_module("sat_frontends.jp."+module_name)
try:
self.import_plugin_module(module, type_)
except ImportError:
continue
def import_command_module(self, module):
""" Add commands from a module to jp
@param module: module containing commands
def import_plugin_module(self, module, type_):
"""add commands or outpus from a module to jp
@param module: module containing commands or outputs
@param type_(str): one of C_PLUGIN_*
"""
try:
for classname in module.__commands__:
cls = getattr(module, classname)
class_names = getattr(module, '__{}__'.format(type_))
except AttributeError:
log.warning(_("Invalid module %s") % module)
log.warning(_("Invalid plugin module [{type}] {module}").format(type=type_, module=module))
raise ImportError
cls(self)
else:
for class_name in class_names:
cls = getattr(module, class_name)
cls(self)
def run(self, args=None):
self.args = self.parser.parse_args(args)
@ -405,20 +435,23 @@ class Jp(object):
class CommandBase(object):
def __init__(self, host, name, use_profile=True, use_progress=False, use_verbose=False, need_connect=None, help=None, **kwargs):
def __init__(self, host, name, use_profile=True, use_output=False,
need_connect=None, help=None, **kwargs):
""" Initialise CommandBase
@param host: Jp instance
@param name(unicode): name of the new command
@param use_profile(bool): if True, add profile selection/connection commands
@param use_progress(bool): if True, add progress bar activation commands
progress* signals will be handled
@param use_verbose(bool): if True, add verbosity command
@param use_output(bool, dict): if not False, add --output option
@param need_connect(bool, None): True if profile connection is needed
False else (profile session must still be started)
None to set auto value (i.e. True if use_profile is set)
Can't be set if use_profile is False
@param help(unicode): help message to display
@param **kwargs: args passed to ArgumentParser
use_* are handled directly, they can be:
- use_progress(bool): if True, add progress bar activation option
progress* signals will be handled
- use_verbose(bool): if True, add verbosity option
@attribute need_loop(bool): to set by commands when loop is needed
"""
@ -428,6 +461,7 @@ class CommandBase(object):
except AttributeError:
self.host = host
# --profile option
parents = kwargs.setdefault('parents', set())
if use_profile:
#self.host.parents['profile'] is an ArgumentParser with profile connection arguments
@ -440,11 +474,29 @@ class CommandBase(object):
# from this point, self.need_connect is None if connection is not needed at all
# False if session starting is needed, and True if full connection is needed
if use_progress:
parents.add(self.host.parents['progress'])
# --output option
if use_output:
if use_output == True:
use_output = C.OUTPUT_TEXT
assert use_output in C.OUTPUT_TYPES
self._output_type = use_output
output_parent = argparse.ArgumentParser(add_help=False)
choices = self.host.getOutputChoices(use_output)
if not choices:
raise exceptions.InternalError("No choice found for {} output type".format(use_output))
default = 'default' if 'default' in choices else choices[0]
output_parent.add_argument('--output', '-O', choices=choices, default=default, help=_(u"Select output format"))
parents.add(output_parent)
if use_verbose:
parents.add(self.host.parents['verbose'])
# other common options
use_opts = {k:v for k,v in kwargs.iteritems() if k.startswith('use_')}
for param, do_use in use_opts.iteritems():
opt=param[4:] # if param is use_verbose, opt is verbose
if opt not in self.host.parents:
raise exceptions.InternalError(u"Unknown parent option {}".format(opt))
del kwargs[param]
if do_use:
parents.add(self.host.parents[opt])
self.parser = host.subparsers.add_parser(name, help=help, **kwargs)
if hasattr(self, "subcommands"):
@ -572,6 +624,9 @@ class CommandBase(object):
def disp(self, msg, verbosity=0, error=False):
return self.host.disp(msg, verbosity, error)
def output(self, data):
return self.host.output(self._output_type, self.args.output, data)
def add_parser_options(self):
try:
subcommands = self.subcommands

View file

@ -105,14 +105,13 @@ class ProfileInfo(base.CommandBase):
class ProfileList(base.CommandBase):
def __init__(self, host):
super(ProfileList, self).__init__(host, 'list', use_profile=False, help=_('list profiles'))
super(ProfileList, self).__init__(host, 'list', use_profile=False, use_output='list', help=_('list profiles'))
def add_parser_options(self):
pass
def start(self):
for profile in self.host.bridge.getProfilesList():
print profile
self.output(self.host.bridge.getProfilesList())
class ProfileCreate(base.CommandBase):

View file

@ -23,3 +23,9 @@ from sat_frontends.quick_frontend import constants
class Const(constants.Const):
APP_NAME = "jp"
PLUGIN_CMD = "commands"
PLUGIN_OUTPUT = "outputs"
OUTPUT_TEXT = 'text' # blob of unicode text
OUTPUT_DICT = 'dict'
OUTPUT_LIST = 'list'
OUTPUT_TYPES = (OUTPUT_TEXT, OUTPUT_DICT, OUTPUT_LIST)

View file

@ -21,5 +21,5 @@ from sat_frontends.jp import base
if __name__ == "__main__":
jp = base.Jp()
jp.import_commands()
jp.import_plugins()
jp.run()

View file

@ -0,0 +1,58 @@
#! /usr/bin/python
# -*- coding: utf-8 -*-
# jp: a SàT command line tool
# Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Standard outputs"""
from sat_frontends.jp.constants import Const as C
import json
__outputs__ = ["Default", "Json"]
DEFAULT = u'default'
JSON = u'json'
JSON_RAW = u'json_raw'
class Default(object):
"""Default outputs"""
def __init__(self, jp):
jp.register_output(C.OUTPUT_TEXT, DEFAULT, self.text)
jp.register_output(C.OUTPUT_LIST, DEFAULT, self.list)
def text(self, data):
print data
def list(self, data):
print u'\n'.join(data)
class Json(object):
"""outputs in json format"""
def __init__(self, jp):
jp.register_output(C.OUTPUT_LIST, JSON, self.dump_pretty)
jp.register_output(C.OUTPUT_LIST, JSON_RAW, self.dump)
jp.register_output(C.OUTPUT_DICT, JSON, self.dump_pretty)
jp.register_output(C.OUTPUT_DICT, JSON_RAW, self.dump)
def dump(self, data):
print json.dumps(data)
def dump_pretty(self, data):
print json.dumps(data, indent=4)