Mercurial > libervia-backend
comparison frontends/src/jp/base.py @ 1950:227a4e617549
jp: --output option:
- new --output option can be added wich use_output. use_output can be True (in which case it wild default to C.OUTPUT_TEXT), or any of C.OUTPUT_TYPES (currently text, list and dict)
- output change the output format mainly to make command result parsing more easy, but it can also be use to add fancy effects (like coloration)
- outputs are added with plugins in the same way as commands (import of both is done in the same method)
- in the command class, output need to be declared with use_output=C.OUTPUT_xxx, then the data only need to be processed with self.output(data)
- outputs can have description (not used yet)
- use_xxx argument handling has been refactored (minor refactoring) to be more generic
- first outputs are "default", "json" (pretty printed) and "json_raw" (compact json)
- the first command to use them is "profile list"
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 23 Apr 2016 23:10:03 +0200 |
parents | 2c75011d7b2d |
children | 4633cfcbcccb |
comparison
equal
deleted
inserted
replaced
1949:c5fd304d0976 | 1950:227a4e617549 |
---|---|
37 from sat.core import exceptions | 37 from sat.core import exceptions |
38 import sat_frontends.jp | 38 import sat_frontends.jp |
39 from sat_frontends.jp.constants import Const as C | 39 from sat_frontends.jp.constants import Const as C |
40 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI | 40 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI |
41 import shlex | 41 import shlex |
42 from collections import OrderedDict | |
42 | 43 |
43 if sys.version_info < (2, 7, 3): | 44 if sys.version_info < (2, 7, 3): |
44 # XXX: shlex.split only handle unicode since python 2.7.3 | 45 # XXX: shlex.split only handle unicode since python 2.7.3 |
45 # this is a workaround for older versions | 46 # this is a workaround for older versions |
46 old_split = shlex.split | 47 old_split = shlex.split |
114 | 115 |
115 # progress attributes | 116 # progress attributes |
116 self._progress_id = None # TODO: manage several progress ids | 117 self._progress_id = None # TODO: manage several progress ids |
117 self.quit_on_progress_end = True | 118 self.quit_on_progress_end = True |
118 | 119 |
120 # outputs | |
121 self._outputs = {} | |
122 for type_ in C.OUTPUT_TYPES: | |
123 self._outputs[type_] = OrderedDict() | |
124 | |
119 @property | 125 @property |
120 def version(self): | 126 def version(self): |
121 return self.bridge.getVersion() | 127 return self.bridge.getVersion() |
122 | 128 |
123 @property | 129 @property |
177 if error: | 183 if error: |
178 print >>sys.stderr,msg | 184 print >>sys.stderr,msg |
179 else: | 185 else: |
180 print msg | 186 print msg |
181 | 187 |
188 def output(self, type_, name, data): | |
189 self._outputs[type_][name]['callback'](data) | |
190 | |
182 def addOnQuitCallback(self, callback, *args, **kwargs): | 191 def addOnQuitCallback(self, callback, *args, **kwargs): |
183 """Add a callback which will be called on quit command | 192 """Add a callback which will be called on quit command |
184 | 193 |
185 @param callback(callback): method to call | 194 @param callback(callback): method to call |
186 """ | 195 """ |
188 callbacks_list = self._onQuitCallbacks | 197 callbacks_list = self._onQuitCallbacks |
189 except AttributeError: | 198 except AttributeError: |
190 callbacks_list = self._onQuitCallbacks = [] | 199 callbacks_list = self._onQuitCallbacks = [] |
191 finally: | 200 finally: |
192 callbacks_list.append((callback, args, kwargs)) | 201 callbacks_list.append((callback, args, kwargs)) |
202 | |
203 def getOutputChoices(self, output_type): | |
204 """Return valid output filters for output_type | |
205 | |
206 @param output_type: True for default, | |
207 else can be any registered type | |
208 """ | |
209 return self._outputs[output_type].keys() | |
193 | 210 |
194 def _make_parents(self): | 211 def _make_parents(self): |
195 self.parents = {} | 212 self.parents = {} |
196 | 213 |
197 # we have a special case here as the start-session option is present only if connection is not needed, | 214 # we have a special case here as the start-session option is present only if connection is not needed, |
218 verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_(u"Add a verbosity level (can be used multiple times)")) | 235 verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_(u"Add a verbosity level (can be used multiple times)")) |
219 | 236 |
220 def add_parser_options(self): | 237 def add_parser_options(self): |
221 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT})) | 238 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT})) |
222 | 239 |
223 def import_commands(self): | 240 def register_output(self, type_, name, callback, description=""): |
224 """ Automaticaly import commands to jp | 241 if type_ not in C.OUTPUT_TYPES: |
242 log.error(u"Invalid output type {}".format(type_)) | |
243 return | |
244 self._outputs[type_][name] = {'callback': callback, | |
245 'description': description | |
246 } | |
247 | |
248 def import_plugins(self): | |
249 """Automaticaly import commands and outputs in jp | |
250 | |
225 looks from modules names cmd_*.py in jp path and import them | 251 looks from modules names cmd_*.py in jp path and import them |
226 | |
227 """ | 252 """ |
228 path = os.path.dirname(sat_frontends.jp.__file__) | 253 path = os.path.dirname(sat_frontends.jp.__file__) |
229 modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, "cmd_*.py")))) | 254 # XXX: outputs must be imported before commands as they are used for arguments |
230 for module_name in modules: | 255 for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), (C.PLUGIN_CMD, 'cmd_*.py')): |
231 module = import_module("sat_frontends.jp."+module_name) | 256 modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, pattern)))) |
232 try: | 257 for module_name in modules: |
233 self.import_command_module(module) | 258 module = import_module("sat_frontends.jp."+module_name) |
234 except ImportError: | 259 try: |
235 continue | 260 self.import_plugin_module(module, type_) |
236 | 261 except ImportError: |
237 def import_command_module(self, module): | 262 continue |
238 """ Add commands from a module to jp | 263 |
239 @param module: module containing commands | 264 def import_plugin_module(self, module, type_): |
240 | 265 """add commands or outpus from a module to jp |
241 """ | 266 |
242 try: | 267 @param module: module containing commands or outputs |
243 for classname in module.__commands__: | 268 @param type_(str): one of C_PLUGIN_* |
244 cls = getattr(module, classname) | 269 """ |
245 except AttributeError: | 270 try: |
246 log.warning(_("Invalid module %s") % module) | 271 class_names = getattr(module, '__{}__'.format(type_)) |
272 except AttributeError: | |
273 log.warning(_("Invalid plugin module [{type}] {module}").format(type=type_, module=module)) | |
247 raise ImportError | 274 raise ImportError |
248 cls(self) | 275 else: |
276 for class_name in class_names: | |
277 cls = getattr(module, class_name) | |
278 cls(self) | |
249 | 279 |
250 def run(self, args=None): | 280 def run(self, args=None): |
251 self.args = self.parser.parse_args(args) | 281 self.args = self.parser.parse_args(args) |
252 try: | 282 try: |
253 self.args.func() | 283 self.args.func() |
403 return param_jid | 433 return param_jid |
404 | 434 |
405 | 435 |
406 class CommandBase(object): | 436 class CommandBase(object): |
407 | 437 |
408 def __init__(self, host, name, use_profile=True, use_progress=False, use_verbose=False, need_connect=None, help=None, **kwargs): | 438 def __init__(self, host, name, use_profile=True, use_output=False, |
439 need_connect=None, help=None, **kwargs): | |
409 """ Initialise CommandBase | 440 """ Initialise CommandBase |
410 @param host: Jp instance | 441 @param host: Jp instance |
411 @param name(unicode): name of the new command | 442 @param name(unicode): name of the new command |
412 @param use_profile(bool): if True, add profile selection/connection commands | 443 @param use_profile(bool): if True, add profile selection/connection commands |
413 @param use_progress(bool): if True, add progress bar activation commands | 444 @param use_output(bool, dict): if not False, add --output option |
414 progress* signals will be handled | |
415 @param use_verbose(bool): if True, add verbosity command | |
416 @param need_connect(bool, None): True if profile connection is needed | 445 @param need_connect(bool, None): True if profile connection is needed |
417 False else (profile session must still be started) | 446 False else (profile session must still be started) |
418 None to set auto value (i.e. True if use_profile is set) | 447 None to set auto value (i.e. True if use_profile is set) |
419 Can't be set if use_profile is False | 448 Can't be set if use_profile is False |
420 @param help(unicode): help message to display | 449 @param help(unicode): help message to display |
421 @param **kwargs: args passed to ArgumentParser | 450 @param **kwargs: args passed to ArgumentParser |
451 use_* are handled directly, they can be: | |
452 - use_progress(bool): if True, add progress bar activation option | |
453 progress* signals will be handled | |
454 - use_verbose(bool): if True, add verbosity option | |
422 @attribute need_loop(bool): to set by commands when loop is needed | 455 @attribute need_loop(bool): to set by commands when loop is needed |
423 | 456 |
424 """ | 457 """ |
425 self.need_loop = False # to be set by commands when loop is needed | 458 self.need_loop = False # to be set by commands when loop is needed |
426 try: # If we have subcommands, host is a CommandBase and we need to use host.host | 459 try: # If we have subcommands, host is a CommandBase and we need to use host.host |
427 self.host = host.host | 460 self.host = host.host |
428 except AttributeError: | 461 except AttributeError: |
429 self.host = host | 462 self.host = host |
430 | 463 |
464 # --profile option | |
431 parents = kwargs.setdefault('parents', set()) | 465 parents = kwargs.setdefault('parents', set()) |
432 if use_profile: | 466 if use_profile: |
433 #self.host.parents['profile'] is an ArgumentParser with profile connection arguments | 467 #self.host.parents['profile'] is an ArgumentParser with profile connection arguments |
434 if need_connect is None: | 468 if need_connect is None: |
435 need_connect = True | 469 need_connect = True |
438 assert need_connect is None | 472 assert need_connect is None |
439 self.need_connect = need_connect | 473 self.need_connect = need_connect |
440 # from this point, self.need_connect is None if connection is not needed at all | 474 # from this point, self.need_connect is None if connection is not needed at all |
441 # False if session starting is needed, and True if full connection is needed | 475 # False if session starting is needed, and True if full connection is needed |
442 | 476 |
443 if use_progress: | 477 # --output option |
444 parents.add(self.host.parents['progress']) | 478 if use_output: |
445 | 479 if use_output == True: |
446 if use_verbose: | 480 use_output = C.OUTPUT_TEXT |
447 parents.add(self.host.parents['verbose']) | 481 assert use_output in C.OUTPUT_TYPES |
482 self._output_type = use_output | |
483 output_parent = argparse.ArgumentParser(add_help=False) | |
484 choices = self.host.getOutputChoices(use_output) | |
485 if not choices: | |
486 raise exceptions.InternalError("No choice found for {} output type".format(use_output)) | |
487 default = 'default' if 'default' in choices else choices[0] | |
488 output_parent.add_argument('--output', '-O', choices=choices, default=default, help=_(u"Select output format")) | |
489 parents.add(output_parent) | |
490 | |
491 # other common options | |
492 use_opts = {k:v for k,v in kwargs.iteritems() if k.startswith('use_')} | |
493 for param, do_use in use_opts.iteritems(): | |
494 opt=param[4:] # if param is use_verbose, opt is verbose | |
495 if opt not in self.host.parents: | |
496 raise exceptions.InternalError(u"Unknown parent option {}".format(opt)) | |
497 del kwargs[param] | |
498 if do_use: | |
499 parents.add(self.host.parents[opt]) | |
448 | 500 |
449 self.parser = host.subparsers.add_parser(name, help=help, **kwargs) | 501 self.parser = host.subparsers.add_parser(name, help=help, **kwargs) |
450 if hasattr(self, "subcommands"): | 502 if hasattr(self, "subcommands"): |
451 self.subparsers = self.parser.add_subparsers() | 503 self.subparsers = self.parser.add_subparsers() |
452 else: | 504 else: |
570 self.disp(_(u"Error while doing operation: {}").format(error_msg), error=True) | 622 self.disp(_(u"Error while doing operation: {}").format(error_msg), error=True) |
571 | 623 |
572 def disp(self, msg, verbosity=0, error=False): | 624 def disp(self, msg, verbosity=0, error=False): |
573 return self.host.disp(msg, verbosity, error) | 625 return self.host.disp(msg, verbosity, error) |
574 | 626 |
627 def output(self, data): | |
628 return self.host.output(self._output_type, self.args.output, data) | |
629 | |
575 def add_parser_options(self): | 630 def add_parser_options(self): |
576 try: | 631 try: |
577 subcommands = self.subcommands | 632 subcommands = self.subcommands |
578 except AttributeError: | 633 except AttributeError: |
579 # We don't have subcommands, the class need to implements add_parser_options | 634 # We don't have subcommands, the class need to implements add_parser_options |