diff options
-rw-r--r-- | grc/base/ParseXML.py | 33 | ||||
-rw-r--r-- | grc/base/Platform.py | 113 | ||||
-rw-r--r-- | grc/gui/ActionHandler.py | 18 | ||||
-rw-r--r-- | grc/gui/Actions.py | 7 | ||||
-rw-r--r-- | grc/gui/Bars.py | 1 | ||||
-rw-r--r-- | grc/gui/CMakeLists.txt | 1 | ||||
-rw-r--r-- | grc/gui/Messages.py | 6 | ||||
-rw-r--r-- | grc/gui/ParserErrorsDialog.py | 100 |
8 files changed, 221 insertions, 58 deletions
diff --git a/grc/base/ParseXML.py b/grc/base/ParseXML.py index 56097395dd..abb6576ac2 100644 --- a/grc/base/ParseXML.py +++ b/grc/base/ParseXML.py @@ -20,12 +20,18 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from lxml import etree from . import odict +xml_failures = {} + + class XMLSyntaxError(Exception): def __init__(self, error_log): self._error_log = error_log + xml_failures[error_log.last_error.filename] = error_log + def __str__(self): return '\n'.join(map(str, self._error_log.filter_from_errors())) + def validate_dtd(xml_file, dtd_file=None): """ Validate an xml file against its dtd. @@ -36,13 +42,23 @@ def validate_dtd(xml_file, dtd_file=None): @throws Exception validation fails """ #perform parsing, use dtd validation if dtd file is not specified - parser = etree.XMLParser(dtd_validation=not dtd_file) - xml = etree.parse(xml_file, parser=parser) - if parser.error_log: raise XMLSyntaxError(parser.error_log) - #perform dtd validation if the dtd file is specified - if not dtd_file: return - dtd = etree.DTD(dtd_file) - if not dtd.validate(xml.getroot()): raise XMLSyntaxError(dtd.error_log) + try: + parser = etree.XMLParser(dtd_validation=not dtd_file) + xml = etree.parse(xml_file, parser=parser) + except etree.LxmlError: + pass + if parser.error_log: + raise XMLSyntaxError(parser.error_log) + + # perform dtd validation if the dtd file is specified + if not dtd_file: + return + try: + dtd = etree.DTD(dtd_file) + if not dtd.validate(xml.getroot()): + raise XMLSyntaxError(dtd.error_log) + except etree.LxmlError: + raise XMLSyntaxError(dtd.error_log) def from_file(xml_file): """ @@ -57,6 +73,7 @@ def from_file(xml_file): xml = etree.parse(xml_file).getroot() return _from_file(xml) + def _from_file(xml): """ Recursivly parse the xml tree into nested data format. @@ -81,6 +98,7 @@ def _from_file(xml): return odict({tag: nested_data}) + def to_file(nested_data, xml_file): """ Write an xml file and use the to xml helper method to load it. @@ -92,6 +110,7 @@ def to_file(nested_data, xml_file): xml = _to_file(nested_data)[0] open(xml_file, 'w').write(etree.tostring(xml, xml_declaration=True, pretty_print=True)) + def _to_file(nested_data): """ Recursivly parse the nested data into xml tree format. diff --git a/grc/base/Platform.py b/grc/base/Platform.py index 88cbf32b89..3ff80e8a03 100644 --- a/grc/base/Platform.py +++ b/grc/base/Platform.py @@ -28,11 +28,11 @@ from Port import Port as _Port from Param import Param as _Param from Constants import BLOCK_TREE_DTD, FLOW_GRAPH_DTD -class Platform(_Element): +class Platform(_Element): def __init__(self, name, version, key, - block_paths, block_dtd, default_flow_graph, generator, - license='', website=None, colors=[]): + block_paths, block_dtd, default_flow_graph, generator, + license='', website=None, colors=None): """ Make a platform from the arguments. @@ -61,48 +61,65 @@ class Platform(_Element): self._block_dtd = block_dtd self._default_flow_graph = default_flow_graph self._generator = generator - self._colors = colors + self._colors = colors or [] #create a dummy flow graph for the blocks self._flow_graph = _Element(self) - #search for *.xml files in the given search path - self.loadblocks(); - - def loadblocks(self): - xml_files = list() + self._blocks = None + self._blocks_n = None + self._category_trees_n = None + self.load_blocks() + + def load_blocks(self): + """load the blocks and block tree from the search paths""" + # reset + self._blocks = odict() + self._blocks_n = odict() + self._category_trees_n = list() + ParseXML.xml_failures.clear() + # try to parse and load blocks + for xml_file in self.iter_xml_files(): + try: + if xml_file.endswith("block_tree.xml"): + self.load_category_tree_xml(xml_file) + else: + self.load_block_xml(xml_file) + except ParseXML.XMLSyntaxError as e: + # print >> sys.stderr, 'Warning: Block validation failed:\n\t%s\n\tIgnoring: %s' % (e, xml_file) + pass + except Exception as e: + print >> sys.stderr, 'Warning: Block loading failed:\n\t%s\n\tIgnoring: %s' % (e, xml_file) + + def iter_xml_files(self): + """Iterator for block descriptions and category trees""" for block_path in self._block_paths: - if os.path.isfile(block_path): xml_files.append(block_path) + if os.path.isfile(block_path): + yield block_path elif os.path.isdir(block_path): for dirpath, dirnames, filenames in os.walk(block_path): for filename in sorted(filter(lambda f: f.endswith('.xml'), filenames)): - xml_files.append(os.path.join(dirpath, filename)) - #load the blocks - self._blocks = odict() - self._blocks_n = odict() - self._block_tree_files = list() - for xml_file in xml_files: - try: #try to add the xml file as a block wrapper - ParseXML.validate_dtd(xml_file, self._block_dtd) - n = ParseXML.from_file(xml_file).find('block') - #inject block wrapper path - n['block_wrapper_path'] = xml_file - block = self.Block(self._flow_graph, n) - key = block.get_key() - #test against repeated keys - if key in self.get_block_keys(): - print >> sys.stderr, 'Warning: Block with key "%s" already exists.\n\tIgnoring: %s'%(key, xml_file) - #store the block - else: - self._blocks[key] = block - self._blocks_n[key] = n - except ParseXML.XMLSyntaxError, e: - try: #try to add the xml file as a block tree - ParseXML.validate_dtd(xml_file, BLOCK_TREE_DTD) - self._block_tree_files.append(xml_file) - except ParseXML.XMLSyntaxError, e: - print >> sys.stderr, 'Warning: Block validation failed:\n\t%s\n\tIgnoring: %s'%(e, xml_file) - except Exception, e: - print >> sys.stderr, 'Warning: Block loading failed:\n\t%s\n\tIgnoring: %s'%(e, xml_file) + yield os.path.join(dirpath, filename) + + def load_block_xml(self, xml_file): + """Load block description from xml file""" + # validate and import + ParseXML.validate_dtd(xml_file, self._block_dtd) + n = ParseXML.from_file(xml_file).find('block') + n['block_wrapper_path'] = xml_file # inject block wrapper path + # get block instance and add it to the list of blocks + block = self.Block(self._flow_graph, n) + key = block.get_key() + if key in self.get_block_keys(): # test against repeated keys + print >> sys.stderr, 'Warning: Block with key "%s" already exists.\n\tIgnoring: %s' % (key, xml_file) + else: # store the block + self._blocks[key] = block + self._blocks_n[key] = n + + def load_category_tree_xml(self, xml_file): + """Validate and parse category tree file and add it to list""" + ParseXML.validate_dtd(xml_file, BLOCK_TREE_DTD) + n = ParseXML.from_file(xml_file).find('cat') + self._category_trees_n.append(n) def parse_flow_graph(self, flow_graph_file): """ @@ -117,7 +134,7 @@ class Platform(_Element): @throws exception if the validation fails """ flow_graph_file = flow_graph_file or self._default_flow_graph - open(flow_graph_file, 'r') #test open + open(flow_graph_file, 'r') # test open ParseXML.validate_dtd(flow_graph_file, FLOW_GRAPH_DTD) return ParseXML.from_file(flow_graph_file) @@ -131,24 +148,26 @@ class Platform(_Element): block_tree: the block tree object """ #recursive function to load categories and blocks - def load_category(cat_n, parent=[]): + def load_category(cat_n, parent=None): #add this category - parent = parent + [cat_n.find('name')] + parent = (parent or []) + [cat_n.find('name')] block_tree.add_block(parent) #recursive call to load sub categories map(lambda c: load_category(c, parent), cat_n.findall('cat')) #add blocks in this category for block_key in cat_n.findall('block'): if block_key not in self.get_block_keys(): - print >> sys.stderr, 'Warning: Block key "%s" not found when loading category tree.'%(block_key) + print >> sys.stderr, 'Warning: Block key "%s" not found when loading category tree.' % (block_key) continue block = self.get_block(block_key) #if it exists, the block's category shall not be overridden by the xml tree - if not block.get_category(): block.set_category(parent) - #load the block tree and update the categories for each block - for block_tree_file in self._block_tree_files: - #recursivly put categories in blocks - load_category(ParseXML.from_file(block_tree_file).find('cat')) + if not block.get_category(): + block.set_category(parent) + + # recursively load the category trees and update the categories for each block + for category_tree_n in self._category_trees_n: + load_category(category_tree_n) + #add blocks to block tree for block in self.get_blocks(): #blocks with empty categories are hidden diff --git a/grc/gui/ActionHandler.py b/grc/gui/ActionHandler.py index a891298fa4..86f06aad58 100644 --- a/grc/gui/ActionHandler.py +++ b/grc/gui/ActionHandler.py @@ -31,6 +31,7 @@ import Messages from .. base import ParseXML from MainWindow import MainWindow from PropsDialog import PropsDialog +from ParserErrorsDialog import ParserErrorsDialog import Dialogs from FileDialogs import OpenFlowGraphFileDialog, SaveFlowGraphFileDialog, SaveImageFileDialog @@ -116,6 +117,10 @@ class ActionHandler: Actions.TYPES_WINDOW_DISPLAY, Actions.TOGGLE_BLOCKS_WINDOW, Actions.TOGGLE_REPORTS_WINDOW, ): action.set_sensitive(True) + if ParseXML.xml_failures: + Messages.send_xml_errors_if_any(ParseXML.xml_failures) + Actions.XML_PARSER_ERRORS_DISPLAY.set_sensitive(True) + if not self.init_file_paths: self.init_file_paths = Preferences.files_open() if not self.init_file_paths: self.init_file_paths = [''] @@ -377,6 +382,11 @@ class ActionHandler: self.get_flow_graph().import_data(n) self.get_flow_graph().update() ################################################## + # View Parser Errors + ################################################## + elif action == Actions.XML_PARSER_ERRORS_DISPLAY: + ParserErrorsDialog(ParseXML.xml_failures).run() + ################################################## # Undo/Redo ################################################## elif action == Actions.FLOW_GRAPH_UNDO: @@ -454,9 +464,11 @@ class ActionHandler: elif action == Actions.PAGE_CHANGE: #pass and run the global actions pass elif action == Actions.RELOAD_BLOCKS: - self.platform.loadblocks() - self.main_window.btwin.clear(); - self.platform.load_block_tree(self.main_window.btwin); + self.platform.load_blocks() + self.main_window.btwin.clear() + self.platform.load_block_tree(self.main_window.btwin) + Actions.XML_PARSER_ERRORS_DISPLAY.set_sensitive(bool(ParseXML.xml_failures)) + Messages.send_xml_errors_if_any(ParseXML.xml_failures) elif action == Actions.FIND_BLOCKS: self.main_window.btwin.show() self.main_window.btwin.search_entry.show() diff --git a/grc/gui/Actions.py b/grc/gui/Actions.py index e96fb4d276..284c78f8fc 100644 --- a/grc/gui/Actions.py +++ b/grc/gui/Actions.py @@ -261,7 +261,7 @@ BLOCK_PASTE = Action( keypresses=(gtk.keysyms.v, gtk.gdk.CONTROL_MASK), ) ERRORS_WINDOW_DISPLAY = Action( - label='_Errors', + label='Flowgraph _Errors', tooltip='View flow graph errors', stock_id=gtk.STOCK_DIALOG_ERROR, ) @@ -354,3 +354,8 @@ BUSSIFY_SINKS = Action( tooltip='Gang sink ports into a single bus port', stock_id=gtk.STOCK_JUMP_TO, ) +XML_PARSER_ERRORS_DISPLAY = Action( + label='_Parser Errors', + tooltip='View errors that occured while parsing XML files', + stock_id=gtk.STOCK_DIALOG_ERROR, +) diff --git a/grc/gui/Bars.py b/grc/gui/Bars.py index 0e2b29c4e3..f016209383 100644 --- a/grc/gui/Bars.py +++ b/grc/gui/Bars.py @@ -103,6 +103,7 @@ MENU_BAR_LIST = ( (gtk.Action('Help', '_Help', None, None), [ Actions.HELP_WINDOW_DISPLAY, Actions.TYPES_WINDOW_DISPLAY, + Actions.XML_PARSER_ERRORS_DISPLAY, None, Actions.ABOUT_WINDOW_DISPLAY, ]), diff --git a/grc/gui/CMakeLists.txt b/grc/gui/CMakeLists.txt index c2eb16e9fd..08aaf3e4df 100644 --- a/grc/gui/CMakeLists.txt +++ b/grc/gui/CMakeLists.txt @@ -39,6 +39,7 @@ GR_PYTHON_INSTALL(FILES MainWindow.py Messages.py NotebookPage.py + ParserErrorsDialog.py PropsDialog.py Preferences.py StateCache.py diff --git a/grc/gui/Messages.py b/grc/gui/Messages.py index d903e40a45..a6620aa72f 100644 --- a/grc/gui/Messages.py +++ b/grc/gui/Messages.py @@ -53,6 +53,12 @@ def send_init(platform): def send_page_switch(file_path): send('\nShowing: "%s"\n'%file_path) +################# functions for loading blocks ######################################## +def send_xml_errors_if_any(xml_failures): + if xml_failures: + send('\nXML parser: Found {} erroneous XML file{} while loading the block tree ' + '(see "Help/Parser errors" for details)\n'.format(len(xml_failures), 's' if len(xml_failures) > 1 else '')) + ################# functions for loading flow graphs ######################################## def send_start_load(file_path): send('\nLoading: "%s"'%file_path + '\n') diff --git a/grc/gui/ParserErrorsDialog.py b/grc/gui/ParserErrorsDialog.py new file mode 100644 index 0000000000..68ee459414 --- /dev/null +++ b/grc/gui/ParserErrorsDialog.py @@ -0,0 +1,100 @@ +""" +Copyright 2013 Free Software Foundation, Inc. +This file is part of GNU Radio + +GNU Radio Companion is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +GNU Radio Companion is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +import pygtk +pygtk.require('2.0') +import gtk + +from Constants import MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT + + +class ParserErrorsDialog(gtk.Dialog): + """ + A dialog for viewing parser errors + """ + + def __init__(self, error_logs): + """ + Properties dialog constructor. + + Args: + block: a block instance + """ + gtk.Dialog.__init__(self, title='Parser Errors', buttons=(gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT)) + + self._error_logs = None + self.tree_store = gtk.TreeStore(str) + self.update_tree_store(error_logs) + + column = gtk.TreeViewColumn('XML Parser Errors by Filename') + renderer = gtk.CellRendererText() + column.pack_start(renderer, True) + column.add_attribute(renderer, 'text', 0) + column.set_sort_column_id(0) + + self.tree_view = tree_view = gtk.TreeView(self.tree_store) + tree_view.set_enable_search(False) # disable pop up search box + tree_view.set_search_column(-1) # really disable search + tree_view.set_reorderable(False) + tree_view.set_headers_visible(False) + tree_view.get_selection().set_mode(gtk.SELECTION_NONE) + tree_view.append_column(column) + + for row in self.tree_store: + tree_view.expand_row(row.path, False) + + scrolled_window = gtk.ScrolledWindow() + scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolled_window.add_with_viewport(tree_view) + + self.vbox.pack_start(scrolled_window, True) + self.set_size_request(2*MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT) + self.show_all() + + def update_tree_store(self, error_logs): + """set up data model""" + self.tree_store.clear() + self._error_logs = error_logs + for filename, errors in error_logs.iteritems(): + parent = self.tree_store.append(None, [str(filename)]) + try: + with open(filename, 'r') as fp: + code = fp.readlines() + except EnvironmentError: + code = None + for error in errors: + # http://lxml.de/api/lxml.etree._LogEntry-class.html + em = self.tree_store.append(parent, ["Line {e.line}: {e.message}".format(e=error)]) + if code: + self.tree_store.append(em, ["\n".join( + "{} {}{}".format(line, code[line - 1].replace("\t", " ").strip("\n"), + " " * 20 + "<!-- ERROR -->" if line == error.line else "") + for line in range(error.line - 2, error.line + 3) if 0 < line <= len(code) + )]) + + def run(self): + """ + Run the dialog and get its response. + + Returns: + true if the response was accept + """ + response = gtk.Dialog.run(self) + self.destroy() + return response == gtk.RESPONSE_ACCEPT |