comparison sat_frontends/jp/base.py @ 3028:ab2696e34d29

Python 3 port: /!\ this is a huge commit /!\ starting from this commit, SàT is needs Python 3.6+ /!\ SàT maybe be instable or some feature may not work anymore, this will improve with time This patch port backend, bridge and frontends to Python 3. Roughly this has been done this way: - 2to3 tools has been applied (with python 3.7) - all references to python2 have been replaced with python3 (notably shebangs) - fixed files not handled by 2to3 (notably the shell script) - several manual fixes - fixed issues reported by Python 3 that where not handled in Python 2 - replaced "async" with "async_" when needed (it's a reserved word from Python 3.7) - replaced zope's "implements" with @implementer decorator - temporary hack to handle data pickled in database, as str or bytes may be returned, to be checked later - fixed hash comparison for password - removed some code which is not needed anymore with Python 3 - deactivated some code which needs to be checked (notably certificate validation) - tested with jp, fixed reported issues until some basic commands worked - ported Primitivus (after porting dependencies like urwid satext) - more manual fixes
author Goffi <goffi@goffi.org>
date Tue, 13 Aug 2019 19:08:41 +0200
parents b2f323237fce
children fee60f17ebac
comparison
equal deleted inserted replaced
3027:ff5bcb12ae60 3028:ab2696e34d29
76 @param *args: args of the callbac 76 @param *args: args of the callbac
77 """ 77 """
78 GLib.timeout_add(delay, callback, *args) 78 GLib.timeout_add(delay, callback, *args)
79 79
80 else: 80 else:
81 print u"can't start jp: only D-Bus bridge is currently handled" 81 print("can't start jp: only D-Bus bridge is currently handled")
82 sys.exit(C.EXIT_ERROR) 82 sys.exit(C.EXIT_ERROR)
83 # FIXME: twisted loop can be used when jp can handle fully async bridges 83 # FIXME: twisted loop can be used when jp can handle fully async bridges
84 # from twisted.internet import reactor 84 # from twisted.internet import reactor
85 85
86 # class JPLoop(object): 86 # class JPLoop(object):
102 102
103 if bridge_name == "embedded": 103 if bridge_name == "embedded":
104 from sat.core import sat_main 104 from sat.core import sat_main
105 sat = sat_main.SAT() 105 sat = sat_main.SAT()
106 106
107 if sys.version_info < (2, 7, 3):
108 # XXX: shlex.split only handle unicode since python 2.7.3
109 # this is a workaround for older versions
110 old_split = shlex.split
111 new_split = (lambda s, *a, **kw: [t.decode('utf-8') for t in old_split(s.encode('utf-8'), *a, **kw)]
112 if isinstance(s, unicode) else old_split(s, *a, **kw))
113 shlex.split = new_split
114
115 try: 107 try:
116 import progressbar 108 import progressbar
117 except ImportError: 109 except ImportError:
118 msg = (_(u'ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar\n') + 110 msg = (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar\n') +
119 _(u'Progress bar deactivated\n--\n')) 111 _('Progress bar deactivated\n--\n'))
120 print >>sys.stderr,msg.encode('utf-8') 112 print(msg.encode('utf-8'), file=sys.stderr)
121 progressbar=None 113 progressbar=None
122 114
123 #consts 115 #consts
124 PROG_NAME = u"jp" 116 PROG_NAME = "jp"
125 DESCRIPTION = """This software is a command line tool for XMPP. 117 DESCRIPTION = """This software is a command line tool for XMPP.
126 Get the latest version at """ + C.APP_URL 118 Get the latest version at """ + C.APP_URL
127 119
128 COPYLEFT = u"""Copyright (C) 2009-2019 Jérôme Poisson, Adrien Cossa 120 COPYLEFT = """Copyright (C) 2009-2019 Jérôme Poisson, Adrien Cossa
129 This program comes with ABSOLUTELY NO WARRANTY; 121 This program comes with ABSOLUTELY NO WARRANTY;
130 This is free software, and you are welcome to redistribute it under certain conditions. 122 This is free software, and you are welcome to redistribute it under certain conditions.
131 """ 123 """
132 124
133 PROGRESS_DELAY = 10 # the progression will be checked every PROGRESS_DELAY ms 125 PROGRESS_DELAY = 10 # the progression will be checked every PROGRESS_DELAY ms
134
135
136 def unicode_decoder(arg):
137 # Needed to have unicode strings from arguments
138 return arg.decode(locale.getpreferredencoding())
139 126
140 127
141 def date_decoder(arg): 128 def date_decoder(arg):
142 return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL) 129 return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL)
143 130
165 """ 152 """
166 # FIXME: need_loop should be removed, everything must be async in bridge so 153 # FIXME: need_loop should be removed, everything must be async in bridge so
167 # loop will always be needed 154 # loop will always be needed
168 bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge') 155 bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge')
169 if bridge_module is None: 156 if bridge_module is None:
170 log.error(u"Can't import {} bridge".format(bridge_name)) 157 log.error("Can't import {} bridge".format(bridge_name))
171 sys.exit(1) 158 sys.exit(1)
172 159
173 self.bridge = bridge_module.Bridge() 160 self.bridge = bridge_module.Bridge()
174 self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) 161 self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb)
175 162
176 def _bridgeCb(self): 163 def _bridgeCb(self):
177 self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, 164 self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
178 description=DESCRIPTION) 165 description=DESCRIPTION)
179 self._make_parents() 166 self._make_parents()
180 self.add_parser_options() 167 self.add_parser_options()
181 self.subparsers = self.parser.add_subparsers(title=_(u'Available commands'), dest='subparser_name') 168 self.subparsers = self.parser.add_subparsers(title=_('Available commands'), dest='subparser_name')
182 self._auto_loop = False # when loop is used for internal reasons 169 self._auto_loop = False # when loop is used for internal reasons
183 self._need_loop = False 170 self._need_loop = False
184 171
185 # progress attributes 172 # progress attributes
186 self._progress_id = None # TODO: manage several progress ids 173 self._progress_id = None # TODO: manage several progress ids
194 181
195 self.own_jid = None # must be filled at runtime if needed 182 self.own_jid = None # must be filled at runtime if needed
196 183
197 def _bridgeEb(self, failure): 184 def _bridgeEb(self, failure):
198 if isinstance(failure, exceptions.BridgeExceptionNoService): 185 if isinstance(failure, exceptions.BridgeExceptionNoService):
199 print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) 186 print((_("Can't connect to SàT backend, are you sure it's launched ?")))
200 elif isinstance(failure, exceptions.BridgeInitError): 187 elif isinstance(failure, exceptions.BridgeInitError):
201 print(_(u"Can't init bridge")) 188 print((_("Can't init bridge")))
202 else: 189 else:
203 print(_(u"Error while initialising bridge: {}".format(failure))) 190 print((_("Error while initialising bridge: {}".format(failure))))
204 sys.exit(C.EXIT_BRIDGE_ERROR) 191 sys.exit(C.EXIT_BRIDGE_ERROR)
205 192
206 @property 193 @property
207 def version(self): 194 def version(self):
208 return self.bridge.getVersion() 195 return self.bridge.getVersion()
262 @param no_lf(bool): if True, do not emit line feed at the end of line 249 @param no_lf(bool): if True, do not emit line feed at the end of line
263 """ 250 """
264 if self.verbosity >= verbosity: 251 if self.verbosity >= verbosity:
265 if error: 252 if error:
266 if no_lf: 253 if no_lf:
267 print >>sys.stderr,msg.encode('utf-8'), 254 print(msg, end=' ', file=sys.stderr)
268 else: 255 else:
269 print >>sys.stderr,msg.encode('utf-8') 256 print(msg, file=sys.stderr)
270 else: 257 else:
271 if no_lf: 258 if no_lf:
272 print msg.encode('utf-8'), 259 print(msg, end=' ')
273 else: 260 else:
274 print msg.encode('utf-8') 261 print(msg)
275 262
276 def output(self, type_, name, extra_outputs, data): 263 def output(self, type_, name, extra_outputs, data):
277 if name in extra_outputs: 264 if name in extra_outputs:
278 extra_outputs[name](data) 265 extra_outputs[name](data)
279 else: 266 else:
295 """Return valid output filters for output_type 282 """Return valid output filters for output_type
296 283
297 @param output_type: True for default, 284 @param output_type: True for default,
298 else can be any registered type 285 else can be any registered type
299 """ 286 """
300 return self._outputs[output_type].keys() 287 return list(self._outputs[output_type].keys())
301 288
302 def _make_parents(self): 289 def _make_parents(self):
303 self.parents = {} 290 self.parents = {}
304 291
305 # we have a special case here as the start-session option is present only if connection is not needed, 292 # we have a special case here as the start-session option is present only if connection is not needed,
306 # so we create two similar parents, one with the option, the other one without it 293 # so we create two similar parents, one with the option, the other one without it
307 for parent_name in ('profile', 'profile_session'): 294 for parent_name in ('profile', 'profile_session'):
308 parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False) 295 parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False)
309 parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)")) 296 parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)"))
310 parent.add_argument("--pwd", action="store", type=unicode_decoder, default='', metavar='PASSWORD', help=_("Password used to connect profile, if necessary")) 297 parent.add_argument("--pwd", action="store", default='', metavar='PASSWORD', help=_("Password used to connect profile, if necessary"))
311 298
312 profile_parent, profile_session_parent = self.parents['profile'], self.parents['profile_session'] 299 profile_parent, profile_session_parent = self.parents['profile'], self.parents['profile_session']
313 300
314 connect_short, connect_long, connect_action, connect_help = "-c", "--connect", "store_true", _(u"Connect the profile before doing anything else") 301 connect_short, connect_long, connect_action, connect_help = "-c", "--connect", "store_true", _("Connect the profile before doing anything else")
315 profile_parent.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) 302 profile_parent.add_argument(connect_short, connect_long, action=connect_action, help=connect_help)
316 303
317 profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group() 304 profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group()
318 profile_session_connect_group.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) 305 profile_session_connect_group.add_argument(connect_short, connect_long, action=connect_action, help=connect_help)
319 profile_session_connect_group.add_argument("--start-session", action="store_true", help=_("Start a profile session without connecting")) 306 profile_session_connect_group.add_argument("--start-session", action="store_true", help=_("Start a profile session without connecting"))
321 progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False) 308 progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False)
322 if progressbar: 309 if progressbar:
323 progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar")) 310 progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar"))
324 311
325 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) 312 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False)
326 verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_(u"Add a verbosity level (can be used multiple times)")) 313 verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_("Add a verbosity level (can be used multiple times)"))
327 314
328 draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False) 315 draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False)
329 draft_group = draft_parent.add_argument_group(_('draft handling')) 316 draft_group = draft_parent.add_argument_group(_('draft handling'))
330 draft_group.add_argument("-D", "--current", action="store_true", help=_(u"load current draft")) 317 draft_group.add_argument("-D", "--current", action="store_true", help=_("load current draft"))
331 draft_group.add_argument("-F", "--draft-path", type=unicode_decoder, help=_(u"path to a draft file to retrieve")) 318 draft_group.add_argument("-F", "--draft-path", help=_("path to a draft file to retrieve"))
332 319
333 320
334 def make_pubsub_group(self, flags, defaults): 321 def make_pubsub_group(self, flags, defaults):
335 """generate pubsub options according to flags 322 """generate pubsub options according to flags
336 323
341 @return (ArgumentParser): parser to add 328 @return (ArgumentParser): parser to add
342 """ 329 """
343 flags = misc.FlagsHandler(flags) 330 flags = misc.FlagsHandler(flags)
344 parent = argparse.ArgumentParser(add_help=False) 331 parent = argparse.ArgumentParser(add_help=False)
345 pubsub_group = parent.add_argument_group('pubsub') 332 pubsub_group = parent.add_argument_group('pubsub')
346 pubsub_group.add_argument("-u", "--pubsub-url", type=unicode_decoder, 333 pubsub_group.add_argument("-u", "--pubsub-url",
347 help=_(u"Pubsub URL (xmpp or http)")) 334 help=_("Pubsub URL (xmpp or http)"))
348 335
349 service_help = _(u"JID of the PubSub service") 336 service_help = _("JID of the PubSub service")
350 if not flags.service: 337 if not flags.service:
351 default = defaults.pop(u'service', _(u'PEP service')) 338 default = defaults.pop('service', _('PEP service'))
352 if default is not None: 339 if default is not None:
353 service_help += _(u" (DEFAULT: {default})".format(default=default)) 340 service_help += _(" (DEFAULT: {default})".format(default=default))
354 pubsub_group.add_argument("-s", "--service", type=unicode_decoder, default=u'', 341 pubsub_group.add_argument("-s", "--service", default='',
355 help=service_help) 342 help=service_help)
356 343
357 node_help = _(u"node to request") 344 node_help = _("node to request")
358 if not flags.node: 345 if not flags.node:
359 default = defaults.pop(u'node', _(u'standard node')) 346 default = defaults.pop('node', _('standard node'))
360 if default is not None: 347 if default is not None:
361 node_help += _(u" (DEFAULT: {default})".format(default=default)) 348 node_help += _(" (DEFAULT: {default})".format(default=default))
362 pubsub_group.add_argument("-n", "--node", type=unicode_decoder, default=u'', help=node_help) 349 pubsub_group.add_argument("-n", "--node", default='', help=node_help)
363 350
364 if flags.single_item: 351 if flags.single_item:
365 item_help = (u"item to retrieve") 352 item_help = ("item to retrieve")
366 if not flags.item: 353 if not flags.item:
367 default = defaults.pop(u'item', _(u'last item')) 354 default = defaults.pop('item', _('last item'))
368 if default is not None: 355 if default is not None:
369 item_help += _(u" (DEFAULT: {default})".format(default=default)) 356 item_help += _(" (DEFAULT: {default})".format(default=default))
370 pubsub_group.add_argument("-i", "--item", type=unicode_decoder, default=u'', 357 pubsub_group.add_argument("-i", "--item", default='',
371 help=item_help) 358 help=item_help)
372 pubsub_group.add_argument("-L", "--last-item", action='store_true', help=_(u'retrieve last item')) 359 pubsub_group.add_argument("-L", "--last-item", action='store_true', help=_('retrieve last item'))
373 elif flags.multi_items: 360 elif flags.multi_items:
374 # mutiple items, this activate several features: max-items, RSM, MAM 361 # mutiple items, this activate several features: max-items, RSM, MAM
375 # and Orbder-by 362 # and Orbder-by
376 pubsub_group.add_argument("-i", "--item", type=unicode_decoder, action='append', dest='items', default=[], help=_(u"items to retrieve (DEFAULT: all)")) 363 pubsub_group.add_argument("-i", "--item", action='append', dest='items', default=[], help=_("items to retrieve (DEFAULT: all)"))
377 if not flags.no_max: 364 if not flags.no_max:
378 max_group = pubsub_group.add_mutually_exclusive_group() 365 max_group = pubsub_group.add_mutually_exclusive_group()
379 # XXX: defaut value for --max-items or --max is set in parse_pubsub_args 366 # XXX: defaut value for --max-items or --max is set in parse_pubsub_args
380 max_group.add_argument( 367 max_group.add_argument(
381 "-M", "--max-items", dest="max", type=int, 368 "-M", "--max-items", dest="max", type=int,
382 help=_(u"maximum number of items to get ({no_limit} to get all items)" 369 help=_("maximum number of items to get ({no_limit} to get all items)"
383 .format(no_limit=C.NO_LIMIT))) 370 .format(no_limit=C.NO_LIMIT)))
384 # FIXME: it could be possible to no duplicate max (between pubsub 371 # FIXME: it could be possible to no duplicate max (between pubsub
385 # max-items and RSM max)should not be duplicated, RSM could be 372 # max-items and RSM max)should not be duplicated, RSM could be
386 # used when available and pubsub max otherwise 373 # used when available and pubsub max otherwise
387 max_group.add_argument( 374 max_group.add_argument(
388 "-m", "--max", dest="rsm_max", type=int, 375 "-m", "--max", dest="rsm_max", type=int,
389 help=_(u"maximum number of items to get per page (DEFAULT: 10)")) 376 help=_("maximum number of items to get per page (DEFAULT: 10)"))
390 377
391 # RSM 378 # RSM
392 379
393 rsm_page_group = pubsub_group.add_mutually_exclusive_group() 380 rsm_page_group = pubsub_group.add_mutually_exclusive_group()
394 rsm_page_group.add_argument( 381 rsm_page_group.add_argument(
395 "-a", "--after", dest="rsm_after", type=unicode_decoder, 382 "-a", "--after", dest="rsm_after",
396 help=_(u"find page after this item"), metavar='ITEM_ID') 383 help=_("find page after this item"), metavar='ITEM_ID')
397 rsm_page_group.add_argument( 384 rsm_page_group.add_argument(
398 "-b", "--before", dest="rsm_before", type=unicode_decoder, 385 "-b", "--before", dest="rsm_before",
399 help=_(u"find page before this item"), metavar='ITEM_ID') 386 help=_("find page before this item"), metavar='ITEM_ID')
400 rsm_page_group.add_argument( 387 rsm_page_group.add_argument(
401 "--index", dest="rsm_index", type=int, 388 "--index", dest="rsm_index", type=int,
402 help=_(u"index of the page to retrieve")) 389 help=_("index of the page to retrieve"))
403 390
404 391
405 # MAM 392 # MAM
406 393
407 pubsub_group.add_argument( 394 pubsub_group.add_argument(
408 "-f", "--filter", dest='mam_filters', type=unicode_decoder, nargs=2, 395 "-f", "--filter", dest='mam_filters', nargs=2,
409 action='append', default=[], help=_(u"MAM filters to use"), 396 action='append', default=[], help=_("MAM filters to use"),
410 metavar=(u"FILTER_NAME", u"VALUE") 397 metavar=("FILTER_NAME", "VALUE")
411 ) 398 )
412 399
413 # Order-By 400 # Order-By
414 401
415 # TODO: order-by should be a list to handle several levels of ordering 402 # TODO: order-by should be a list to handle several levels of ordering
417 # current specifications, as only "creation" and "modification" are 404 # current specifications, as only "creation" and "modification" are
418 # available) 405 # available)
419 pubsub_group.add_argument( 406 pubsub_group.add_argument(
420 "-o", "--order-by", choices=[C.ORDER_BY_CREATION, 407 "-o", "--order-by", choices=[C.ORDER_BY_CREATION,
421 C.ORDER_BY_MODIFICATION], 408 C.ORDER_BY_MODIFICATION],
422 help=_(u"how items should be ordered")) 409 help=_("how items should be ordered"))
423 410
424 if not flags.all_used: 411 if not flags.all_used:
425 raise exceptions.InternalError('unknown flags: {flags}'.format(flags=u', '.join(flags.unused))) 412 raise exceptions.InternalError('unknown flags: {flags}'.format(flags=', '.join(flags.unused)))
426 if defaults: 413 if defaults:
427 raise exceptions.InternalError('unused defaults: {defaults}'.format(defaults=defaults)) 414 raise exceptions.InternalError('unused defaults: {defaults}'.format(defaults=defaults))
428 415
429 return parent 416 return parent
430 417
431 def add_parser_options(self): 418 def add_parser_options(self):
432 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT})) 419 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT}))
433 420
434 def register_output(self, type_, name, callback, description="", default=False): 421 def register_output(self, type_, name, callback, description="", default=False):
435 if type_ not in C.OUTPUT_TYPES: 422 if type_ not in C.OUTPUT_TYPES:
436 log.error(u"Invalid output type {}".format(type_)) 423 log.error("Invalid output type {}".format(type_))
437 return 424 return
438 self._outputs[type_][name] = {'callback': callback, 425 self._outputs[type_][name] = {'callback': callback,
439 'description': description 426 'description': description
440 } 427 }
441 if default: 428 if default:
442 if type_ in self.default_output: 429 if type_ in self.default_output:
443 self.disp(_(u'there is already a default output for {}, ignoring new one').format(type_)) 430 self.disp(_('there is already a default output for {}, ignoring new one').format(type_))
444 else: 431 else:
445 self.default_output[type_] = name 432 self.default_output[type_] = name
446 433
447 434
448 def parse_output_options(self): 435 def parse_output_options(self):
449 options = self.command.args.output_opts 436 options = self.command.args.output_opts
450 options_dict = {} 437 options_dict = {}
451 for option in options: 438 for option in options:
452 try: 439 try:
453 key, value = option.split(u'=', 1) 440 key, value = option.split('=', 1)
454 except ValueError: 441 except ValueError:
455 key, value = option, None 442 key, value = option, None
456 options_dict[key.strip()] = value.strip() if value is not None else None 443 options_dict[key.strip()] = value.strip() if value is not None else None
457 return options_dict 444 return options_dict
458 445
459 def check_output_options(self, accepted_set, options): 446 def check_output_options(self, accepted_set, options):
460 if not accepted_set.issuperset(options): 447 if not accepted_set.issuperset(options):
461 self.disp(u"The following output options are invalid: {invalid_options}".format( 448 self.disp("The following output options are invalid: {invalid_options}".format(
462 invalid_options = u', '.join(set(options).difference(accepted_set))), 449 invalid_options = ', '.join(set(options).difference(accepted_set))),
463 error=True) 450 error=True)
464 self.quit(C.EXIT_BAD_ARG) 451 self.quit(C.EXIT_BAD_ARG)
465 452
466 def import_plugins(self): 453 def import_plugins(self):
467 """Automaticaly import commands and outputs in jp 454 """Automaticaly import commands and outputs in jp
476 module_path = "sat_frontends.jp." + module_name 463 module_path = "sat_frontends.jp." + module_name
477 try: 464 try:
478 module = import_module(module_path) 465 module = import_module(module_path)
479 self.import_plugin_module(module, type_) 466 self.import_plugin_module(module, type_)
480 except ImportError as e: 467 except ImportError as e:
481 self.disp(_(u"Can't import {module_path} plugin, ignoring it: {msg}".format( 468 self.disp(_("Can't import {module_path} plugin, ignoring it: {msg}".format(
482 module_path = module_path, 469 module_path = module_path,
483 msg = e)), error=True) 470 msg = e)), error=True)
484 except exceptions.CancelError: 471 except exceptions.CancelError:
485 continue 472 continue
486 except exceptions.MissingModule as e: 473 except exceptions.MissingModule as e:
487 self.disp(_(u"Missing module for plugin {name}: {missing}".format( 474 self.disp(_("Missing module for plugin {name}: {missing}".format(
488 name = module_path, 475 name = module_path,
489 missing = e)), error=True) 476 missing = e)), error=True)
490 477
491 478
492 def import_plugin_module(self, module, type_): 479 def import_plugin_module(self, module, type_):
496 @param type_(str): one of C_PLUGIN_* 483 @param type_(str): one of C_PLUGIN_*
497 """ 484 """
498 try: 485 try:
499 class_names = getattr(module, '__{}__'.format(type_)) 486 class_names = getattr(module, '__{}__'.format(type_))
500 except AttributeError: 487 except AttributeError:
501 log.disp(_(u"Invalid plugin module [{type}] {module}").format(type=type_, module=module), error=True) 488 log.disp(_("Invalid plugin module [{type}] {module}").format(type=type_, module=module), error=True)
502 raise ImportError 489 raise ImportError
503 else: 490 else:
504 for class_name in class_names: 491 for class_name in class_names:
505 cls = getattr(module, class_name) 492 cls = getattr(module, class_name)
506 cls(self) 493 cls(self)
507 494
508 def get_xmpp_uri_from_http(self, http_url): 495 def get_xmpp_uri_from_http(self, http_url):
509 """parse HTML page at http(s) URL, and looks for xmpp: uri""" 496 """parse HTML page at http(s) URL, and looks for xmpp: uri"""
510 if http_url.startswith('https'): 497 if http_url.startswith('https'):
511 scheme = u'https' 498 scheme = 'https'
512 elif http_url.startswith('http'): 499 elif http_url.startswith('http'):
513 scheme = u'http' 500 scheme = 'http'
514 else: 501 else:
515 raise exceptions.InternalError(u'An HTTP scheme is expected in this method') 502 raise exceptions.InternalError('An HTTP scheme is expected in this method')
516 self.disp(u"{scheme} URL found, trying to find associated xmpp: URI".format(scheme=scheme.upper()),1) 503 self.disp("{scheme} URL found, trying to find associated xmpp: URI".format(scheme=scheme.upper()),1)
517 # HTTP URL, we try to find xmpp: links 504 # HTTP URL, we try to find xmpp: links
518 try: 505 try:
519 from lxml import etree 506 from lxml import etree
520 except ImportError: 507 except ImportError:
521 self.disp(u"lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True) 508 self.disp("lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True)
522 self.quit(1) 509 self.quit(1)
523 import urllib2 510 import urllib.request, urllib.error, urllib.parse
524 parser = etree.HTMLParser() 511 parser = etree.HTMLParser()
525 try: 512 try:
526 root = etree.parse(urllib2.urlopen(http_url), parser) 513 root = etree.parse(urllib.request.urlopen(http_url), parser)
527 except etree.XMLSyntaxError as e: 514 except etree.XMLSyntaxError as e:
528 self.disp(_(u"Can't parse HTML page : {msg}").format(msg=e)) 515 self.disp(_("Can't parse HTML page : {msg}").format(msg=e))
529 links = [] 516 links = []
530 else: 517 else:
531 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]") 518 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]")
532 if not links: 519 if not links:
533 self.disp(u'Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True) 520 self.disp('Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True)
534 self.quit(1) 521 self.quit(1)
535 xmpp_uri = links[0].get('href') 522 xmpp_uri = links[0].get('href')
536 return xmpp_uri 523 return xmpp_uri
537 524
538 def parse_pubsub_args(self): 525 def parse_pubsub_args(self):
544 url = self.get_xmpp_uri_from_http(url) 531 url = self.get_xmpp_uri_from_http(url)
545 532
546 try: 533 try:
547 uri_data = uri.parseXMPPUri(url) 534 uri_data = uri.parseXMPPUri(url)
548 except ValueError: 535 except ValueError:
549 self.parser.error(_(u'invalid XMPP URL: {url}').format(url=url)) 536 self.parser.error(_('invalid XMPP URL: {url}').format(url=url))
550 else: 537 else:
551 if uri_data[u'type'] == 'pubsub': 538 if uri_data['type'] == 'pubsub':
552 # URL is alright, we only set data not already set by other options 539 # URL is alright, we only set data not already set by other options
553 if not self.args.service: 540 if not self.args.service:
554 self.args.service = uri_data[u'path'] 541 self.args.service = uri_data['path']
555 if not self.args.node: 542 if not self.args.node:
556 self.args.node = uri_data[u'node'] 543 self.args.node = uri_data['node']
557 uri_item = uri_data.get(u'item') 544 uri_item = uri_data.get('item')
558 if uri_item: 545 if uri_item:
559 # there is an item in URI 546 # there is an item in URI
560 # we use it only if item is not already set 547 # we use it only if item is not already set
561 # and item_last is not used either 548 # and item_last is not used either
562 try: 549 try:
563 item = self.args.item 550 item = self.args.item
564 except AttributeError: 551 except AttributeError:
565 try: 552 try:
566 items = self.args.items 553 items = self.args.items
567 except AttributeError: 554 except AttributeError:
568 self.disp(_(u"item specified in URL but not needed in command, ignoring it"), error=True) 555 self.disp(_("item specified in URL but not needed in command, ignoring it"), error=True)
569 else: 556 else:
570 if not items: 557 if not items:
571 self.args.items = [uri_item] 558 self.args.items = [uri_item]
572 else: 559 else:
573 if not item: 560 if not item:
576 except AttributeError: 563 except AttributeError:
577 item_last = False 564 item_last = False
578 if not item_last: 565 if not item_last:
579 self.args.item = uri_item 566 self.args.item = uri_item
580 else: 567 else:
581 self.parser.error(_(u'XMPP URL is not a pubsub one: {url}').format(url=url)) 568 self.parser.error(_('XMPP URL is not a pubsub one: {url}').format(url=url))
582 flags = self.args._cmd._pubsub_flags 569 flags = self.args._cmd._pubsub_flags
583 # we check required arguments here instead of using add_arguments' required option 570 # we check required arguments here instead of using add_arguments' required option
584 # because the required argument can be set in URL 571 # because the required argument can be set in URL
585 if C.SERVICE in flags and not self.args.service: 572 if C.SERVICE in flags and not self.args.service:
586 self.parser.error(_(u"argument -s/--service is required")) 573 self.parser.error(_("argument -s/--service is required"))
587 if C.NODE in flags and not self.args.node: 574 if C.NODE in flags and not self.args.node:
588 self.parser.error(_(u"argument -n/--node is required")) 575 self.parser.error(_("argument -n/--node is required"))
589 if C.ITEM in flags and not self.args.item: 576 if C.ITEM in flags and not self.args.item:
590 self.parser.error(_(u"argument -i/--item is required")) 577 self.parser.error(_("argument -i/--item is required"))
591 578
592 # FIXME: mutually groups can't be nested in a group and don't support title 579 # FIXME: mutually groups can't be nested in a group and don't support title
593 # so we check conflict here. This may be fixed in Python 3, to be checked 580 # so we check conflict here. This may be fixed in Python 3, to be checked
594 try: 581 try:
595 if self.args.item and self.args.item_last: 582 if self.args.item and self.args.item_last:
596 self.parser.error(_(u"--item and --item-last can't be used at the same time")) 583 self.parser.error(_("--item and --item-last can't be used at the same time"))
597 except AttributeError: 584 except AttributeError:
598 pass 585 pass
599 586
600 try: 587 try:
601 max_items = self.args.max 588 max_items = self.args.max
637 try: 624 try:
638 self.loop.quit() 625 self.loop.quit()
639 except AttributeError: 626 except AttributeError:
640 pass 627 pass
641 628
642 def confirmOrQuit(self, message, cancel_message=_(u"action cancelled by user")): 629 def confirmOrQuit(self, message, cancel_message=_("action cancelled by user")):
643 """Request user to confirm action, and quit if he doesn't""" 630 """Request user to confirm action, and quit if he doesn't"""
644 631
645 res = raw_input("{} (y/N)? ".format(message)) 632 res = input("{} (y/N)? ".format(message))
646 if res not in ("y", "Y"): 633 if res not in ("y", "Y"):
647 self.disp(cancel_message) 634 self.disp(cancel_message)
648 self.quit(C.EXIT_USER_CANCELLED) 635 self.quit(C.EXIT_USER_CANCELLED)
649 636
650 def quitFromSignal(self, errcode=0): 637 def quitFromSignal(self, errcode=0):
726 - 1 when there is a connection error 713 - 1 when there is a connection error
727 """ 714 """
728 # FIXME: need better exit codes 715 # FIXME: need better exit codes
729 716
730 def cant_connect(failure): 717 def cant_connect(failure):
731 log.error(_(u"Can't connect profile: {reason}").format(reason=failure)) 718 log.error(_("Can't connect profile: {reason}").format(reason=failure))
732 self.quit(1) 719 self.quit(1)
733 720
734 def cant_start_session(failure): 721 def cant_start_session(failure):
735 log.error(_(u"Can't start {profile}'s session: {reason}").format(profile=self.profile, reason=failure)) 722 log.error(_("Can't start {profile}'s session: {reason}").format(profile=self.profile, reason=failure))
736 self.quit(1) 723 self.quit(1)
737 724
738 self.profile = self.bridge.profileNameGet(self.args.profile) 725 self.profile = self.bridge.profileNameGet(self.args.profile)
739 726
740 if not self.profile: 727 if not self.profile:
750 self.bridge.profileStartSession(self.args.pwd, self.profile, lambda __: callback(), cant_start_session) 737 self.bridge.profileStartSession(self.args.pwd, self.profile, lambda __: callback(), cant_start_session)
751 self._auto_loop = True 738 self._auto_loop = True
752 return 739 return
753 elif not self.bridge.profileIsSessionStarted(self.profile): 740 elif not self.bridge.profileIsSessionStarted(self.profile):
754 if not self.args.connect: 741 if not self.args.connect:
755 log.error(_(u"Session for [{profile}] is not started, please start it before using jp, or use either --start-session or --connect option").format(profile=self.profile)) 742 log.error(_("Session for [{profile}] is not started, please start it before using jp, or use either --start-session or --connect option").format(profile=self.profile))
756 self.quit(1) 743 self.quit(1)
757 elif not getattr(self.args, "connect", False): 744 elif not getattr(self.args, "connect", False):
758 callback() 745 callback()
759 return 746 return
760 747
766 self.bridge.connect(self.profile, self.args.pwd, {}, lambda __: callback(), cant_connect) 753 self.bridge.connect(self.profile, self.args.pwd, {}, lambda __: callback(), cant_connect)
767 self._auto_loop = True 754 self._auto_loop = True
768 return 755 return
769 else: 756 else:
770 if not self.bridge.isConnected(self.profile): 757 if not self.bridge.isConnected(self.profile):
771 log.error(_(u"Profile [{profile}] is not connected, please connect it before using jp, or use --connect option").format(profile=self.profile)) 758 log.error(_("Profile [{profile}] is not connected, please connect it before using jp, or use --connect option").format(profile=self.profile))
772 self.quit(1) 759 self.quit(1)
773 760
774 callback() 761 callback()
775 762
776 def get_full_jid(self, param_jid): 763 def get_full_jid(self, param_jid):
853 if not choices: 840 if not choices:
854 raise exceptions.InternalError("No choice found for {} output type".format(use_output)) 841 raise exceptions.InternalError("No choice found for {} output type".format(use_output))
855 try: 842 try:
856 default = self.host.default_output[use_output] 843 default = self.host.default_output[use_output]
857 except KeyError: 844 except KeyError:
858 if u'default' in choices: 845 if 'default' in choices:
859 default = u'default' 846 default = 'default'
860 elif u'simple' in choices: 847 elif 'simple' in choices:
861 default = u'simple' 848 default = 'simple'
862 else: 849 else:
863 default = list(choices)[0] 850 default = list(choices)[0]
864 output_parent.add_argument('--output', '-O', choices=sorted(choices), default=default, help=_(u"select output format (default: {})".format(default))) 851 output_parent.add_argument('--output', '-O', choices=sorted(choices), default=default, help=_("select output format (default: {})".format(default)))
865 output_parent.add_argument('--output-option', '--oo', type=unicode_decoder, action="append", dest='output_opts', default=[], help=_(u"output specific option")) 852 output_parent.add_argument('--output-option', '--oo', action="append", dest='output_opts', default=[], help=_("output specific option"))
866 parents.add(output_parent) 853 parents.add(output_parent)
867 else: 854 else:
868 assert extra_outputs is None 855 assert extra_outputs is None
869 856
870 self._use_pubsub = kwargs.pop('use_pubsub', False) 857 self._use_pubsub = kwargs.pop('use_pubsub', False)
873 defaults = kwargs.pop('pubsub_defaults', {}) 860 defaults = kwargs.pop('pubsub_defaults', {})
874 parents.add(self.host.make_pubsub_group(flags, defaults)) 861 parents.add(self.host.make_pubsub_group(flags, defaults))
875 self._pubsub_flags = flags 862 self._pubsub_flags = flags
876 863
877 # other common options 864 # other common options
878 use_opts = {k:v for k,v in kwargs.iteritems() if k.startswith('use_')} 865 use_opts = {k:v for k,v in kwargs.items() if k.startswith('use_')}
879 for param, do_use in use_opts.iteritems(): 866 for param, do_use in use_opts.items():
880 opt=param[4:] # if param is use_verbose, opt is verbose 867 opt=param[4:] # if param is use_verbose, opt is verbose
881 if opt not in self.host.parents: 868 if opt not in self.host.parents:
882 raise exceptions.InternalError(u"Unknown parent option {}".format(opt)) 869 raise exceptions.InternalError("Unknown parent option {}".format(opt))
883 del kwargs[param] 870 del kwargs[param]
884 if do_use: 871 if do_use:
885 parents.add(self.host.parents[opt]) 872 parents.add(self.host.parents[opt])
886 873
887 self.parser = host.subparsers.add_parser(name, help=help, **kwargs) 874 self.parser = host.subparsers.add_parser(name, help=help, **kwargs)
955 data = self.host.bridge.progressGet(self.progress_id, self.profile) 942 data = self.host.bridge.progressGet(self.progress_id, self.profile)
956 if data: 943 if data:
957 try: 944 try:
958 size = data['size'] 945 size = data['size']
959 except KeyError: 946 except KeyError:
960 self.disp(_(u"file size is not known, we can't show a progress bar"), 1, error=True) 947 self.disp(_("file size is not known, we can't show a progress bar"), 1, error=True)
961 return False 948 return False
962 if self.host.pbar is None: 949 if self.host.pbar is None:
963 #first answer, we must construct the bar 950 #first answer, we must construct the bar
964 self.host.pbar = progressbar.ProgressBar(max_value=int(size), 951 self.host.pbar = progressbar.ProgressBar(max_value=int(size),
965 widgets=[_(u"Progress: "),progressbar.Percentage(), 952 widgets=[_("Progress: "),progressbar.Percentage(),
966 " ", 953 " ",
967 progressbar.Bar(), 954 progressbar.Bar(),
968 " ", 955 " ",
969 progressbar.FileTransferSpeed(), 956 progressbar.FileTransferSpeed(),
970 " ", 957 " ",
984 """Called when progress has just started 971 """Called when progress has just started
985 972
986 can be overidden by a command 973 can be overidden by a command
987 @param metadata(dict): metadata as sent by bridge.progressStarted 974 @param metadata(dict): metadata as sent by bridge.progressStarted
988 """ 975 """
989 self.disp(_(u"Operation started"), 2) 976 self.disp(_("Operation started"), 2)
990 977
991 def onProgressUpdate(self, metadata): 978 def onProgressUpdate(self, metadata):
992 """Method called on each progress updata 979 """Method called on each progress updata
993 980
994 can be overidden by a command to handle progress metadata 981 can be overidden by a command to handle progress metadata
1000 """Called when progress has just finished 987 """Called when progress has just finished
1001 988
1002 can be overidden by a command 989 can be overidden by a command
1003 @param metadata(dict): metadata as sent by bridge.progressFinished 990 @param metadata(dict): metadata as sent by bridge.progressFinished
1004 """ 991 """
1005 self.disp(_(u"Operation successfully finished"), 2) 992 self.disp(_("Operation successfully finished"), 2)
1006 993
1007 def onProgressError(self, error_msg): 994 def onProgressError(self, error_msg):
1008 """Called when a progress failed 995 """Called when a progress failed
1009 996
1010 @param error_msg(unicode): error message as sent by bridge.progressError 997 @param error_msg(unicode): error message as sent by bridge.progressError
1011 """ 998 """
1012 self.disp(_(u"Error while doing operation: {}").format(error_msg), error=True) 999 self.disp(_("Error while doing operation: {}").format(error_msg), error=True)
1013 1000
1014 def disp(self, msg, verbosity=0, error=False, no_lf=False): 1001 def disp(self, msg, verbosity=0, error=False, no_lf=False):
1015 return self.host.disp(msg, verbosity, error, no_lf) 1002 return self.host.disp(msg, verbosity, error, no_lf)
1016 1003
1017 def output(self, data): 1004 def output(self, data):
1018 try: 1005 try:
1019 output_type = self._output_type 1006 output_type = self._output_type
1020 except AttributeError: 1007 except AttributeError:
1021 raise exceptions.InternalError(_(u'trying to use output when use_output has not been set')) 1008 raise exceptions.InternalError(_('trying to use output when use_output has not been set'))
1022 return self.host.output(output_type, self.args.output, self.extra_outputs, data) 1009 return self.host.output(output_type, self.args.output, self.extra_outputs, data)
1023 1010
1024 def exitCb(self, msg=None): 1011 def exitCb(self, msg=None):
1025 """generic callback for success 1012 """generic callback for success
1026 1013
1039 @param msg(unicode, None): message template 1026 @param msg(unicode, None): message template
1040 use {} if you want to display failure message 1027 use {} if you want to display failure message
1041 @param exit_code(int): shell exit code 1028 @param exit_code(int): shell exit code
1042 """ 1029 """
1043 if msg is None: 1030 if msg is None:
1044 msg = _(u"error: {}") 1031 msg = _("error: {}")
1045 self.disp(msg.format(failure_), error=True) 1032 self.disp(msg.format(failure_), error=True)
1046 self.host.quit(exit_code) 1033 self.host.quit(exit_code)
1047 1034
1048 def getPubsubExtra(self, extra=None): 1035 def getPubsubExtra(self, extra=None):
1049 """Helper method to compute extra data from pubsub arguments 1036 """Helper method to compute extra data from pubsub arguments
1052 @return (dict): dict which can be used directly in the bridge for pubsub 1039 @return (dict): dict which can be used directly in the bridge for pubsub
1053 """ 1040 """
1054 if extra is None: 1041 if extra is None:
1055 extra = {} 1042 extra = {}
1056 else: 1043 else:
1057 intersection = {C.KEY_ORDER_BY}.intersection(extra.keys()) 1044 intersection = {C.KEY_ORDER_BY}.intersection(list(extra.keys()))
1058 if intersection: 1045 if intersection:
1059 raise exceptions.ConflictError( 1046 raise exceptions.ConflictError(
1060 u"given extra dict has conflicting keys with pubsub keys " 1047 "given extra dict has conflicting keys with pubsub keys "
1061 u"{intersection}".format(intersection=intersection)) 1048 "{intersection}".format(intersection=intersection))
1062 1049
1063 # RSM 1050 # RSM
1064 1051
1065 for attribute in (u'max', u'after', u'before', 'index'): 1052 for attribute in ('max', 'after', 'before', 'index'):
1066 key = u'rsm_' + attribute 1053 key = 'rsm_' + attribute
1067 if key in extra: 1054 if key in extra:
1068 raise exceptions.ConflictError( 1055 raise exceptions.ConflictError(
1069 u"This key already exists in extra: u{key}".format(key=key)) 1056 "This key already exists in extra: u{key}".format(key=key))
1070 value = getattr(self.args, key, None) 1057 value = getattr(self.args, key, None)
1071 if value is not None: 1058 if value is not None:
1072 extra[key] = unicode(value) 1059 extra[key] = str(value)
1073 1060
1074 # MAM 1061 # MAM
1075 1062
1076 if hasattr(self.args, u'mam_filters'): 1063 if hasattr(self.args, 'mam_filters'):
1077 for key, value in self.args.mam_filters: 1064 for key, value in self.args.mam_filters:
1078 key = u'filter_' + key 1065 key = 'filter_' + key
1079 if key in extra: 1066 if key in extra:
1080 raise exceptions.ConflictError( 1067 raise exceptions.ConflictError(
1081 u"This key already exists in extra: u{key}".format(key=key)) 1068 "This key already exists in extra: u{key}".format(key=key))
1082 extra[key] = value 1069 extra[key] = value
1083 1070
1084 # Order-By 1071 # Order-By
1085 1072
1086 try: 1073 try: