diff frontends/src/jp/base.py @ 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 2c75011d7b2d
children 4633cfcbcccb
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