task.py 22 KB

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