task.py 18 KB

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