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
This commit is contained in:
Goffi 2014-02-10 13:44:09 +01:00
commit 7ca3c657f4

View file

@ -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 @@ logging.basicConfig(level=logging.DEBUG,
###
import sys
import os
from os.path import abspath, basename, dirname
from argparse import ArgumentParser
from sat.tools.jid import JID
import locale
import os.path
import argparse
import gobject
from glob import iglob
from importlib import import_module
from sat.tools.jid import JID
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 @@ class JP(object):
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)
def run(self):
raise NotImplementedError
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
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()
@property
def version(self):
return self.bridge.getVersion()
def _loop_start(self):
@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 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 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 stop_loop(self):
try:
self.loop.quit()
except AttributeError:
pass
def go(self):
self.run()
if self._start_loop:
self._loop_start()
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 @@ class JPWithProfile(JP):
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 @@ class JPWithProfile(JP):
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.args.profile)
self.profile = self.bridge.getProfileName(self.profile_name)
if not self.profile:
error(_("The profile asked doesn't exist"))
exit(1)
error(_("The profile [%s] doesn't exist") % self.args.profile)
self.quit(1)
if self.bridge.isConnected(self.profile):
print "Already connected"
else:
self._start_loop = True
self.bridge.asyncConnect(self.profile, self._run, cantConnect)
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 CommandBase(object):
class JPAsk(JPWithProfile):
def confirm_type(self):
"""Must return a string containing the confirm type. For instance,
FILE_TRANSFER or PIPE_TRANSFER, etc.
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
:rtype: str
"""
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
def dest_jids(self):
return None
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'])
def _askConfirmation(self, confirm_id, confirm_type, data, profile):
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
@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 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)