Mercurial > libervia-backend
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)