task.py 19 KB

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