task.py 22 KB

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