Commit 5f84e619 by Victor Shnayder

Add hook for error handling during xml import

* add error_handler member to DescriptorSystem
* call it where import errors happen
* also includes some refactoring in xml.py
* some more line length and docstring cleanups
parent 63f34f2e
...@@ -29,6 +29,7 @@ def process_includes(fn): ...@@ -29,6 +29,7 @@ def process_includes(fn):
etree.tostring(next_include, pretty_print=True)) etree.tostring(next_include, pretty_print=True))
msg += 'Cannot find file %s in %s' % (file, dir) msg += 'Cannot find file %s in %s' % (file, dir)
log.exception(msg) log.exception(msg)
system.error_handler(msg)
raise raise
try: try:
# read in and convert to XML # read in and convert to XML
...@@ -38,6 +39,7 @@ def process_includes(fn): ...@@ -38,6 +39,7 @@ def process_includes(fn):
etree.tostring(next_include, pretty_print=True)) etree.tostring(next_include, pretty_print=True))
msg += 'Cannot parse XML in %s' % (file) msg += 'Cannot parse XML in %s' % (file)
log.exception(msg) log.exception(msg)
system.error_handler(msg)
raise raise
# insert new XML into tree in place of inlcude # insert new XML into tree in place of inlcude
parent = next_include.getparent() parent = next_include.getparent()
......
import sys
def strict_error_handler(msg, exc_info=None):
'''
Do not let errors pass. If exc_info is not None, ignore msg, and just
re-raise. Otherwise, check if we are in an exception-handling context.
If so, re-raise. Otherwise, raise Exception(msg).
Meant for use in validation, where any errors should trap.
'''
if exc_info is not None:
raise exc_info[0], exc_info[1], exc_info[2]
# Check if there is an exception being handled somewhere up the stack
if sys.exc_info() != (None, None, None):
raise
raise Exception(msg)
def ignore_errors_handler(msg, exc_info=None):
'''Ignore all errors, relying on the caller to workaround.
Meant for use in the LMS, where an error in one part of the course
shouldn't bring down the whole system'''
pass
...@@ -31,8 +31,11 @@ class RawDescriptor(MakoModuleDescriptor, XmlDescriptor): ...@@ -31,8 +31,11 @@ class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
except etree.XMLSyntaxError as err: except etree.XMLSyntaxError as err:
lines = self.definition['data'].split('\n') lines = self.definition['data'].split('\n')
line, offset = err.position line, offset = err.position
log.exception("Unable to create xml for problem {loc}. Context: '{context}'".format( msg = ("Unable to create xml for problem {loc}. "
context=lines[line-1][offset - 40:offset + 40], "Context: '{context}'".format(
loc=self.location context=lines[line-1][offset - 40:offset + 40],
)) loc=self.location))
log.exception(msg)
self.system.error_handler(msg)
# no workaround possible, so just re-raise
raise raise
...@@ -203,13 +203,16 @@ class XModule(HTMLSnippet): ...@@ -203,13 +203,16 @@ class XModule(HTMLSnippet):
Return module instances for all the children of this module. Return module instances for all the children of this module.
''' '''
if self._loaded_children is None: if self._loaded_children is None:
self._loaded_children = [self.system.get_module(child) for child in self.definition.get('children', [])] self._loaded_children = [
self.system.get_module(child)
for child in self.definition.get('children', [])]
return self._loaded_children return self._loaded_children
def get_display_items(self): def get_display_items(self):
''' '''
Returns a list of descendent module instances that will display immediately Returns a list of descendent module instances that will display
inside this module immediately inside this module
''' '''
items = [] items = []
for child in self.get_children(): for child in self.get_children():
...@@ -219,8 +222,8 @@ class XModule(HTMLSnippet): ...@@ -219,8 +222,8 @@ class XModule(HTMLSnippet):
def displayable_items(self): def displayable_items(self):
''' '''
Returns list of displayable modules contained by this module. If this module Returns list of displayable modules contained by this module. If this
is visible, should return [self] module is visible, should return [self]
''' '''
return [self] return [self]
...@@ -439,16 +442,19 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -439,16 +442,19 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
on the contents of xml_data. on the contents of xml_data.
xml_data must be a string containing valid xml xml_data must be a string containing valid xml
system is an XMLParsingSystem system is an XMLParsingSystem
org and course are optional strings that will be used in the generated modules
url identifiers org and course are optional strings that will be used in the generated
modules url identifiers
""" """
class_ = XModuleDescriptor.load_class( class_ = XModuleDescriptor.load_class(
etree.fromstring(xml_data).tag, etree.fromstring(xml_data).tag,
default_class default_class
) )
# leave next line in code, commented out - useful for low-level debugging # leave next line, commented out - useful for low-level debugging
# log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (etree.fromstring(xml_data).tag,class_)) log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (
etree.fromstring(xml_data).tag,class_))
return class_.from_xml(xml_data, system, org, course) return class_.from_xml(xml_data, system, org, course)
@classmethod @classmethod
...@@ -457,35 +463,42 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -457,35 +463,42 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
Creates an instance of this descriptor from the supplied xml_data. Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for xml_data: A string of xml that will be translated into data and children
this module for this module
system is an XMLParsingSystem system is an XMLParsingSystem
org and course are optional strings that will be used in the generated modules
url identifiers org and course are optional strings that will be used in the generated
module's url identifiers
""" """
raise NotImplementedError('Modules must implement from_xml to be parsable from xml') raise NotImplementedError(
'Modules must implement from_xml to be parsable from xml')
def export_to_xml(self, resource_fs): def export_to_xml(self, resource_fs):
""" """
Returns an xml string representing this module, and all modules underneath it. Returns an xml string representing this module, and all modules
May also write required resources out to resource_fs underneath it. May also write required resources out to resource_fs
Assumes that modules have single parantage (that no module appears twice in the same course), Assumes that modules have single parentage (that no module appears twice
and that it is thus safe to nest modules as xml children as appropriate. in the same course), and that it is thus safe to nest modules as xml
children as appropriate.
The returned XML should be able to be parsed back into an identical XModuleDescriptor The returned XML should be able to be parsed back into an identical
using the from_xml method with the same system, org, and course XModuleDescriptor using the from_xml method with the same system, org,
and course
""" """
raise NotImplementedError('Modules must implement export_to_xml to enable xml export') raise NotImplementedError(
'Modules must implement export_to_xml to enable xml export')
# =============================== Testing =================================== # =============================== Testing ==================================
def get_sample_state(self): def get_sample_state(self):
""" """
Return a list of tuples of instance_state, shared_state. Each tuple defines a sample case for this module Return a list of tuples of instance_state, shared_state. Each tuple
defines a sample case for this module
""" """
return [('{}', '{}')] return [('{}', '{}')]
# =============================== BUILTIN METHODS =========================== # =============================== BUILTIN METHODS ==========================
def __eq__(self, other): def __eq__(self, other):
eq = (self.__class__ == other.__class__ and eq = (self.__class__ == other.__class__ and
all(getattr(self, attr, None) == getattr(other, attr, None) all(getattr(self, attr, None) == getattr(other, attr, None)
...@@ -493,38 +506,76 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -493,38 +506,76 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
if not eq: if not eq:
for attr in self.equality_attributes: for attr in self.equality_attributes:
print getattr(self, attr, None), getattr(other, attr, None), getattr(self, attr, None) == getattr(other, attr, None) print(getattr(self, attr, None),
getattr(other, attr, None),
getattr(self, attr, None) == getattr(other, attr, None))
return eq return eq
def __repr__(self): def __repr__(self):
return "{class_}({system!r}, {definition!r}, location={location!r}, metadata={metadata!r})".format( return ("{class_}({system!r}, {definition!r}, location={location!r},"
" metadata={metadata!r})".format(
class_=self.__class__.__name__, class_=self.__class__.__name__,
system=self.system, system=self.system,
definition=self.definition, definition=self.definition,
location=self.location, location=self.location,
metadata=self.metadata metadata=self.metadata
) ))
class DescriptorSystem(object): class DescriptorSystem(object):
def __init__(self, load_item, resources_fs, **kwargs): def __init__(self, load_item, resources_fs,
error_handler,
**kwargs):
""" """
load_item: Takes a Location and returns an XModuleDescriptor load_item: Takes a Location and returns an XModuleDescriptor
resources_fs: A Filesystem object that contains all of the resources_fs: A Filesystem object that contains all of the
resources needed for the course resources needed for the course
error_handler: A hook for handling errors in loading the descriptor.
Must be a function of (error_msg, exc_info=None).
See errorhandlers.py for some simple ones.
Patterns for using the error handler:
try:
x = access_some_resource()
check_some_format(x)
except SomeProblem:
msg = 'Grommet {0} is broken'.format(x)
log.exception(msg) # don't rely on handler to log
self.system.error_handler(msg)
# if we get here, work around if possible
raise # if no way to work around
OR
return 'Oops, couldn't load grommet'
OR, if not in an exception context:
if not check_something(thingy):
msg = "thingy {0} is broken".format(thingy)
log.critical(msg)
error_handler(msg)
# if we get here, work around
pass # e.g. if no workaround needed
""" """
self.load_item = load_item self.load_item = load_item
self.resources_fs = resources_fs self.resources_fs = resources_fs
self.error_handler = error_handler
class XMLParsingSystem(DescriptorSystem): class XMLParsingSystem(DescriptorSystem):
def __init__(self, load_item, resources_fs, process_xml, **kwargs): def __init__(self, load_item, resources_fs, process_xml, **kwargs):
""" """
process_xml: Takes an xml string, and returns the the XModuleDescriptor created from that xml load_item: Takes a Location and returns an XModuleDescriptor
process_xml: Takes an xml string, and returns a XModuleDescriptor
created from that xml
""" """
DescriptorSystem.__init__(self, load_item, resources_fs) DescriptorSystem.__init__(self, load_item, resources_fs, **kwargs)
self.process_xml = process_xml self.process_xml = process_xml
......
...@@ -187,7 +187,10 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -187,7 +187,10 @@ class XmlDescriptor(XModuleDescriptor):
with system.resources_fs.open(filepath) as file: with system.resources_fs.open(filepath) as file:
definition_xml = cls.file_to_xml(file) definition_xml = cls.file_to_xml(file)
except (ResourceNotFoundError, etree.XMLSyntaxError): except (ResourceNotFoundError, etree.XMLSyntaxError):
log.exception('Unable to load file contents at path %s' % filepath) msg = 'Unable to load file contents at path %s' % filepath
log.exception(msg)
system.error_handler(msg)
# if error_handler didn't reraise, work around it.
return {'data': 'Error loading file contents at path %s' % filepath} return {'data': 'Error loading file contents at path %s' % filepath}
cls.clean_metadata_from_xml(definition_xml) cls.clean_metadata_from_xml(definition_xml)
...@@ -206,20 +209,24 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -206,20 +209,24 @@ class XmlDescriptor(XModuleDescriptor):
@classmethod @classmethod
def _format_filepath(cls, category, name): def _format_filepath(cls, category, name):
return u'{category}/{name}.{ext}'.format(category=category, name=name, ext=cls.filename_extension) return u'{category}/{name}.{ext}'.format(category=category,
name=name,
ext=cls.filename_extension)
def export_to_xml(self, resource_fs): def export_to_xml(self, resource_fs):
""" """
Returns an xml string representing this module, and all modules underneath it. Returns an xml string representing this module, and all modules
May also write required resources out to resource_fs underneath it. May also write required resources out to resource_fs
Assumes that modules have single parantage (that no module appears twice in the same course), Assumes that modules have single parentage (that no module appears twice
and that it is thus safe to nest modules as xml children as appropriate. in the same course), and that it is thus safe to nest modules as xml
children as appropriate.
The returned XML should be able to be parsed back into an identical XModuleDescriptor The returned XML should be able to be parsed back into an identical
using the from_xml method with the same system, org, and course XModuleDescriptor using the from_xml method with the same system, org,
and course
resource_fs is a pyfilesystem office (from the fs package) resource_fs is a pyfilesystem object (from the fs package)
""" """
xml_object = self.definition_to_xml(resource_fs) xml_object = self.definition_to_xml(resource_fs)
self.__class__.clean_metadata_from_xml(xml_object) self.__class__.clean_metadata_from_xml(xml_object)
...@@ -244,7 +251,8 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -244,7 +251,8 @@ class XmlDescriptor(XModuleDescriptor):
attr_map = self.xml_attribute_map.get(attr, AttrMap(attr)) attr_map = self.xml_attribute_map.get(attr, AttrMap(attr))
metadata_key = attr_map.metadata_key metadata_key = attr_map.metadata_key
if metadata_key not in self.metadata or metadata_key in self._inherited_metadata: if (metadata_key not in self.metadata or
metadata_key in self._inherited_metadata):
continue continue
val = attr_map.from_metadata(self.metadata[metadata_key]) val = attr_map.from_metadata(self.metadata[metadata_key])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment