changeset 1950:227a4e617549

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"
author Goffi <goffi@goffi.org>
date Sat, 23 Apr 2016 23:10:03 +0200
parents c5fd304d0976
children ac89f455fcbf
files frontends/src/jp/base.py frontends/src/jp/cmd_profile.py frontends/src/jp/constants.py frontends/src/jp/jp frontends/src/jp/output_std.py
diffstat 5 files changed, 147 insertions(+), 29 deletions(-) [+]
line wrap: on
line diff
--- 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
--- 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):
--- 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)
--- 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()
--- /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 <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)