source: grass/trunk/lib/python/script/task.py

Last change on this file was 73956, checked in by martinl, 6 years ago

attempt to fix grass.task.get_interface python3 issue, see #3731

  • Property svn:eol-style set to native
  • Property svn:mime-type set to text/x-python
File size: 22.0 KB
Line 
1"""
2Get interface description of GRASS commands
3
4Based on gui/wxpython/gui_modules/menuform.py
5
6Usage:
7
8::
9
10 from grass.script import task as gtask
11 gtask.command_info('r.info')
12
13(C) 2011 by the GRASS Development Team
14This program is free software under the GNU General Public
15License (>=v2). Read the file COPYING that comes with GRASS
16for details.
17
18.. sectionauthor:: Martin Landa <landa.martin gmail.com>
19"""
20import re
21import sys
22import string
23
24if sys.version_info.major == 3:
25 unicode = str
26
27try:
28 import xml.etree.ElementTree as etree
29except ImportError:
30 import elementtree.ElementTree as etree # Python <= 2.4
31from xml.parsers import expat # TODO: works for any Python?
32# Get the XML parsing exceptions to catch. The behavior chnaged with Python 2.7
33# and ElementTree 1.3.
34if hasattr(etree, 'ParseError'):
35 ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
36else:
37 ETREE_EXCEPTIONS = (expat.ExpatError)
38
39from .utils import encode, decode, split
40from .core import *
41
42
43class grassTask:
44 """This class holds the structures needed for filling by the parser
45
46 Parameter blackList is a dictionary with fixed structure, eg.
47
48 ::
49
50 blackList = {'items' : {'d.legend' : { 'flags' : ['m'], 'params' : [] }},
51 'enabled': True}
52
53 :param str path: full path
54 :param blackList: hide some options in the GUI (dictionary)
55 """
56 def __init__(self, path=None, blackList=None):
57 self.path = path
58 self.name = _('unknown')
59 self.params = list()
60 self.description = ''
61 self.label = ''
62 self.flags = list()
63 self.keywords = list()
64 self.errorMsg = ''
65 self.firstParam = None
66 if blackList:
67 self.blackList = blackList
68 else:
69 self.blackList = {'enabled': False, 'items': {}}
70
71 if path is not None:
72 try:
73 processTask(tree=etree.fromstring(get_interface_description(path)),
74 task=self)
75 except ScriptError as e:
76 self.errorMsg = e.value
77
78 self.define_first()
79
80 def define_first(self):
81 """Define first parameter
82
83 :return: name of first parameter
84 """
85 if len(self.params) > 0:
86 self.firstParam = self.params[0]['name']
87
88 return self.firstParam
89
90 def get_error_msg(self):
91 """Get error message ('' for no error)
92 """
93 return self.errorMsg
94
95 def get_name(self):
96 """Get task name
97 """
98 if sys.platform == 'win32':
99 name, ext = os.path.splitext(self.name)
100 if ext in ('.py', '.sh'):
101 return name
102 else:
103 return self.name
104
105 return self.name
106
107 def get_description(self, full=True):
108 """Get module's description
109
110 :param bool full: True for label + desc
111 """
112 if self.label:
113 if full:
114 return self.label + ' ' + self.description
115 else:
116 return self.label
117 else:
118 return self.description
119
120 def get_keywords(self):
121 """Get module's keywords
122 """
123 return self.keywords
124
125 def get_list_params(self, element='name'):
126 """Get list of parameters
127
128 :param str element: element name
129 """
130 params = []
131 for p in self.params:
132 params.append(p[element])
133
134 return params
135
136 def get_list_flags(self, element='name'):
137 """Get list of flags
138
139 :param str element: element name
140 """
141 flags = []
142 for p in self.flags:
143 flags.append(p[element])
144
145 return flags
146
147 def get_param(self, value, element='name', raiseError=True):
148 """Find and return a param by name
149
150 :param value: param's value
151 :param str element: element name
152 :param bool raiseError: True for raise on error
153 """
154 for p in self.params:
155 val = p.get(element, None)
156 if val is None:
157 continue
158 if isinstance(val, (list, tuple)):
159 if value in val:
160 return p
161 elif isinstance(val, (bytes, unicode)):
162 if p[element][:len(value)] == value:
163 return p
164 else:
165 if p[element] == value:
166 return p
167
168 if raiseError:
169 raise ValueError(_("Parameter element '%(element)s' not found: '%(value)s'") % \
170 { 'element' : element, 'value' : value })
171 else:
172 return None
173
174 def get_flag(self, aFlag):
175 """Find and return a flag by name
176
177 Raises ValueError when the flag is not found.
178
179 :param str aFlag: name of the flag
180 """
181 for f in self.flags:
182 if f['name'] == aFlag:
183 return f
184 raise ValueError(_("Flag not found: %s") % aFlag)
185
186 def get_cmd_error(self):
187 """Get error string produced by get_cmd(ignoreErrors = False)
188
189 :return: list of errors
190 """
191 errorList = list()
192 # determine if suppress_required flag is given
193 for f in self.flags:
194 if f['value'] and f['suppress_required']:
195 return errorList
196
197 for p in self.params:
198 if not p.get('value', '') and p.get('required', False):
199 if not p.get('default', ''):
200 desc = p.get('label', '')
201 if not desc:
202 desc = p['description']
203 errorList.append(_("Parameter '%(name)s' (%(desc)s) is missing.") % \
204 {'name': p['name'], 'desc': encode(desc)})
205
206 return errorList
207
208 def get_cmd(self, ignoreErrors=False, ignoreRequired=False,
209 ignoreDefault=True):
210 """Produce an array of command name and arguments for feeding
211 into some execve-like command processor.
212
213 :param bool ignoreErrors: True to return whatever has been built so
214 far, even though it would not be a correct
215 command for GRASS
216 :param bool ignoreRequired: True to ignore required flags, otherwise
217 '@<required@>' is shown
218 :param bool ignoreDefault: True to ignore parameters with default values
219 """
220 cmd = [self.get_name()]
221
222 suppress_required = False
223 for flag in self.flags:
224 if flag['value']:
225 if len(flag['name']) > 1: # e.g. overwrite
226 cmd += ['--' + flag['name']]
227 else:
228 cmd += ['-' + flag['name']]
229 if flag['suppress_required']:
230 suppress_required = True
231 for p in self.params:
232 if p.get('value', '') == '' and p.get('required', False):
233 if p.get('default', '') != '':
234 cmd += ['%s=%s' % (p['name'], p['default'])]
235 elif ignoreErrors and not suppress_required and not ignoreRequired:
236 cmd += ['%s=%s' % (p['name'], _('<required>'))]
237 elif p.get('value', '') == '' and p.get('default', '') != '' and not ignoreDefault:
238 cmd += ['%s=%s' % (p['name'], p['default'])]
239 elif p.get('value', '') != '' and \
240 (p['value'] != p.get('default', '') or not ignoreDefault):
241 # output only values that have been set, and different from defaults
242 cmd += ['%s=%s' % (p['name'], p['value'])]
243
244 errList = self.get_cmd_error()
245 if ignoreErrors is False and errList:
246 raise ValueError('\n'.join(errList))
247
248 return cmd
249
250 def get_options(self):
251 """Get options
252 """
253 return {'flags': self.flags, 'params': self.params}
254
255 def has_required(self):
256 """Check if command has at least one required parameter
257 """
258 for p in self.params:
259 if p.get('required', False):
260 return True
261
262 return False
263
264 def set_param(self, aParam, aValue, element='value'):
265 """Set param value/values.
266 """
267 try:
268 param = self.get_param(aParam)
269 except ValueError:
270 return
271
272 param[element] = aValue
273
274 def set_flag(self, aFlag, aValue, element='value'):
275 """Enable / disable flag.
276 """
277 try:
278 param = self.get_flag(aFlag)
279 except ValueError:
280 return
281
282 param[element] = aValue
283
284 def set_options(self, opts):
285 """Set flags and parameters
286
287 :param opts list of flags and parameters"""
288 for opt in opts:
289 if opt[0] == '-': # flag
290 self.set_flag(opt.lstrip('-'), True)
291 else: # parameter
292 key, value = opt.split('=', 1)
293 self.set_param(key, value)
294
295
296class processTask:
297 """A ElementTree handler for the --interface-description output,
298 as defined in grass-interface.dtd. Extend or modify this and the
299 DTD if the XML output of GRASS' parser is extended or modified.
300
301 :param tree: root tree node
302 :param task: grassTask instance or None
303 :param blackList: list of flags/params to hide
304
305 :return: grassTask instance
306 """
307 def __init__(self, tree, task=None, blackList=None):
308 if task:
309 self.task = task
310 else:
311 self.task = grassTask()
312 if blackList:
313 self.task.blackList = blackList
314
315 self.root = tree
316
317 self._process_module()
318 self._process_params()
319 self._process_flags()
320 self.task.define_first()
321
322 def _process_module(self):
323 """Process module description
324 """
325 self.task.name = self.root.get('name', default='unknown')
326
327 # keywords
328 for keyword in self._get_node_text(self.root, 'keywords').split(','):
329 self.task.keywords.append(keyword.strip())
330
331 self.task.label = self._get_node_text(self.root, 'label')
332 self.task.description = self._get_node_text(self.root, 'description')
333
334 def _process_params(self):
335 """Process parameters
336 """
337 for p in self.root.findall('parameter'):
338 # gisprompt
339 node_gisprompt = p.find('gisprompt')
340 gisprompt = False
341 age = element = prompt = None
342 if node_gisprompt is not None:
343 gisprompt = True
344 age = node_gisprompt.get('age', '')
345 element = node_gisprompt.get('element', '')
346 prompt = node_gisprompt.get('prompt', '')
347
348 # value(s)
349 values = []
350 values_desc = []
351 node_values = p.find('values')
352 if node_values is not None:
353 for pv in node_values.findall('value'):
354 values.append(self._get_node_text(pv, 'name'))
355 desc = self._get_node_text(pv, 'description')
356 if desc:
357 values_desc.append(desc)
358
359 # keydesc
360 key_desc = []
361 node_key_desc = p.find('keydesc')
362 if node_key_desc is not None:
363 for ki in node_key_desc.findall('item'):
364 key_desc.append(ki.text)
365
366 if p.get('multiple', 'no') == 'yes':
367 multiple = True
368 else:
369 multiple = False
370 if p.get('required', 'no') == 'yes':
371 required = True
372 else:
373 required = False
374
375 if self.task.blackList['enabled'] and \
376 self.task.name in self.task.blackList['items'] and \
377 p.get('name') in self.task.blackList['items'][self.task.name].get('params', []):
378 hidden = True
379 else:
380 hidden = False
381
382 self.task.params.append( {
383 "name" : p.get('name'),
384 "type" : p.get('type'),
385 "required" : required,
386 "multiple" : multiple,
387 "label" : self._get_node_text(p, 'label'),
388 "description" : self._get_node_text(p, 'description'),
389 'gisprompt' : gisprompt,
390 'age' : age,
391 'element' : element,
392 'prompt' : prompt,
393 "guisection" : self._get_node_text(p, 'guisection'),
394 "guidependency" : self._get_node_text(p, 'guidependency'),
395 "default" : self._get_node_text(p, 'default'),
396 "values" : values,
397 "values_desc" : values_desc,
398 "value" : '',
399 "key_desc" : key_desc,
400 "hidden" : hidden
401 })
402
403 def _process_flags(self):
404 """Process flags
405 """
406 for p in self.root.findall('flag'):
407 if self.task.blackList['enabled'] and \
408 self.task.name in self.task.blackList['items'] and \
409 p.get('name') in self.task.blackList['items'][self.task.name].get('flags', []):
410 hidden = True
411 else:
412 hidden = False
413
414 if p.find('suppress_required') is not None:
415 suppress_required = True
416 else:
417 suppress_required = False
418
419 self.task.flags.append( {
420 "name" : p.get('name'),
421 "label" : self._get_node_text(p, 'label'),
422 "description" : self._get_node_text(p, 'description'),
423 "guisection" : self._get_node_text(p, 'guisection'),
424 "suppress_required" : suppress_required,
425 "value" : False,
426 "hidden" : hidden
427 } )
428
429 def _get_node_text(self, node, tag, default=''):
430 """Get node text"""
431 p = node.find(tag)
432 if p is not None:
433 res = ' '.join(p.text.split())
434 return res
435
436 return default
437
438 def get_task(self):
439 """Get grassTask instance"""
440 return self.task
441
442
443def convert_xml_to_utf8(xml_text):
444 # enc = locale.getdefaultlocale()[1]
445
446 # modify: fetch encoding from the interface description text(xml)
447 # e.g. <?xml version="1.0" encoding="GBK"?>
448 pattern = re.compile(b'<\?xml[^>]*\Wencoding="([^"]*)"[^>]*\?>')
449 m = re.match(pattern, xml_text)
450 if m is None:
451 return xml_text
452 #
453 enc = m.groups()[0]
454 # for Python 3
455 enc_decoded = enc.decode('ascii')
456
457 # modify: change the encoding to "utf-8", for correct parsing
458 xml_text_utf8 = xml_text.decode(enc_decoded).encode("utf-8")
459 p = re.compile(b'encoding="' + enc + b'"', re.IGNORECASE)
460 xml_text_utf8 = p.sub(b'encoding="utf-8"', xml_text_utf8)
461
462 return xml_text_utf8
463
464
465def get_interface_description(cmd):
466 """Returns the XML description for the GRASS cmd (force text encoding to
467 "utf-8").
468
469 The DTD must be located in $GISBASE/gui/xml/grass-interface.dtd,
470 otherwise the parser will not succeed.
471
472 :param cmd: command (name of GRASS module)
473 """
474 try:
475 p = Popen([encode(cmd), b'--interface-description'], stdout=PIPE,
476 stderr=PIPE)
477 cmdout, cmderr = p.communicate()
478
479 # TODO: do it better (?)
480 if not cmdout and sys.platform == 'win32':
481 # we in fact expect pure module name (without extension)
482 # so, lets remove extension
483 if cmd.endswith('.py'):
484 cmd = os.path.splitext(cmd)[0]
485
486 if cmd == 'd.rast3d':
487 sys.path.insert(0, os.path.join(os.getenv('GISBASE'), 'etc',
488 'gui', 'scripts'))
489
490 p = Popen([sys.executable, get_real_command(cmd),
491 '--interface-description'],
492 stdout=PIPE, stderr=PIPE)
493 cmdout, cmderr = p.communicate()
494
495 if cmd == 'd.rast3d':
496 del sys.path[0] # remove gui/scripts from the path
497
498 if p.returncode != 0:
499 raise ScriptError(_("Unable to fetch interface description for command '<{cmd}>'."
500 "\n\nDetails: <{det}>".format(cmd=cmd, det=decode(cmderr))))
501
502 except OSError as e:
503 raise ScriptError(_("Unable to fetch interface description for command '<{cmd}>'."
504 "\n\nDetails: <{det}>".format(cmd=cmd, det=e)))
505
506 desc = convert_xml_to_utf8(cmdout)
507 desc = desc.replace(b'grass-interface.dtd',
508 os.path.join(os.getenv('GISBASE'), 'gui', 'xml',
509 'grass-interface.dtd').encode('utf-8'))
510 return desc
511
512
513def parse_interface(name, parser=processTask, blackList=None):
514 """Parse interface of given GRASS module
515
516 The *name* is either GRASS module name (of a module on path) or
517 a full or relative path to an executable.
518
519 :param str name: name of GRASS module to be parsed
520 :param parser:
521 :param blackList:
522 """
523 try:
524 tree = etree.fromstring(get_interface_description(name))
525 except ETREE_EXCEPTIONS as error:
526 raise ScriptError(_("Cannot parse interface description of"
527 "<{name}> module: {error}").format(name=name, error=error))
528 task = parser(tree, blackList=blackList).get_task()
529 # if name from interface is different than the originally
530 # provided name, then the provided name is likely a full path needed
531 # to actually run the module later
532 # (processTask uses only the XML which does not contain the original
533 # path used to execute the module)
534 if task.name != name:
535 task.path = name
536 return task
537
538
539def command_info(cmd):
540 """Returns meta information for any GRASS command as dictionary
541 with entries for description, keywords, usage, flags, and
542 parameters, e.g.
543
544 >>> command_info('g.tempfile') # doctest: +NORMALIZE_WHITESPACE
545 {'keywords': ['general', 'support'], 'params': [{'gisprompt': False,
546 'multiple': False, 'name': 'pid', 'guidependency': '', 'default': '',
547 'age': None, 'required': True, 'value': '', 'label': '', 'guisection': '',
548 'key_desc': [], 'values': [], 'values_desc': [], 'prompt': None,
549 'hidden': False, 'element': None, 'type': 'integer', 'description':
550 'Process id to use when naming the tempfile'}], 'flags': [{'description':
551 "Dry run - don't create a file, just prints it's file name", 'value':
552 False, 'label': '', 'guisection': '', 'suppress_required': False,
553 'hidden': False, 'name': 'd'}, {'description': 'Print usage summary',
554 'value': False, 'label': '', 'guisection': '', 'suppress_required': False,
555 'hidden': False, 'name': 'help'}, {'description': 'Verbose module output',
556 'value': False, 'label': '', 'guisection': '', 'suppress_required': False,
557 'hidden': False, 'name': 'verbose'}, {'description': 'Quiet module output',
558 'value': False, 'label': '', 'guisection': '', 'suppress_required': False,
559 'hidden': False, 'name': 'quiet'}], 'description': "Creates a temporary
560 file and prints it's file name.", 'usage': 'g.tempfile pid=integer [--help]
561 [--verbose] [--quiet]'}
562
563 >>> command_info('v.buffer')
564 ['vector', 'geometry', 'buffer']
565
566 :param str cmd: the command to query
567 """
568 task = parse_interface(cmd)
569 cmdinfo = {}
570
571 cmdinfo['description'] = task.get_description()
572 cmdinfo['keywords'] = task.get_keywords()
573 cmdinfo['flags'] = flags = task.get_options()['flags']
574 cmdinfo['params'] = params = task.get_options()['params']
575
576 usage = task.get_name()
577 flags_short = list()
578 flags_long = list()
579 for f in flags:
580 fname = f.get('name', 'unknown')
581 if len(fname) > 1:
582 flags_long.append(fname)
583 else:
584 flags_short.append(fname)
585
586 if len(flags_short) > 1:
587 usage += ' [-' + ''.join(flags_short) + ']'
588
589 for p in params:
590 ptype = ','.join(p.get('key_desc', []))
591 if not ptype:
592 ptype = p.get('type', '')
593 req = p.get('required', False)
594 if not req:
595 usage += ' ['
596 else:
597 usage += ' '
598 usage += p['name'] + '=' + ptype
599 if p.get('multiple', False):
600 usage += '[,' + ptype + ',...]'
601 if not req:
602 usage += ']'
603
604 for key in flags_long:
605 usage += ' [--' + key + ']'
606
607 cmdinfo['usage'] = usage
608
609 return cmdinfo
610
611def cmdtuple_to_list(cmd):
612 """Convert command tuple to list.
613
614 :param tuple cmd: GRASS command to be converted
615
616 :return: command in list
617 """
618 cmdList = []
619 if not cmd:
620 return cmdList
621
622 cmdList.append(cmd[0])
623
624 if 'flags' in cmd[1]:
625 for flag in cmd[1]['flags']:
626 cmdList.append('-' + flag)
627 for flag in ('help', 'verbose', 'quiet', 'overwrite'):
628 if flag in cmd[1] and cmd[1][flag] is True:
629 cmdList.append('--' + flag)
630
631 for k, v in cmd[1].items():
632 if k in ('flags', 'help', 'verbose', 'quiet', 'overwrite'):
633 continue
634 if ' ' in v:
635 v = '"%s"' % v
636 cmdList.append('%s=%s' % (k, v))
637
638 return cmdList
639
640def cmdlist_to_tuple(cmd):
641 """Convert command list to tuple for run_command() and others
642
643 :param list cmd: GRASS command to be converted
644
645 :return: command as tuple
646 """
647 if len(cmd) < 1:
648 return None
649
650 dcmd = {}
651 for item in cmd[1:]:
652 if '=' in item: # params
653 key, value = item.split('=', 1)
654 dcmd[str(key)] = value.replace('"', '')
655 elif item[:2] == '--': # long flags
656 flag = item[2:]
657 if flag in ('help', 'verbose', 'quiet', 'overwrite'):
658 dcmd[str(flag)] = True
659 elif len(item) == 2 and item[0] == '-': # -> flags
660 if 'flags' not in dcmd:
661 dcmd['flags'] = ''
662 dcmd['flags'] += item[1]
663 else: # unnamed parameter
664 module = parse_interface(cmd[0])
665 dcmd[module.define_first()] = item
666
667 return (cmd[0], dcmd)
668
669def cmdstring_to_tuple(cmd):
670 """Convert command string to tuple for run_command() and others
671
672 :param str cmd: command to be converted
673
674 :return: command as tuple
675 """
676 return cmdlist_to_tuple(split(cmd))
Note: See TracBrowser for help on using the repository browser.