# HG changeset patch # User Goffi # Date 1461445803 -7200 # Node ID 227a4e6175498ffd3628d13b7a974e574fa006bf # Parent c5fd304d09762822b337a66a475c69d1564c40da 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" diff -r c5fd304d0976 -r 227a4e617549 frontends/src/jp/base.py --- a/frontends/src/jp/base.py Sat Apr 23 01:28:35 2016 +0200 +++ b/frontends/src/jp/base.py Sat Apr 23 23:10:03 2016 +0200 @@ -39,6 +39,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 + 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 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 @@ 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 @@ # 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 @@ 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 diff -r c5fd304d0976 -r 227a4e617549 frontends/src/jp/cmd_profile.py --- a/frontends/src/jp/cmd_profile.py Sat Apr 23 01:28:35 2016 +0200 +++ b/frontends/src/jp/cmd_profile.py Sat Apr 23 23:10:03 2016 +0200 @@ -105,14 +105,13 @@ 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): diff -r c5fd304d0976 -r 227a4e617549 frontends/src/jp/constants.py --- a/frontends/src/jp/constants.py Sat Apr 23 01:28:35 2016 +0200 +++ b/frontends/src/jp/constants.py Sat Apr 23 23:10:03 2016 +0200 @@ -23,3 +23,9 @@ 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) diff -r c5fd304d0976 -r 227a4e617549 frontends/src/jp/jp --- a/frontends/src/jp/jp Sat Apr 23 01:28:35 2016 +0200 +++ b/frontends/src/jp/jp Sat Apr 23 23:10:03 2016 +0200 @@ -21,5 +21,5 @@ if __name__ == "__main__": jp = base.Jp() - jp.import_commands() + jp.import_plugins() jp.run() diff -r c5fd304d0976 -r 227a4e617549 frontends/src/jp/output_std.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/jp/output_std.py Sat Apr 23 23:10:03 2016 +0200 @@ -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 . +"""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)