toolboxes.py 27 KB

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