task.py 19 KB

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