123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915 |
- """
- @package core.toolboxes
- @brief Functions for modifying menu from default/user toolboxes specified in XML files
- (C) 2013 by the GRASS Development Team
- This program is free software under the GNU General Public License
- (>=v2). Read the file COPYING that comes with GRASS for details.
- @author Vaclav Petras <wenzeslaus gmail.com>
- @author Anna Petrasova <kratochanna gmail.com>
- """
- from __future__ import print_function
- import os
- import sys
- import copy
- import xml.etree.ElementTree as etree
- from xml.parsers import expat
- # Get the XML parsing exceptions to catch. The behavior chnaged with Python 2.7
- # and ElementTree 1.3.
- if hasattr(etree, 'ParseError'):
- ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
- else:
- ETREE_EXCEPTIONS = (expat.ExpatError)
- if sys.version_info[0:2] > (2, 6):
- has_xpath = True
- else:
- has_xpath = False
- import grass.script.task as gtask
- import grass.script.core as gcore
- from grass.script.utils import try_remove
- from grass.exceptions import ScriptError, CalledModuleError
- # duplicating code from core/globalvar.py
- # if this will become part of grass Python library or module, this should be
- # parametrized, so we will get rid of the definition here
- # (GUI will use its definition and build also its own)
- WXGUIDIR = os.path.join(os.getenv("GISBASE"), "gui", "wxpython")
- # this could be placed to functions
- mainMenuFile = os.path.join(WXGUIDIR, 'xml', 'main_menu.xml')
- toolboxesFile = os.path.join(WXGUIDIR, 'xml', 'toolboxes.xml')
- wxguiItemsFile = os.path.join(WXGUIDIR, 'xml', 'wxgui_items.xml')
- moduleItemsFile = os.path.join(WXGUIDIR, 'xml', 'module_items.xml')
- def GetSettingsPath():
- # this is for cases during compilation and it is not possible to import wx
- # TODO: if the function would be in the grass Python library there would
- # no need to do this
- try:
- from core.utils import GetSettingsPath as actualGetSettingsPath
- return actualGetSettingsPath()
- except ImportError:
- # expecting that there will be no such path
- # (these files are always check for existence here)
- return ""
- def _getUserToolboxesFile():
- userToolboxesFile = os.path.join(
- GetSettingsPath(),
- 'toolboxes', 'toolboxes.xml')
- if not os.path.exists(userToolboxesFile):
- userToolboxesFile = None
- return userToolboxesFile
- def _getUserMainMenuFile():
- userMainMenuFile = os.path.join(
- GetSettingsPath(),
- 'toolboxes', 'main_menu.xml')
- if not os.path.exists(userMainMenuFile):
- userMainMenuFile = None
- return userMainMenuFile
- def _(string):
- """Get translated version of a string"""
- # is attribute initialized to actual value?
- if _.translate is None:
- try:
- # if not get the translate function named _
- from core.utils import _ as actual_translate
- # assign the imported function to translade attribute
- _.translate = actual_translate
- except ImportError:
- # speak English if there is a problem with import of wx
- def noop_traslate(string):
- return string
- _.translate = noop_traslate
- return _.translate(string)
- # attribute translate of function _
- _.translate = None
- # TODO: this should be part of some reader object
- _MESSAGES = []
- def _warning(message):
- """Show warning"""
- _MESSAGES.append("WARNING: %s" % message)
- def getMessages():
- return _MESSAGES
- def clearMessages():
- del _MESSAGES[:]
- def _debug(level, message):
- """Show debug message"""
- # this has interface as originally used GUI Debug but uses grass.script
- gcore.debug(message, level)
- def _encode_string(string):
- """Encode a unicode *string* using the system encoding
- If it is not possible to use system encoding, UTF-8 is used.
- """
- try:
- from core.gcmd import EncodeString
- return EncodeString(string)
- except ImportError:
- # This is the case when we have errors during compilation but
- # the environment is not complete (compilation, custom setups
- # of GRASS environmet) and we cannot import wx correctly.
- # UTF-8 is pretty good guess for most cases (and should work for
- # Mac OS X where wx 32 vs 64 bit issue is happaning).
- return string.encode('utf-8')
- def toolboxesOutdated():
- """Removes auto-generated menudata.xml
- to let gui regenerate it next time it starts."""
- path = os.path.join(GetSettingsPath(), 'toolboxes', 'menudata.xml')
- if os.path.exists(path):
- try_remove(path)
- def getMenudataFile(userRootFile, newFile, fallback):
- """Returns path to XML file for building menu or another tree.
- Creates toolbox directory where user defined toolboxes should be
- located. Checks whether it is needed to create new XML file (user
- changed toolboxes) or the already generated file could be used.
- If something goes wrong during building or user doesn't modify menu,
- default file (from distribution) is returned.
- """
- _debug(
- 1,
- "toolboxes.getMenudataFile: {userRootFile}, {newFile}, {fallback}".format(
- **locals()))
- distributionRootFile = os.path.join(WXGUIDIR, 'xml', userRootFile)
- userRootFile = os.path.join(GetSettingsPath(), 'toolboxes', userRootFile)
- if not os.path.exists(userRootFile):
- userRootFile = None
- # always create toolboxes directory if does not exist yet
- tbDir = _setupToolboxes()
- if tbDir:
- menudataFile = os.path.join(tbDir, newFile)
- generateNew = False
- # when any of main_menu.xml or toolboxes.xml are changed,
- # generate new menudata.xml
- if os.path.exists(menudataFile):
- # remove menu file when there is no main_menu and toolboxes
- if not _getUserToolboxesFile() and not userRootFile:
- os.remove(menudataFile)
- _debug(
- 2, "toolboxes.getMenudataFile: no user defined files, menudata deleted")
- return fallback
- if bool(_getUserToolboxesFile()) != bool(userRootFile):
- # always generate new because we don't know if there has been
- # any change
- generateNew = True
- _debug(
- 2, "toolboxes.getMenudataFile: only one of the user defined files")
- else:
- # if newer files -> generate new
- menudataTime = os.path.getmtime(menudataFile)
- if _getUserToolboxesFile():
- if os.path.getmtime(
- _getUserToolboxesFile()) > menudataTime:
- _debug(
- 2, "toolboxes.getMenudataFile: user toolboxes is newer than menudata")
- generateNew = True
- if userRootFile:
- if os.path.getmtime(userRootFile) > menudataTime:
- _debug(
- 2, "toolboxes.getMenudataFile: user root file is newer than menudata")
- generateNew = True
- elif _getUserToolboxesFile() or userRootFile:
- _debug(2, "toolboxes.getMenudataFile: no menudata")
- generateNew = True
- else:
- _debug(2, "toolboxes.getMenudataFile: no user defined files")
- return fallback
- if generateNew:
- try:
- # The case when user does not have custom root
- # file but has toolboxes requieres regeneration.
- # Unfortunately, this is the case can be often: defined
- # toolboxes but undefined module tree file.
- _debug(2, "toolboxes.getMenudataFile: creating a tree")
- tree = createTree(
- distributionRootFile=distributionRootFile,
- userRootFile=userRootFile)
- except ETREE_EXCEPTIONS:
- _warning(_("Unable to parse user toolboxes XML files. "
- "Default files will be loaded."))
- return fallback
- try:
- xml = _getXMLString(tree.getroot())
- fh = open(menudataFile, 'w')
- fh.write(xml)
- fh.close()
- return menudataFile
- except:
- _debug(
- 2,
- "toolboxes.getMenudataFile: writing menudata failed, returning fallback file")
- return fallback
- else:
- return menudataFile
- else:
- _debug(2, "toolboxes.getMenudataFile: returning menudata fallback file")
- return fallback
- def _setupToolboxes():
- """Create 'toolboxes' directory if doesn't exist."""
- basePath = GetSettingsPath()
- path = os.path.join(basePath, 'toolboxes')
- if not os.path.exists(basePath):
- return None
- if _createPath(path):
- return path
- return None
- def _createPath(path):
- """Creates path (for toolboxes) if it doesn't exist'"""
- if not os.path.exists(path):
- try:
- os.mkdir(path)
- except OSError as e:
- # we cannot use GError or similar because the gui doesn't start at
- # all
- gcore.warning(
- '%(reason)s\n%(detail)s' % ({
- 'reason': _('Unable to create toolboxes directory.'),
- 'detail': str(e)}))
- return False
- return True
- def createTree(distributionRootFile, userRootFile, userDefined=True):
- """Creates XML file with data for menu.
- Parses toolboxes files from distribution and from users,
- puts them together, adds metadata to modules and convert
- tree to previous format used for loading menu.
- :param userDefined: use toolboxes defined by user or not (during compilation)
- :return: ElementTree instance
- """
- if userDefined and userRootFile:
- mainMenu = etree.parse(userRootFile)
- else:
- mainMenu = etree.parse(distributionRootFile)
- toolboxes = etree.parse(toolboxesFile)
- if userDefined and _getUserToolboxesFile():
- userToolboxes = etree.parse(_getUserToolboxesFile())
- else:
- userToolboxes = None
- wxguiItems = etree.parse(wxguiItemsFile)
- moduleItems = etree.parse(moduleItemsFile)
- return toolboxes2menudata(mainMenu=mainMenu,
- toolboxes=toolboxes,
- userToolboxes=userToolboxes,
- wxguiItems=wxguiItems,
- moduleItems=moduleItems)
- def toolboxes2menudata(mainMenu, toolboxes, userToolboxes,
- wxguiItems, moduleItems):
- """Creates XML file with data for menu.
- Parses toolboxes files from distribution and from users,
- puts them together, adds metadata to modules and convert
- tree to previous format used for loading menu.
- :param userDefined: use toolboxes defined by user or not (during compilation)
- :return: ElementTree instance
- """
- root = mainMenu.getroot()
- userHasToolboxes = False
- # in case user has empty toolboxes file (to avoid genereation)
- if userToolboxes and userToolboxes.findall('.//toolbox'):
- _expandUserToolboxesItem(root, userToolboxes)
- _expandToolboxes(root, userToolboxes)
- userHasToolboxes = True
- if not userHasToolboxes:
- _removeUserToolboxesItem(root)
- _expandToolboxes(root, toolboxes)
- # we do not expand addons here since they need to be expanded in runtime
- _expandItems(root, moduleItems, 'module-item')
- _expandItems(root, wxguiItems, 'wxgui-item')
- # in case of compilation there are no additional runtime modules
- # but we need to create empty elements
- _expandRuntimeModules(root)
- _addHandlers(root)
- _convertTree(root)
- _indent(root)
- return mainMenu
- def _indent(elem, level=0):
- """Helper function to fix indentation of XML files."""
- i = "\n" + level * " "
- if len(elem):
- if not elem.text or not elem.text.strip():
- elem.text = i + " "
- if not elem.tail or not elem.tail.strip():
- elem.tail = i
- for elem in elem:
- _indent(elem, level + 1)
- if not elem.tail or not elem.tail.strip():
- elem.tail = i
- else:
- if level and (not elem.tail or not elem.tail.strip()):
- elem.tail = i
- def expandAddons(tree):
- """Expands addons element.
- """
- root = tree.getroot()
- _expandAddonsItem(root)
- # expanding and converting is done twice, so there is some overhead
- # we don't load metadata by calling modules on Windows because when
- # installed addons are not compatible, Windows show ugly crash dialog
- # for every incompatible addon
- if sys.platform == "win32":
- _expandRuntimeModules(root, loadMetadata=False)
- else:
- _expandRuntimeModules(root, loadMetadata=True)
- _addHandlers(root)
- _convertTree(root)
- def _expandToolboxes(node, toolboxes):
- """Expands tree with toolboxes.
- Function is called recursively.
- :param node: tree node where to look for subtoolboxes to be expanded
- :param toolboxes: tree of toolboxes to be used for expansion
- >>> menu = etree.fromstring('''
- ... <toolbox name="Raster">
- ... <label>&Raster</label>
- ... <items>
- ... <module-item name="r.mask"/>
- ... <wxgui-item name="RasterMapCalculator"/>
- ... <subtoolbox name="NeighborhoodAnalysis"/>
- ... <subtoolbox name="OverlayRasters"/>
- ... </items>
- ... </toolbox>''')
- >>> toolboxes = etree.fromstring('''
- ... <toolboxes>
- ... <toolbox name="NeighborhoodAnalysis">
- ... <label>Neighborhood analysis</label>
- ... <items>
- ... <module-item name="r.neighbors"/>
- ... <module-item name="v.neighbors"/>
- ... </items>
- ... </toolbox>
- ... <toolbox name="OverlayRasters">
- ... <label>Overlay rasters</label>
- ... <items>
- ... <module-item name="r.cross"/>
- ... </items>
- ... </toolbox>
- ... </toolboxes>''')
- >>> _expandToolboxes(menu, toolboxes)
- >>> print etree.tostring(menu)
- <toolbox name="Raster">
- <label>&Raster</label>
- <items>
- <module-item name="r.mask" />
- <wxgui-item name="RasterMapCalculator" />
- <toolbox name="NeighborhoodAnalysis">
- <label>Neighborhood analysis</label>
- <items>
- <module-item name="r.neighbors" />
- <module-item name="v.neighbors" />
- </items>
- </toolbox>
- <toolbox name="OverlayRasters">
- <label>Overlay rasters</label>
- <items>
- <module-item name="r.cross" />
- </items>
- </toolbox>
- </items>
- </toolbox>
- """
- nodes = node.findall('.//toolbox')
- if node.tag == 'toolbox': # root
- nodes.append(node)
- for n in nodes:
- if n.find('items') is None:
- continue
- for subtoolbox in n.findall('./items/subtoolbox'):
- items = n.find('./items')
- idx = items.getchildren().index(subtoolbox)
- if has_xpath:
- toolbox = toolboxes.find(
- './/toolbox[@name="%s"]' %
- subtoolbox.get('name'))
- else:
- toolbox = None
- potentialToolboxes = toolboxes.findall('.//toolbox')
- sName = subtoolbox.get('name')
- for pToolbox in potentialToolboxes:
- if pToolbox.get('name') == sName:
- toolbox = pToolbox
- break
- if toolbox is None: # not in file
- continue
- _expandToolboxes(toolbox, toolboxes)
- items.insert(idx, toolbox)
- items.remove(subtoolbox)
- def _expandUserToolboxesItem(node, toolboxes):
- """Expand tag 'user-toolboxes-list'.
- Include all user toolboxes.
- >>> tree = etree.fromstring('<toolbox><items><user-toolboxes-list/></items></toolbox>')
- >>> toolboxes = etree.fromstring('<toolboxes><toolbox name="UserToolbox"><items><module-item name="g.region"/></items></toolbox></toolboxes>')
- >>> _expandUserToolboxesItem(tree, toolboxes)
- >>> etree.tostring(tree)
- '<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>'
- """
- tboxes = toolboxes.findall('.//toolbox')
- for n in node.findall('./items/user-toolboxes-list'):
- items = node.find('./items')
- idx = items.getchildren().index(n)
- el = etree.Element(
- 'toolbox', attrib={
- 'name': 'GeneratedUserToolboxesList'})
- items.insert(idx, el)
- label = etree.SubElement(el, tag='label')
- label.text = _("Custom toolboxes")
- it = etree.SubElement(el, tag='items')
- for toolbox in tboxes:
- it.append(copy.deepcopy(toolbox))
- items.remove(n)
- def _removeUserToolboxesItem(root):
- """Removes tag 'user-toolboxes-list' if there are no user toolboxes.
- >>> tree = etree.fromstring('<toolbox><items><user-toolboxes-list/></items></toolbox>')
- >>> _removeUserToolboxesItem(tree)
- >>> etree.tostring(tree)
- '<toolbox><items /></toolbox>'
- """
- for n in root.findall('./items/user-toolboxes-list'):
- items = root.find('./items')
- items.remove(n)
- def _getAddons():
- try:
- output = gcore.read_command('g.extension', quiet=True, flags='ag')
- except CalledModuleError as error:
- _warning(_("List of addons cannot be obtained"
- " because g.extension failed."
- " Details: %s") % error)
- return []
- flist = []
- for line in output.splitlines():
- if not line.startswith('executables'):
- continue
- for fexe in line.split('=', 1)[1].split(','):
- flist.append(fexe)
- return sorted(flist)
- def _removeAddonsItem(node, addonsNodes):
- # TODO: change impl to be similar with the remove toolboxes
- for n in addonsNodes:
- items = node.find('./items')
- if items is not None:
- items.remove(n)
- # because of inconsistent menudata file
- items = node.find('./menubar')
- if items is not None:
- items.remove(n)
- def _expandAddonsItem(node):
- """Expands addons element with currently installed addons.
- .. note::
- there is no mechanism yet to tell the gui to rebuild the
- menudata.xml file when new addons are added/removed.
- """
- # no addonsTag -> do nothing
- addonsTags = node.findall('.//addons')
- if not addonsTags:
- return
- # fetch addons
- addons = _getAddons()
- # no addons -> remove addons tag
- if not addons:
- _removeAddonsItem(node, addonsTags)
- return
- # create addons toolbox
- # keywords and desc are handled later automatically
- for n in addonsTags:
- # find parent is not possible with implementation of etree (in 2.7)
- items = node.find('./menubar')
- idx = items.getchildren().index(n)
- # do not set name since it is already in menudata file
- # attib={'name': 'AddonsList'}
- el = etree.Element('menu')
- items.insert(idx, el)
- label = etree.SubElement(el, tag='label')
- label.text = _("Addons")
- it = etree.SubElement(el, tag='items')
- for addon in addons:
- addonItem = etree.SubElement(it, tag='module-item')
- addonItem.attrib = {'name': addon}
- addonLabel = etree.SubElement(addonItem, tag='label')
- addonLabel.text = addon
- items.remove(n)
- def _expandItems(node, items, itemTag):
- """Expand items from file
- >>> tree = etree.fromstring('<items><module-item name="g.region"></module-item></items>')
- >>> items = etree.fromstring('<module-items><module-item name="g.region"><module>g.region</module><description>GRASS region management</description></module-item></module-items>')
- >>> _expandItems(tree, items, 'module-item')
- >>> etree.tostring(tree)
- '<items><module-item name="g.region"><module>g.region</module><description>GRASS region management</description></module-item></items>'
- """
- for moduleItem in node.findall('.//' + itemTag):
- itemName = moduleItem.get('name')
- if has_xpath:
- moduleNode = items.find('.//%s[@name="%s"]' % (itemTag, itemName))
- else:
- moduleNode = None
- potentialModuleNodes = items.findall('.//%s' % itemTag)
- for mNode in potentialModuleNodes:
- if mNode.get('name') == itemName:
- moduleNode = mNode
- break
- if moduleNode is None: # module not available in dist
- continue
- mItemChildren = moduleItem.getchildren()
- tagList = [n.tag for n in mItemChildren]
- for node in moduleNode.getchildren():
- if node.tag not in tagList:
- moduleItem.append(node)
- def _expandRuntimeModules(node, loadMetadata=True):
- """Add information to modules (desc, keywords)
- by running them with --interface-description.
- If loadMetadata is False, modules are not called,
- useful for incompatible addons.
- >>> tree = etree.fromstring('<items>'
- ... '<module-item name="g.region"></module-item>'
- ... '</items>')
- >>> _expandRuntimeModules(tree)
- >>> etree.tostring(tree)
- '<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>'
- >>> tree = etree.fromstring('<items>'
- ... '<module-item name="m.proj"></module-item>'
- ... '</items>')
- >>> _expandRuntimeModules(tree)
- >>> etree.tostring(tree)
- '<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>'
- """
- hasErrors = False
- modules = node.findall('.//module-item')
- for module in modules:
- name = module.get('name')
- if module.find('module') is None:
- n = etree.SubElement(parent=module, tag='module')
- n.text = name
- if module.find('description') is None:
- if loadMetadata:
- desc, keywords = _loadMetadata(name)
- else:
- desc, keywords = '', ''
- n = etree.SubElement(parent=module, tag='description')
- n.text = _escapeXML(desc)
- n = etree.SubElement(parent=module, tag='keywords')
- n.text = _escapeXML(','.join(keywords))
- if loadMetadata and not desc:
- hasErrors = True
- if hasErrors:
- # not translatable until toolboxes compilation on Mac is fixed
- # translating causes importing globalvar, where sys.exit is called
- sys.stderr.write(
- "WARNING: Some addons failed when loading. "
- "Please consider to update your addons by running 'g.extension.all -f'.\n")
- def _escapeXML(text):
- """Helper function for correct escaping characters for XML.
- Duplicate function in core/toolboxes and probably also in man compilation
- and some existing Python package.
- >>> _escapeXML('<>&')
- '&lt;>&'
- """
- return text.replace('<', '<').replace("&", '&').replace(">", '>')
- def _loadMetadata(module):
- """Load metadata to modules.
- :param module: module name
- :return: (description, keywords as a list)
- """
- try:
- task = gtask.parse_interface(module)
- except ScriptError as e:
- sys.stderr.write("%s: %s\n" % (module, e))
- return '', ''
- return task.get_description(full=True), \
- task.get_keywords()
- def _addHandlers(node):
- """Add missing handlers to modules"""
- for n in node.findall('.//module-item'):
- if n.find('handler') is None:
- handlerNode = etree.SubElement(parent=n, tag='handler')
- handlerNode.text = 'OnMenuCmd'
- # e.g. g.region -p
- for n in node.findall('.//wxgui-item'):
- if n.find('command') is not None:
- handlerNode = etree.SubElement(parent=n, tag='handler')
- handlerNode.text = 'RunMenuCmd'
- def _convertTag(node, old, new):
- """Converts tag name.
- >>> tree = etree.fromstring('<toolboxes><toolbox><items><module-item/></items></toolbox></toolboxes>')
- >>> _convertTag(tree, 'toolbox', 'menu')
- >>> _convertTag(tree, 'module-item', 'menuitem')
- >>> etree.tostring(tree)
- '<toolboxes><menu><items><menuitem /></items></menu></toolboxes>'
- """
- for n in node.findall('.//%s' % old):
- n.tag = new
- def _convertTagAndRemoveAttrib(node, old, new):
- """Converts tag name and removes attributes.
- >>> tree = etree.fromstring('<toolboxes><toolbox name="Raster"><items><module-item name="g.region"/></items></toolbox></toolboxes>')
- >>> _convertTagAndRemoveAttrib(tree, 'toolbox', 'menu')
- >>> _convertTagAndRemoveAttrib(tree, 'module-item', 'menuitem')
- >>> etree.tostring(tree)
- '<toolboxes><menu><items><menuitem /></items></menu></toolboxes>'
- """
- for n in node.findall('.//%s' % old):
- n.tag = new
- n.attrib = {}
- def _convertTree(root):
- """Converts tree to be the form readable by core/menutree.py.
- >>> 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>')
- >>> _convertTree(tree)
- >>> etree.tostring(tree)
- '<menudata><menubar><menu><label>Raster</label><items><menuitem><command>g.region</command></menuitem></items></menu></menubar></menudata>'
- """
- root.attrib = {}
- label = root.find('label')
- # must check because of inconsistent XML menudata file
- if label is not None:
- root.remove(label)
- _convertTag(root, 'description', 'help')
- _convertTag(root, 'wx-id', 'id')
- _convertTag(root, 'module', 'command')
- _convertTag(root, 'related-module', 'command')
- _convertTagAndRemoveAttrib(root, 'wxgui-item', 'menuitem')
- _convertTagAndRemoveAttrib(root, 'module-item', 'menuitem')
- root.tag = 'menudata'
- i1 = root.find('./items')
- # must check because of inconsistent XML menudata file
- if i1 is not None:
- i1.tag = 'menubar'
- _convertTagAndRemoveAttrib(root, 'toolbox', 'menu')
- def _getXMLString(root):
- """Converts XML tree to string
- Since it is usually requier, this function adds a comment (about
- autogenerated file) to XML file.
- :return: XML as string
- """
- xml = etree.tostring(root, encoding='UTF-8')
- return xml.replace("<?xml version='1.0' encoding='UTF-8'?>\n",
- "<?xml version='1.0' encoding='UTF-8'?>\n"
- "<!--This is an auto-generated file-->\n")
- def do_doctest_gettext_workaround():
- """Setups environment for doing a doctest with gettext usage.
- When using gettext with dynamically defined underscore function
- (`_("For translation")`), doctest does not work properly.
- One option is to use `import as` instead of dynamically defined
- underscore function but this requires change all modules which are
- used by tested module.
- The second option is to define dummy underscore function and one
- other function which creates the right environment to satisfy all.
- This is done by this function. Moreover, `sys.displayhook` and also
- `sys.__displayhook__` needs to be redefined too (the later one probably
- should not be newer redefined but some cases just requires that).
- GRASS specific note is that wxGUI switched to use imported
- underscore function for translation. However, GRASS Python libraries
- still uses the dynamically defined underscore function, so this
- workaround function is still needed when you import something from
- GRASS Python libraries.
- """
- def new_displayhook(string):
- """A replacement for default `sys.displayhook`"""
- if string is not None:
- sys.stdout.write("%r\n" % (string,))
- def new_translator(string):
- """A fake gettext underscore function."""
- return string
- sys.displayhook = new_displayhook
- sys.__displayhook__ = new_displayhook
- import __builtin__
- __builtin__._ = new_translator
- def doc_test():
- """Tests the module using doctest
- :return: a number of failed tests
- """
- import doctest
- do_doctest_gettext_workaround()
- return doctest.testmod().failed
- def module_test():
- """Tests the module using test files included in the current
- directory and in files from distribution.
- """
- toolboxesFile = os.path.join(WXGUIDIR, 'xml', 'toolboxes.xml')
- userToolboxesFile = 'data/test_toolboxes_user_toolboxes.xml'
- menuFile = 'data/test_toolboxes_menu.xml'
- wxguiItemsFile = os.path.join(WXGUIDIR, 'xml', 'wxgui_items.xml')
- moduleItemsFile = os.path.join(WXGUIDIR, 'xml', 'module_items.xml')
- toolboxes = etree.parse(toolboxesFile)
- userToolboxes = etree.parse(userToolboxesFile)
- menu = etree.parse(menuFile)
- wxguiItems = etree.parse(wxguiItemsFile)
- moduleItems = etree.parse(moduleItemsFile)
- tree = toolboxes2menudata(mainMenu=menu,
- toolboxes=toolboxes,
- userToolboxes=userToolboxes,
- wxguiItems=wxguiItems,
- moduleItems=moduleItems)
- root = tree.getroot()
- tested = _getXMLString(root)
- # for generating correct test file supposing that the implementation
- # is now correct and working
- # run the normal test and check the difference before overwriting
- # the old correct test file
- if len(sys.argv) > 2 and sys.argv[2] == "generate-correct-file":
- sys.stdout.write(_getXMLString(root))
- return 0
- menudataFile = 'data/test_toolboxes_menudata_ref.xml'
- with open(menudataFile) as correctMenudata:
- correct = str(correctMenudata.read())
- import difflib
- differ = difflib.Differ()
- result = list(differ.compare(correct.splitlines(True),
- tested.splitlines(True)))
- someDiff = False
- for line in result:
- if line.startswith('+') or line.startswith('-'):
- sys.stdout.write(line)
- someDiff = True
- if someDiff:
- print("Difference between files.")
- return 1
- else:
- print("OK")
- return 0
- def validate_file(filename):
- try:
- etree.parse(filename)
- except ETREE_EXCEPTIONS as error:
- print("XML file <{name}> is not well formed: {error}".format(
- name=filename, error=error))
- return 1
- return 0
- def main():
- """Converts the toolboxes files on standard paths to the menudata file
- File is written to the standard output.
- """
- # TODO: fix parameter handling
- if len(sys.argv) > 1:
- mainFile = os.path.join(WXGUIDIR, 'xml', 'module_tree.xml')
- else:
- mainFile = os.path.join(WXGUIDIR, 'xml', 'main_menu.xml')
- tree = createTree(distributionRootFile=mainFile, userRootFile=None,
- userDefined=False)
- root = tree.getroot()
- sys.stdout.write(_getXMLString(root))
- return 0
- if __name__ == '__main__':
- # TODO: fix parameter handling
- if len(sys.argv) > 1:
- if sys.argv[1] == 'doctest':
- sys.exit(doc_test())
- elif sys.argv[1] == 'test':
- sys.exit(module_test())
- elif sys.argv[1] == 'validate':
- sys.exit(validate_file(sys.argv[2]))
- sys.exit(main())
|