diff frontends/src/jp/base.py @ 817:c39117d00f35

jp: refactoring: - imports from sat_frontends.jp instead of local imports - added __init__.py - commands now inherits from a base class: each base.CommandBase instance is a subcommand - new arguments are added in CommandBase.add_parser_options methods, starting point si CommandBase.run or CommandBase.connected if a profile connection is needed - commands are exported using a __commands__ variable at the top of the module - sub-subcommand are easily added by using an other CommandBase instance as parent instead of using a Jp instance. In this case, the parent subcommand must be the one exported, and have a subcommands iterable (see cmd_file or cmd_pipe for examples). - options which are often used (like --profile) are automatically added on demand (use_profile=True, use_progress=True) - commands are automatically loaded when there are in a module named cmd_XXX - restored --connect option - restored progress bar - restored getVersion bridge call on jp --version - fixed file and pipe commands - fixed forgotten translations - fixed non SàT compliant docstrings - better about/version dialog
author Goffi <goffi@goffi.org>
date Mon, 10 Feb 2014 13:44:09 +0100
parents 59c7bc51c323
children 300b4de701a6
line wrap: on
line diff
--- a/frontends/src/jp/base.py	Wed Feb 05 14:52:40 2014 +0100
+++ b/frontends/src/jp/base.py	Mon Feb 10 13:44:09 2014 +0100
@@ -17,24 +17,8 @@
 # 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/>.
 
-from __future__ import with_statement
 from sat.core.i18n import _
 
-#consts
-name = u"jp"
-about = name+u""" v%s (c) Jérôme Poisson (aka Goffi) 2009, 2010, 2011, 2012, 2013, 2014
-
----
-"""+name+u""" Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (aka Goffi)
-This program comes with ABSOLUTELY NO WARRANTY;
-This is free software, and you are welcome to redistribute it
-under certain conditions.
----
-
-This software is a command line tool for jabber
-Get the latest version at http://www.goffi.org
-"""
-
 global pbar_available
 pbar_available = True #checked before using ProgressBar
 
@@ -46,35 +30,40 @@
 ###
 
 import sys
-import os
-from os.path import abspath, basename, dirname
-from argparse import ArgumentParser
+import locale
+import os.path
+import argparse
+import gobject
+from glob import iglob
+from importlib import import_module
 from sat.tools.jid import JID
-import gobject
 from sat_frontends.bridge.DBus import DBusBridgeFrontend
-from sat.core.exceptions import BridgeExceptionNoService, BridgeInitError
-from sat.tools.utils import clean_ustr
-import tarfile
-import tempfile
-import shutil
+from sat.core import exceptions
+import sat_frontends.jp
 try:
-    from progressbar import ProgressBar, Percentage, Bar, ETA, FileTransferSpeed
-except ImportError, e:
-        info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar'))
-        info (_('Progress bar deactivated\n--\n'))
-        pbar_available=False
+    import progressbar
+except ImportError:
+    info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar'))
+    info (_('Progress bar deactivated\n--\n'))
+    progressbar=None
+
+#consts
+prog_name = u"jp"
+description = """This software is a command line tool for XMPP.
+Get the latest version at http://sat.goffi.org"""
+
+copyleft = u"""Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (aka Goffi)
+This program comes with ABSOLUTELY NO WARRANTY;
+This is free software, and you are welcome to redistribute it under certain conditions.
+"""
 
 
-#version = unicode(self.bridge.getVersion())
-version = "undefined"
-parser = ArgumentParser()
-parser.add_argument('--version', action='version', version=about % version)
-subparser = parser.add_subparsers(dest='subparser_name')
-# File managment
+def unicode_decoder(arg):
+    # Needed to have unicode strings from arguments
+    return arg.decode(locale.getpreferredencoding())
 
 
-
-class JP(object):
+class Jp(object):
     """
     This class can be use to establish a connection with the
     bridge. Moreover, it should manage a main loop.
@@ -83,64 +72,112 @@
     specify what kind of operation you want to perform.
 
     """
-    def __init__(self, start_mainloop = False):
+    def __init__(self):
         try:
-            self.bridge=DBusBridgeFrontend()
-        except BridgeExceptionNoService:
+            self.bridge = DBusBridgeFrontend()
+        except exceptions.BridgeExceptionNoService:
             print(_(u"Can't connect to SàT backend, are you sure it's launched ?"))
             sys.exit(1)
-        except BridgeInitError:
+        except excpetions.BridgeInitError:
             print(_(u"Can't init bridge"))
             sys.exit(1)
 
-        self._start_loop = start_mainloop
+        self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
+                                              description=description)
+
+        self._make_parents()
+        self.add_parser_options()
+        self.subparsers = self.parser.add_subparsers(title=_('Available commands'), dest='subparser_name')
+        self._auto_loop = False # when loop is used for internal reasons
+        self.need_loop = False # to set by commands when loop is needed
+        self._progress_id = None # TODO: manage several progress ids
+        self.quit_on_progress_end = True # set to False if you manage yourself exiting, or if you want the user to stop by himself
+
+    @property
+    def version(self):
+        return self.bridge.getVersion()
 
-    def run(self):
-        raise NotImplementedError
+    @property
+    def progress_id(self):
+        return self._progress_id
+
+    @progress_id.setter
+    def progress_id(self, value):
+        self._progress_id = value
+
+    def _make_parents(self):
+        self.parents = {}
+
+        profile_parent = self.parents['profile'] = argparse.ArgumentParser(add_help=False)
+        profile_parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)"))
+        profile_parent.add_argument("-c", "--connect", action="store_true", help=_("Connect the profile before doing anything else"))
+
+        progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False)
+        if progressbar:
+            progress_parent.add_argument("-g", "--progress", action="store_true", help=_("Show progress bar"))
 
-    def _run(self):
-        """Call run and lauch a loop if needed"""
-        print "You are connected!"
-        self.run()
-        if self._start_loop:
-            print "Exiting loop..."
-            self.loop.quit()
+    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
+
+        """
+        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
 
-    def _loop_start(self):
+    def import_command_module(self, module):
+        """ Add commands from a module to jp
+        @param module: module containing commands
+
+        """
+        try:
+            for classname in module.__commands__:
+                cls = getattr(module, classname)
+        except AttributeError:
+            warning(_("Invalid module %s") % module)
+            raise ImportError
+        cls(self)
+
+
+    def run(self, args=None):
+        self.args = self.parser.parse_args(args)
+        self.args.func()
+        if self.need_loop or self._auto_loop:
+            self._start_loop()
+
+    def _start_loop(self):
         self.loop = gobject.MainLoop()
         try:
             self.loop.run()
         except KeyboardInterrupt:
             info(_("User interruption: good bye"))
 
-    def start_mainloop(self):
-        self._start_loop = True
-
-    def go(self):
-        self.run()
-        if self._start_loop:
-            self._loop_start()
-
+    def stop_loop(self):
+        try:
+            self.loop.quit()
+        except AttributeError:
+            pass
 
-class JPWithProfile(JP):
-    """Manage a bridge (inherit from :class:`JP`), but it also adds
-    profile managment, ie, connection to the profile.
-
-    Moreover, some useful methods are predefined such as
-    :py:meth:`check_jids`. The connection to XMPP is automatically
-    managed.
-    """
-
-    def __init__(self, profile_name, start_mainloop = False):
-        JP.__init__(self, start_mainloop)
-        self.profile_name = profile_name
+    def quit(self, errcode=0):
+        self.stop_loop()
+        if errcode:
+            sys.exit(errcode)
 
     def check_jids(self, jids):
         """Check jids validity, transform roster name to corresponding jids
 
-        :param profile: A profile name
-        :param jids: A list of jids
-        :rtype: A list of jids
+        @param profile: profile name
+        @param jids: list of jids
+        @return: List of jids
+
         """
         names2jid = {}
         nodes2jid = {}
@@ -151,7 +188,7 @@
                 names2jid[attr["name"].lower()] = _jid
             nodes2jid[JID(_jid).node.lower()] = _jid
 
-        def expandJid(jid):
+        def expand_jid(jid):
             _jid = jid.lower()
             if _jid in names2jid:
                 expanded = names2jid[_jid]
@@ -164,100 +201,204 @@
         def check(jid):
             if not jid.is_valid:
                 error (_("%s is not a valid JID !"), jid)
-                exit(1)
+                self.quit(1)
 
         dest_jids=[]
         try:
             for i in range(len(jids)):
-                dest_jids.append(expandJid(jids[i]))
+                dest_jids.append(expand_jid(jids[i]))
                 check(dest_jids[i])
         except AttributeError:
             pass
 
         return dest_jids
 
-    def check_jabber_connection(self):
-        """Check that jabber status is allright"""
-        def cantConnect(arg):
-            print arg
+    def connect_profile(self, callback):
+        """ Check if the profile is connected
+        @param callback: method to call when profile is connected
+        @exit: - 1 when profile is not connected and --connect is not set
+               - 1 when the profile doesn't exists
+               - 1 when there is a connection error
+        """
+        # FIXME: need better exit codes
+
+        def cant_connect():
             error(_(u"Can't connect profile"))
-            exit(1)
+            self.quit(1)
 
-        self.profile = self.bridge.getProfileName(self.profile_name)
-        if not self.profile:
-            error(_("The profile asked doesn't exist"))
-            exit(1)
+        self.profile = self.bridge.getProfileName(self.args.profile)
 
-        if self.bridge.isConnected(self.profile):
-            print "Already connected"
-        else:
-            self._start_loop = True
-            self.bridge.asyncConnect(self.profile, self._run, cantConnect)
+        if not self.profile:
+            error(_("The profile [%s] doesn't exist") % self.args.profile)
+            self.quit(1)
+
+        if self.args.connect: #if connection is asked, we connect the profile
+            self.bridge.asyncConnect(self.profile, callback, cant_connect)
+            self._auto_loop = True
             return
-        self.run()
 
+        elif not self.bridge.isConnected(self.profile):
+            error(_(u"Profile [%(profile)s] is not connected, please connect it before using jp, or use --connect option") % { "profile": self.profile })
+            self.quit(1)
 
-    def _getFullJid(self, param_jid):
-        """Return the full jid if possible (add last resource when find a bare jid"""
+        callback()
+
+    def get_full_jid(self, param_jid):
+        """Return the full jid if possible (add last resource when find a bare jid)"""
         _jid = JID(param_jid)
         if not _jid.resource:
             #if the resource is not given, we try to add the last known resource
-            last_resource = self.bridge.getLastResource(param_jid, self.profile_name)
+            last_resource = self.bridge.getLastResource(param_jid, self.profile)
             if last_resource:
                 return "%s/%s" % (_jid.bare, last_resource)
         return param_jid
 
-    def go(self):
-        self.check_jabber_connection()
-        if self._start_loop:
-            self._loop_start()
+    def watch_progress(self):
+        self.pbar = None
+        gobject.timeout_add(10, self._progress_cb)
 
+    def _progress_cb(self):
+        if self.progress_id:
+            data = self.bridge.getProgress(self.progress_id, self.profile)
+            if data:
+                if not data['position']:
+                    data['position'] = '0'
+                if not self.pbar:
+                    #first answer, we must construct the bar
+                    self.pbar = progressbar.ProgressBar(int(data['size']),
+                                                        [_("Progress: "),progressbar.Percentage(),
+                                                         " ",
+                                                         progressbar.Bar(),
+                                                         " ",
+                                                         progressbar.FileTransferSpeed(),
+                                                         " ",
+                                                         progressbar.ETA()])
+                    self.pbar.start()
+
+                self.pbar.update(int(data['position']))
+
+            elif self.pbar:
+                self.pbar.finish()
+                if self.quit_on_progress_end:
+                    self.quit()
+                return False
+
+        return True
 
 
-class JPAsk(JPWithProfile):
-    def confirm_type(self):
-        """Must return a string containing the confirm type. For instance,
-        FILE_TRANSFER or PIPE_TRANSFER, etc.
+class CommandBase(object):
 
-        :rtype: str
+    def __init__(self, host, name, use_profile=True, use_progress=False, help=None, **kwargs):
+        """ Initialise CommandBase
+        @param host: Jp instance
+        @param name: name of the new command
+        @param use_profile: if True, add profile selection/connection commands
+        @param use_progress: if True, add progress bar activation commands
+        @param help: help message to display
+        @param **kwargs: args passed to ArgumentParser
+
         """
-        raise NotImplemented
+        try: # If we have subcommands, host is a CommandBase and we need to use host.host
+            self.host = host.host
+        except AttributeError:
+            self.host = host
+
+        parents = kwargs.setdefault('parents', set())
+        if use_profile:
+            #self.host.parents['profile'] is an ArgumentParser with profile connection arguments
+            parents.add(self.host.parents['profile'])
+        if use_progress:
+            parents.add(self.host.parents['progress'])
+
+        self.parser = host.subparsers.add_parser(name, help=help, **kwargs)
+        if hasattr(self, "subcommands"):
+            self.subparsers = self.parser.add_subparsers()
+        else:
+            self.parser.set_defaults(func=self.run)
+        self.add_parser_options()
+
+    @property
+    def args(self):
+        return self.host.args
+
+    @property
+    def need_loop(self):
+        return self.host.need_loop
+
+    @need_loop.setter
+    def need_loop(self, value):
+        self.host.need_loop = value
+
+    @property
+    def profile(self):
+        return self.host.profile
+
+    @property
+    def progress_id(self):
+        return self.host.progress_id
 
-    def dest_jids(self):
-        return None
+    @progress_id.setter
+    def progress_id(self, value):
+        self.host.progress_id = value
+
+    def add_parser_options(self):
+        try:
+            subcommands = self.subcommands
+        except AttributeError:
+            # We don't have subcommands, the class need to implements add_parser_options
+            raise NotImplementedError
+
+        # now we add subcommands to ourself
+        for cls in subcommands:
+            cls(self)
 
-    def _askConfirmation(self, confirm_id, confirm_type, data, profile):
+    def run(self):
+        try:
+            if self.args.profile:
+                self.host.connect_profile(self.connected)
+        except AttributeError:
+            # the command doesn't need to connect profile
+            pass
+        try:
+            if self.args.progress:
+                self.host.watch_progress()
+        except AttributeError:
+            # the command doesn't use progress bar
+            pass
+
+    def connected(self):
+        if not self.need_loop:
+            self.host.stop_loop()
+
+
+class CommandAnswering(CommandBase):
+    #FIXME: temp, will be refactored when progress_bar/confirmations will be refactored
+
+    def _ask_confirmation(self, confirm_id, confirm_type, data, profile):
+        """ Callback used for file transfer, accept files depending on parameters"""
         if profile != self.profile:
             debug("Ask confirmation ignored: not our profile")
             return
-        if confirm_type == self.confirm_type():
-            self._confirm_id = confirm_id
-            if self.dest_jids() and not JID(data['from']).bare in [JID(_jid).bare for _jid in self.dest_jids()]:
+        if confirm_type == self.confirm_type:
+            if self.dest_jids and not JID(data['from']).bare in [JID(_jid).bare for _jid in self.dest_jids()]:
                 return #file is not sent by a filtered jid
             else:
-                self.ask(data)
+                self.ask(data, confirm_id)
 
     def ask(self):
         """
         The return value is used to answer to the bridge.
-        :rtype: (bool, dict)
+        @return: bool or dict
         """
         raise NotImplementedError
 
-    def answer(self, accepted, answer_data):
-        """
-        :param accepted: boolean
-        :param aswer_data: dict of answer datas
-        """
-        self.bridge.confirmationAnswer(self._confirm_id, False, answer_data, self.profile)
-
-    def run(self):
+    def connected(self):
         """Auto reply to confirmations requests"""
-        #we register incoming confirmation
-        self.bridge.register("askConfirmation", self._askConfirmation)
+        self.need_loop = True
+        super(CommandAnswering, self).connected()
+        # we watch confirmation signals
+        self.host.bridge.register("ask_confirmation", self._ask_confirmation)
 
         #and we ask those we have missed
-        for confirm_id, confirm_type, data in self.bridge.getWaitingConf(self.profile):
-            self._askConfirmation(confirm_id, confirm_type, data, self.profile)
-
-
+        for confirm_id, confirm_type, data in self.host.bridge.getWaitingConf(self.profile):
+            self._ask_confirmation(confirm_id, confirm_type, data, self.profile)