toolboxes.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915
  1. """
  2. @package core.toolboxes
  3. @brief Functions for modifying menu from default/user toolboxes specified in XML files
  4. (C) 2013 by the GRASS Development Team
  5. This program is free software under the GNU General Public License
  6. (>=v2). Read the file COPYING that comes with GRASS for details.
  7. @author Vaclav Petras <wenzeslaus gmail.com>
  8. @author Anna Petrasova <kratochanna gmail.com>
  9. """
  10. from __future__ import print_function
  11. import os
  12. import sys
  13. import copy
  14. import xml.etree.ElementTree as etree
  15. from xml.parsers import expat
  16. # Get the XML parsing exceptions to catch. The behavior chnaged with Python 2.7
  17. # and ElementTree 1.3.
  18. if hasattr(etree, 'ParseError'):
  19. ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
  20. else:
  21. ETREE_EXCEPTIONS = (expat.ExpatError)
  22. if sys.version_info[0:2] > (2, 6):
  23. has_xpath = True
  24. else:
  25. has_xpath = False
  26. import grass.script.task as gtask
  27. import grass.script.core as gcore
  28. from grass.script.utils import try_remove
  29. from grass.exceptions import ScriptError, CalledModuleError
  30. # duplicating code from core/globalvar.py
  31. # if this will become part of grass Python library or module, this should be
  32. # parametrized, so we will get rid of the definition here
  33. # (GUI will use its definition and build also its own)
  34. WXGUIDIR = os.path.join(os.getenv("GISBASE"), "gui", "wxpython")
  35. # this could be placed to functions
  36. mainMenuFile = os.path.join(WXGUIDIR, 'xml', 'main_menu.xml')
  37. toolboxesFile = os.path.join(WXGUIDIR, 'xml', 'toolboxes.xml')
  38. wxguiItemsFile = os.path.join(WXGUIDIR, 'xml', 'wxgui_items.xml')
  39. moduleItemsFile = os.path.join(WXGUIDIR, 'xml', 'module_items.xml')
  40. def GetSettingsPath():
  41. # this is for cases during compilation and it is not possible to import wx
  42. # TODO: if the function would be in the grass Python library there would
  43. # no need to do this
  44. try:
  45. from core.utils import GetSettingsPath as actualGetSettingsPath
  46. return actualGetSettingsPath()
  47. except ImportError:
  48. # expecting that there will be no such path
  49. # (these files are always check for existence here)
  50. return ""
  51. def _getUserToolboxesFile():
  52. userToolboxesFile = os.path.join(
  53. GetSettingsPath(),
  54. 'toolboxes', 'toolboxes.xml')
  55. if not os.path.exists(userToolboxesFile):
  56. userToolboxesFile = None
  57. return userToolboxesFile
  58. def _getUserMainMenuFile():
  59. userMainMenuFile = os.path.join(
  60. GetSettingsPath(),
  61. 'toolboxes', 'main_menu.xml')
  62. if not os.path.exists(userMainMenuFile):
  63. userMainMenuFile = None
  64. return userMainMenuFile
  65. def _(string):
  66. """Get translated version of a string"""
  67. # is attribute initialized to actual value?
  68. if _.translate is None:
  69. try:
  70. # if not get the translate function named _
  71. from core.utils import _ as actual_translate
  72. # assign the imported function to translade attribute
  73. _.translate = actual_translate
  74. except ImportError:
  75. # speak English if there is a problem with import of wx
  76. def noop_traslate(string):
  77. return string
  78. _.translate = noop_traslate
  79. return _.translate(string)
  80. # attribute translate of function _
  81. _.translate = None
  82. # TODO: this should be part of some reader object
  83. _MESSAGES = []
  84. def _warning(message):
  85. """Show warning"""
  86. _MESSAGES.append("WARNING: %s" % message)
  87. def getMessages():
  88. return _MESSAGES
  89. def clearMessages():
  90. del _MESSAGES[:]
  91. def _debug(level, message):
  92. """Show debug message"""
  93. # this has interface as originally used GUI Debug but uses grass.script
  94. gcore.debug(message, level)
  95. def _encode_string(string):
  96. """Encode a unicode *string* using the system encoding
  97. If it is not possible to use system encoding, UTF-8 is used.
  98. """
  99. try:
  100. from core.gcmd import EncodeString
  101. return EncodeString(string)
  102. except ImportError:
  103. # This is the case when we have errors during compilation but
  104. # the environment is not complete (compilation, custom setups
  105. # of GRASS environmet) and we cannot import wx correctly.
  106. # UTF-8 is pretty good guess for most cases (and should work for
  107. # Mac OS X where wx 32 vs 64 bit issue is happaning).
  108. return string.encode('utf-8')
  109. def toolboxesOutdated():
  110. """Removes auto-generated menudata.xml
  111. to let gui regenerate it next time it starts."""
  112. path = os.path.join(GetSettingsPath(), 'toolboxes', 'menudata.xml')
  113. if os.path.exists(path):
  114. try_remove(path)
  115. def getMenudataFile(userRootFile, newFile, fallback):
  116. """Returns path to XML file for building menu or another tree.
  117. Creates toolbox directory where user defined toolboxes should be
  118. located. Checks whether it is needed to create new XML file (user
  119. changed toolboxes) or the already generated file could be used.
  120. If something goes wrong during building or user doesn't modify menu,
  121. default file (from distribution) is returned.
  122. """
  123. _debug(
  124. 1,
  125. "toolboxes.getMenudataFile: {userRootFile}, {newFile}, {fallback}".format(
  126. **locals()))
  127. distributionRootFile = os.path.join(WXGUIDIR, 'xml', userRootFile)
  128. userRootFile = os.path.join(GetSettingsPath(), 'toolboxes', userRootFile)
  129. if not os.path.exists(userRootFile):
  130. userRootFile = None
  131. # always create toolboxes directory if does not exist yet
  132. tbDir = _setupToolboxes()
  133. if tbDir:
  134. menudataFile = os.path.join(tbDir, newFile)
  135. generateNew = False
  136. # when any of main_menu.xml or toolboxes.xml are changed,
  137. # generate new menudata.xml
  138. if os.path.exists(menudataFile):
  139. # remove menu file when there is no main_menu and toolboxes
  140. if not _getUserToolboxesFile() and not userRootFile:
  141. os.remove(menudataFile)
  142. _debug(
  143. 2, "toolboxes.getMenudataFile: no user defined files, menudata deleted")
  144. return fallback
  145. if bool(_getUserToolboxesFile()) != bool(userRootFile):
  146. # always generate new because we don't know if there has been
  147. # any change
  148. generateNew = True
  149. _debug(
  150. 2, "toolboxes.getMenudataFile: only one of the user defined files")
  151. else:
  152. # if newer files -> generate new
  153. menudataTime = os.path.getmtime(menudataFile)
  154. if _getUserToolboxesFile():
  155. if os.path.getmtime(
  156. _getUserToolboxesFile()) > menudataTime:
  157. _debug(
  158. 2, "toolboxes.getMenudataFile: user toolboxes is newer than menudata")
  159. generateNew = True
  160. if userRootFile:
  161. if os.path.getmtime(userRootFile) > menudataTime:
  162. _debug(
  163. 2, "toolboxes.getMenudataFile: user root file is newer than menudata")
  164. generateNew = True
  165. elif _getUserToolboxesFile() or userRootFile:
  166. _debug(2, "toolboxes.getMenudataFile: no menudata")
  167. generateNew = True
  168. else:
  169. _debug(2, "toolboxes.getMenudataFile: no user defined files")
  170. return fallback
  171. if generateNew:
  172. try:
  173. # The case when user does not have custom root
  174. # file but has toolboxes requieres regeneration.
  175. # Unfortunately, this is the case can be often: defined
  176. # toolboxes but undefined module tree file.
  177. _debug(2, "toolboxes.getMenudataFile: creating a tree")
  178. tree = createTree(
  179. distributionRootFile=distributionRootFile,
  180. userRootFile=userRootFile)
  181. except ETREE_EXCEPTIONS:
  182. _warning(_("Unable to parse user toolboxes XML files. "
  183. "Default files will be loaded."))
  184. return fallback
  185. try:
  186. xml = _getXMLString(tree.getroot())
  187. fh = open(menudataFile, 'w')
  188. fh.write(xml)
  189. fh.close()
  190. return menudataFile
  191. except:
  192. _debug(
  193. 2,
  194. "toolboxes.getMenudataFile: writing menudata failed, returning fallback file")
  195. return fallback
  196. else:
  197. return menudataFile
  198. else:
  199. _debug(2, "toolboxes.getMenudataFile: returning menudata fallback file")
  200. return fallback
  201. def _setupToolboxes():
  202. """Create 'toolboxes' directory if doesn't exist."""
  203. basePath = GetSettingsPath()
  204. path = os.path.join(basePath, 'toolboxes')
  205. if not os.path.exists(basePath):
  206. return None
  207. if _createPath(path):
  208. return path
  209. return None
  210. def _createPath(path):
  211. """Creates path (for toolboxes) if it doesn't exist'"""
  212. if not os.path.exists(path):
  213. try:
  214. os.mkdir(path)
  215. except OSError as e:
  216. # we cannot use GError or similar because the gui doesn't start at
  217. # all
  218. gcore.warning(
  219. '%(reason)s\n%(detail)s' % ({
  220. 'reason': _('Unable to create toolboxes directory.'),
  221. 'detail': str(e)}))
  222. return False
  223. return True
  224. def createTree(distributionRootFile, userRootFile, userDefined=True):
  225. """Creates XML file with data for menu.
  226. Parses toolboxes files from distribution and from users,
  227. puts them together, adds metadata to modules and convert
  228. tree to previous format used for loading menu.
  229. :param userDefined: use toolboxes defined by user or not (during compilation)
  230. :return: ElementTree instance
  231. """
  232. if userDefined and userRootFile:
  233. mainMenu = etree.parse(userRootFile)
  234. else:
  235. mainMenu = etree.parse(distributionRootFile)
  236. toolboxes = etree.parse(toolboxesFile)
  237. if userDefined and _getUserToolboxesFile():
  238. userToolboxes = etree.parse(_getUserToolboxesFile())
  239. else:
  240. userToolboxes = None
  241. wxguiItems = etree.parse(wxguiItemsFile)
  242. moduleItems = etree.parse(moduleItemsFile)
  243. return toolboxes2menudata(mainMenu=mainMenu,
  244. toolboxes=toolboxes,
  245. userToolboxes=userToolboxes,
  246. wxguiItems=wxguiItems,
  247. moduleItems=moduleItems)
  248. def toolboxes2menudata(mainMenu, toolboxes, userToolboxes,
  249. wxguiItems, moduleItems):
  250. """Creates XML file with data for menu.
  251. Parses toolboxes files from distribution and from users,
  252. puts them together, adds metadata to modules and convert
  253. tree to previous format used for loading menu.
  254. :param userDefined: use toolboxes defined by user or not (during compilation)
  255. :return: ElementTree instance
  256. """
  257. root = mainMenu.getroot()
  258. userHasToolboxes = False
  259. # in case user has empty toolboxes file (to avoid genereation)
  260. if userToolboxes and userToolboxes.findall('.//toolbox'):
  261. _expandUserToolboxesItem(root, userToolboxes)
  262. _expandToolboxes(root, userToolboxes)
  263. userHasToolboxes = True
  264. if not userHasToolboxes:
  265. _removeUserToolboxesItem(root)
  266. _expandToolboxes(root, toolboxes)
  267. # we do not expand addons here since they need to be expanded in runtime
  268. _expandItems(root, moduleItems, 'module-item')
  269. _expandItems(root, wxguiItems, 'wxgui-item')
  270. # in case of compilation there are no additional runtime modules
  271. # but we need to create empty elements
  272. _expandRuntimeModules(root)
  273. _addHandlers(root)
  274. _convertTree(root)
  275. _indent(root)
  276. return mainMenu
  277. def _indent(elem, level=0):
  278. """Helper function to fix indentation of XML files."""
  279. i = "\n" + level * " "
  280. if len(elem):
  281. if not elem.text or not elem.text.strip():
  282. elem.text = i + " "
  283. if not elem.tail or not elem.tail.strip():
  284. elem.tail = i
  285. for elem in elem:
  286. _indent(elem, level + 1)
  287. if not elem.tail or not elem.tail.strip():
  288. elem.tail = i
  289. else:
  290. if level and (not elem.tail or not elem.tail.strip()):
  291. elem.tail = i
  292. def expandAddons(tree):
  293. """Expands addons element.
  294. """
  295. root = tree.getroot()
  296. _expandAddonsItem(root)
  297. # expanding and converting is done twice, so there is some overhead
  298. # we don't load metadata by calling modules on Windows because when
  299. # installed addons are not compatible, Windows show ugly crash dialog
  300. # for every incompatible addon
  301. if sys.platform == "win32":
  302. _expandRuntimeModules(root, loadMetadata=False)
  303. else:
  304. _expandRuntimeModules(root, loadMetadata=True)
  305. _addHandlers(root)
  306. _convertTree(root)
  307. def _expandToolboxes(node, toolboxes):
  308. """Expands tree with toolboxes.
  309. Function is called recursively.
  310. :param node: tree node where to look for subtoolboxes to be expanded
  311. :param toolboxes: tree of toolboxes to be used for expansion
  312. >>> menu = etree.fromstring('''
  313. ... <toolbox name="Raster">
  314. ... <label>&amp;Raster</label>
  315. ... <items>
  316. ... <module-item name="r.mask"/>
  317. ... <wxgui-item name="RasterMapCalculator"/>
  318. ... <subtoolbox name="NeighborhoodAnalysis"/>
  319. ... <subtoolbox name="OverlayRasters"/>
  320. ... </items>
  321. ... </toolbox>''')
  322. >>> toolboxes = etree.fromstring('''
  323. ... <toolboxes>
  324. ... <toolbox name="NeighborhoodAnalysis">
  325. ... <label>Neighborhood analysis</label>
  326. ... <items>
  327. ... <module-item name="r.neighbors"/>
  328. ... <module-item name="v.neighbors"/>
  329. ... </items>
  330. ... </toolbox>
  331. ... <toolbox name="OverlayRasters">
  332. ... <label>Overlay rasters</label>
  333. ... <items>
  334. ... <module-item name="r.cross"/>
  335. ... </items>
  336. ... </toolbox>
  337. ... </toolboxes>''')
  338. >>> _expandToolboxes(menu, toolboxes)
  339. >>> print etree.tostring(menu)
  340. <toolbox name="Raster">
  341. <label>&amp;Raster</label>
  342. <items>
  343. <module-item name="r.mask" />
  344. <wxgui-item name="RasterMapCalculator" />
  345. <toolbox name="NeighborhoodAnalysis">
  346. <label>Neighborhood analysis</label>
  347. <items>
  348. <module-item name="r.neighbors" />
  349. <module-item name="v.neighbors" />
  350. </items>
  351. </toolbox>
  352. <toolbox name="OverlayRasters">
  353. <label>Overlay rasters</label>
  354. <items>
  355. <module-item name="r.cross" />
  356. </items>
  357. </toolbox>
  358. </items>
  359. </toolbox>
  360. """
  361. nodes = node.findall('.//toolbox')
  362. if node.tag == 'toolbox': # root
  363. nodes.append(node)
  364. for n in nodes:
  365. if n.find('items') is None:
  366. continue
  367. for subtoolbox in n.findall('./items/subtoolbox'):
  368. items = n.find('./items')
  369. idx = items.getchildren().index(subtoolbox)
  370. if has_xpath:
  371. toolbox = toolboxes.find(
  372. './/toolbox[@name="%s"]' %
  373. subtoolbox.get('name'))
  374. else:
  375. toolbox = None
  376. potentialToolboxes = toolboxes.findall('.//toolbox')
  377. sName = subtoolbox.get('name')
  378. for pToolbox in potentialToolboxes:
  379. if pToolbox.get('name') == sName:
  380. toolbox = pToolbox
  381. break
  382. if toolbox is None: # not in file
  383. continue
  384. _expandToolboxes(toolbox, toolboxes)
  385. items.insert(idx, toolbox)
  386. items.remove(subtoolbox)
  387. def _expandUserToolboxesItem(node, toolboxes):
  388. """Expand tag 'user-toolboxes-list'.
  389. Include all user toolboxes.
  390. >>> tree = etree.fromstring('<toolbox><items><user-toolboxes-list/></items></toolbox>')
  391. >>> toolboxes = etree.fromstring('<toolboxes><toolbox name="UserToolbox"><items><module-item name="g.region"/></items></toolbox></toolboxes>')
  392. >>> _expandUserToolboxesItem(tree, toolboxes)
  393. >>> etree.tostring(tree)
  394. '<toolbox><items><toolbox name="GeneratedUserToolboxesList"><label>Custom toolboxes</label><items><toolbox name="UserToolbox"><items><module-item name="g.region" /></items></toolbox></items></toolbox></items></toolbox>'
  395. """
  396. tboxes = toolboxes.findall('.//toolbox')
  397. for n in node.findall('./items/user-toolboxes-list'):
  398. items = node.find('./items')
  399. idx = items.getchildren().index(n)
  400. el = etree.Element(
  401. 'toolbox', attrib={
  402. 'name': 'GeneratedUserToolboxesList'})
  403. items.insert(idx, el)
  404. label = etree.SubElement(el, tag='label')
  405. label.text = _("Custom toolboxes")
  406. it = etree.SubElement(el, tag='items')
  407. for toolbox in tboxes:
  408. it.append(copy.deepcopy(toolbox))
  409. items.remove(n)
  410. def _removeUserToolboxesItem(root):
  411. """Removes tag 'user-toolboxes-list' if there are no user toolboxes.
  412. >>> tree = etree.fromstring('<toolbox><items><user-toolboxes-list/></items></toolbox>')
  413. >>> _removeUserToolboxesItem(tree)
  414. >>> etree.tostring(tree)
  415. '<toolbox><items /></toolbox>'
  416. """
  417. for n in root.findall('./items/user-toolboxes-list'):
  418. items = root.find('./items')
  419. items.remove(n)
  420. def _getAddons():
  421. try:
  422. output = gcore.read_command('g.extension', quiet=True, flags='ag')
  423. except CalledModuleError as error:
  424. _warning(_("List of addons cannot be obtained"
  425. " because g.extension failed."
  426. " Details: %s") % error)
  427. return []
  428. flist = []
  429. for line in output.splitlines():
  430. if not line.startswith('executables'):
  431. continue
  432. for fexe in line.split('=', 1)[1].split(','):
  433. flist.append(fexe)
  434. return sorted(flist)
  435. def _removeAddonsItem(node, addonsNodes):
  436. # TODO: change impl to be similar with the remove toolboxes
  437. for n in addonsNodes:
  438. items = node.find('./items')
  439. if items is not None:
  440. items.remove(n)
  441. # because of inconsistent menudata file
  442. items = node.find('./menubar')
  443. if items is not None:
  444. items.remove(n)
  445. def _expandAddonsItem(node):
  446. """Expands addons element with currently installed addons.
  447. .. note::
  448. there is no mechanism yet to tell the gui to rebuild the
  449. menudata.xml file when new addons are added/removed.
  450. """
  451. # no addonsTag -> do nothing
  452. addonsTags = node.findall('.//addons')
  453. if not addonsTags:
  454. return
  455. # fetch addons
  456. addons = _getAddons()
  457. # no addons -> remove addons tag
  458. if not addons:
  459. _removeAddonsItem(node, addonsTags)
  460. return
  461. # create addons toolbox
  462. # keywords and desc are handled later automatically
  463. for n in addonsTags:
  464. # find parent is not possible with implementation of etree (in 2.7)
  465. items = node.find('./menubar')
  466. idx = items.getchildren().index(n)
  467. # do not set name since it is already in menudata file
  468. # attib={'name': 'AddonsList'}
  469. el = etree.Element('menu')
  470. items.insert(idx, el)
  471. label = etree.SubElement(el, tag='label')
  472. label.text = _("Addons")
  473. it = etree.SubElement(el, tag='items')
  474. for addon in addons:
  475. addonItem = etree.SubElement(it, tag='module-item')
  476. addonItem.attrib = {'name': addon}
  477. addonLabel = etree.SubElement(addonItem, tag='label')
  478. addonLabel.text = addon
  479. items.remove(n)
  480. def _expandItems(node, items, itemTag):
  481. """Expand items from file
  482. >>> tree = etree.fromstring('<items><module-item name="g.region"></module-item></items>')
  483. >>> items = etree.fromstring('<module-items><module-item name="g.region"><module>g.region</module><description>GRASS region management</description></module-item></module-items>')
  484. >>> _expandItems(tree, items, 'module-item')
  485. >>> etree.tostring(tree)
  486. '<items><module-item name="g.region"><module>g.region</module><description>GRASS region management</description></module-item></items>'
  487. """
  488. for moduleItem in node.findall('.//' + itemTag):
  489. itemName = moduleItem.get('name')
  490. if has_xpath:
  491. moduleNode = items.find('.//%s[@name="%s"]' % (itemTag, itemName))
  492. else:
  493. moduleNode = None
  494. potentialModuleNodes = items.findall('.//%s' % itemTag)
  495. for mNode in potentialModuleNodes:
  496. if mNode.get('name') == itemName:
  497. moduleNode = mNode
  498. break
  499. if moduleNode is None: # module not available in dist
  500. continue
  501. mItemChildren = moduleItem.getchildren()
  502. tagList = [n.tag for n in mItemChildren]
  503. for node in moduleNode.getchildren():
  504. if node.tag not in tagList:
  505. moduleItem.append(node)
  506. def _expandRuntimeModules(node, loadMetadata=True):
  507. """Add information to modules (desc, keywords)
  508. by running them with --interface-description.
  509. If loadMetadata is False, modules are not called,
  510. useful for incompatible addons.
  511. >>> tree = etree.fromstring('<items>'
  512. ... '<module-item name="g.region"></module-item>'
  513. ... '</items>')
  514. >>> _expandRuntimeModules(tree)
  515. >>> etree.tostring(tree)
  516. '<items><module-item name="g.region"><module>g.region</module><description>Manages the boundary definitions for the geographic region.</description><keywords>general,settings,computational region,extent,resolution,level1</keywords></module-item></items>'
  517. >>> tree = etree.fromstring('<items>'
  518. ... '<module-item name="m.proj"></module-item>'
  519. ... '</items>')
  520. >>> _expandRuntimeModules(tree)
  521. >>> etree.tostring(tree)
  522. '<items><module-item name="m.proj"><module>m.proj</module><description>Converts coordinates from one projection to another (cs2cs frontend).</description><keywords>miscellaneous,projection,transformation</keywords></module-item></items>'
  523. """
  524. hasErrors = False
  525. modules = node.findall('.//module-item')
  526. for module in modules:
  527. name = module.get('name')
  528. if module.find('module') is None:
  529. n = etree.SubElement(parent=module, tag='module')
  530. n.text = name
  531. if module.find('description') is None:
  532. if loadMetadata:
  533. desc, keywords = _loadMetadata(name)
  534. else:
  535. desc, keywords = '', ''
  536. n = etree.SubElement(parent=module, tag='description')
  537. n.text = _escapeXML(desc)
  538. n = etree.SubElement(parent=module, tag='keywords')
  539. n.text = _escapeXML(','.join(keywords))
  540. if loadMetadata and not desc:
  541. hasErrors = True
  542. if hasErrors:
  543. # not translatable until toolboxes compilation on Mac is fixed
  544. # translating causes importing globalvar, where sys.exit is called
  545. sys.stderr.write(
  546. "WARNING: Some addons failed when loading. "
  547. "Please consider to update your addons by running 'g.extension.all -f'.\n")
  548. def _escapeXML(text):
  549. """Helper function for correct escaping characters for XML.
  550. Duplicate function in core/toolboxes and probably also in man compilation
  551. and some existing Python package.
  552. >>> _escapeXML('<>&')
  553. '&amp;lt;&gt;&amp;'
  554. """
  555. return text.replace('<', '&lt;').replace("&", '&amp;').replace(">", '&gt;')
  556. def _loadMetadata(module):
  557. """Load metadata to modules.
  558. :param module: module name
  559. :return: (description, keywords as a list)
  560. """
  561. try:
  562. task = gtask.parse_interface(module)
  563. except ScriptError as e:
  564. sys.stderr.write("%s: %s\n" % (module, e))
  565. return '', ''
  566. return task.get_description(full=True), \
  567. task.get_keywords()
  568. def _addHandlers(node):
  569. """Add missing handlers to modules"""
  570. for n in node.findall('.//module-item'):
  571. if n.find('handler') is None:
  572. handlerNode = etree.SubElement(parent=n, tag='handler')
  573. handlerNode.text = 'OnMenuCmd'
  574. # e.g. g.region -p
  575. for n in node.findall('.//wxgui-item'):
  576. if n.find('command') is not None:
  577. handlerNode = etree.SubElement(parent=n, tag='handler')
  578. handlerNode.text = 'RunMenuCmd'
  579. def _convertTag(node, old, new):
  580. """Converts tag name.
  581. >>> tree = etree.fromstring('<toolboxes><toolbox><items><module-item/></items></toolbox></toolboxes>')
  582. >>> _convertTag(tree, 'toolbox', 'menu')
  583. >>> _convertTag(tree, 'module-item', 'menuitem')
  584. >>> etree.tostring(tree)
  585. '<toolboxes><menu><items><menuitem /></items></menu></toolboxes>'
  586. """
  587. for n in node.findall('.//%s' % old):
  588. n.tag = new
  589. def _convertTagAndRemoveAttrib(node, old, new):
  590. """Converts tag name and removes attributes.
  591. >>> tree = etree.fromstring('<toolboxes><toolbox name="Raster"><items><module-item name="g.region"/></items></toolbox></toolboxes>')
  592. >>> _convertTagAndRemoveAttrib(tree, 'toolbox', 'menu')
  593. >>> _convertTagAndRemoveAttrib(tree, 'module-item', 'menuitem')
  594. >>> etree.tostring(tree)
  595. '<toolboxes><menu><items><menuitem /></items></menu></toolboxes>'
  596. """
  597. for n in node.findall('.//%s' % old):
  598. n.tag = new
  599. n.attrib = {}
  600. def _convertTree(root):
  601. """Converts tree to be the form readable by core/menutree.py.
  602. >>> tree = etree.fromstring('<toolbox name="MainMenu"><label>Main menu</label><items><toolbox><label>Raster</label><items><module-item name="g.region"><module>g.region</module></module-item></items></toolbox></items></toolbox>')
  603. >>> _convertTree(tree)
  604. >>> etree.tostring(tree)
  605. '<menudata><menubar><menu><label>Raster</label><items><menuitem><command>g.region</command></menuitem></items></menu></menubar></menudata>'
  606. """
  607. root.attrib = {}
  608. label = root.find('label')
  609. # must check because of inconsistent XML menudata file
  610. if label is not None:
  611. root.remove(label)
  612. _convertTag(root, 'description', 'help')
  613. _convertTag(root, 'wx-id', 'id')
  614. _convertTag(root, 'module', 'command')
  615. _convertTag(root, 'related-module', 'command')
  616. _convertTagAndRemoveAttrib(root, 'wxgui-item', 'menuitem')
  617. _convertTagAndRemoveAttrib(root, 'module-item', 'menuitem')
  618. root.tag = 'menudata'
  619. i1 = root.find('./items')
  620. # must check because of inconsistent XML menudata file
  621. if i1 is not None:
  622. i1.tag = 'menubar'
  623. _convertTagAndRemoveAttrib(root, 'toolbox', 'menu')
  624. def _getXMLString(root):
  625. """Converts XML tree to string
  626. Since it is usually requier, this function adds a comment (about
  627. autogenerated file) to XML file.
  628. :return: XML as string
  629. """
  630. xml = etree.tostring(root, encoding='UTF-8')
  631. return xml.replace("<?xml version='1.0' encoding='UTF-8'?>\n",
  632. "<?xml version='1.0' encoding='UTF-8'?>\n"
  633. "<!--This is an auto-generated file-->\n")
  634. def do_doctest_gettext_workaround():
  635. """Setups environment for doing a doctest with gettext usage.
  636. When using gettext with dynamically defined underscore function
  637. (`_("For translation")`), doctest does not work properly.
  638. One option is to use `import as` instead of dynamically defined
  639. underscore function but this requires change all modules which are
  640. used by tested module.
  641. The second option is to define dummy underscore function and one
  642. other function which creates the right environment to satisfy all.
  643. This is done by this function. Moreover, `sys.displayhook` and also
  644. `sys.__displayhook__` needs to be redefined too (the later one probably
  645. should not be newer redefined but some cases just requires that).
  646. GRASS specific note is that wxGUI switched to use imported
  647. underscore function for translation. However, GRASS Python libraries
  648. still uses the dynamically defined underscore function, so this
  649. workaround function is still needed when you import something from
  650. GRASS Python libraries.
  651. """
  652. def new_displayhook(string):
  653. """A replacement for default `sys.displayhook`"""
  654. if string is not None:
  655. sys.stdout.write("%r\n" % (string,))
  656. def new_translator(string):
  657. """A fake gettext underscore function."""
  658. return string
  659. sys.displayhook = new_displayhook
  660. sys.__displayhook__ = new_displayhook
  661. import __builtin__
  662. __builtin__._ = new_translator
  663. def doc_test():
  664. """Tests the module using doctest
  665. :return: a number of failed tests
  666. """
  667. import doctest
  668. do_doctest_gettext_workaround()
  669. return doctest.testmod().failed
  670. def module_test():
  671. """Tests the module using test files included in the current
  672. directory and in files from distribution.
  673. """
  674. toolboxesFile = os.path.join(WXGUIDIR, 'xml', 'toolboxes.xml')
  675. userToolboxesFile = 'data/test_toolboxes_user_toolboxes.xml'
  676. menuFile = 'data/test_toolboxes_menu.xml'
  677. wxguiItemsFile = os.path.join(WXGUIDIR, 'xml', 'wxgui_items.xml')
  678. moduleItemsFile = os.path.join(WXGUIDIR, 'xml', 'module_items.xml')
  679. toolboxes = etree.parse(toolboxesFile)
  680. userToolboxes = etree.parse(userToolboxesFile)
  681. menu = etree.parse(menuFile)
  682. wxguiItems = etree.parse(wxguiItemsFile)
  683. moduleItems = etree.parse(moduleItemsFile)
  684. tree = toolboxes2menudata(mainMenu=menu,
  685. toolboxes=toolboxes,
  686. userToolboxes=userToolboxes,
  687. wxguiItems=wxguiItems,
  688. moduleItems=moduleItems)
  689. root = tree.getroot()
  690. tested = _getXMLString(root)
  691. # for generating correct test file supposing that the implementation
  692. # is now correct and working
  693. # run the normal test and check the difference before overwriting
  694. # the old correct test file
  695. if len(sys.argv) > 2 and sys.argv[2] == "generate-correct-file":
  696. sys.stdout.write(_getXMLString(root))
  697. return 0
  698. menudataFile = 'data/test_toolboxes_menudata_ref.xml'
  699. with open(menudataFile) as correctMenudata:
  700. correct = str(correctMenudata.read())
  701. import difflib
  702. differ = difflib.Differ()
  703. result = list(differ.compare(correct.splitlines(True),
  704. tested.splitlines(True)))
  705. someDiff = False
  706. for line in result:
  707. if line.startswith('+') or line.startswith('-'):
  708. sys.stdout.write(line)
  709. someDiff = True
  710. if someDiff:
  711. print("Difference between files.")
  712. return 1
  713. else:
  714. print("OK")
  715. return 0
  716. def validate_file(filename):
  717. try:
  718. etree.parse(filename)
  719. except ETREE_EXCEPTIONS as error:
  720. print("XML file <{name}> is not well formed: {error}".format(
  721. name=filename, error=error))
  722. return 1
  723. return 0
  724. def main():
  725. """Converts the toolboxes files on standard paths to the menudata file
  726. File is written to the standard output.
  727. """
  728. # TODO: fix parameter handling
  729. if len(sys.argv) > 1:
  730. mainFile = os.path.join(WXGUIDIR, 'xml', 'module_tree.xml')
  731. else:
  732. mainFile = os.path.join(WXGUIDIR, 'xml', 'main_menu.xml')
  733. tree = createTree(distributionRootFile=mainFile, userRootFile=None,
  734. userDefined=False)
  735. root = tree.getroot()
  736. sys.stdout.write(_getXMLString(root))
  737. return 0
  738. if __name__ == '__main__':
  739. # TODO: fix parameter handling
  740. if len(sys.argv) > 1:
  741. if sys.argv[1] == 'doctest':
  742. sys.exit(doc_test())
  743. elif sys.argv[1] == 'test':
  744. sys.exit(module_test())
  745. elif sys.argv[1] == 'validate':
  746. sys.exit(validate_file(sys.argv[2]))
  747. sys.exit(main())