diff options
author | Jason Madden <jamadden@gmail.com> | 2018-09-27 18:41:45 -0500 |
---|---|---|
committer | Jason Madden <jamadden@gmail.com> | 2018-09-28 06:49:00 -0500 |
commit | 43ff10a5e36733baf521f232792f9847265fe723 (patch) | |
tree | ab853c32e8c649f4663c39f6e6536e0b147a9849 | |
parent | 7c98fbbc5b1b864ce76ef235c8152054eaf6b96f (diff) | |
download | zope-configuration-43ff10a5e36733baf521f232792f9847265fe723.tar.gz |
Simplify exception chaining and nested exception error messages.
Fixes #43
Given a/b/c/d.zcml included in that order with an error in d.zcml,
previously we would get this:
```
Traceback (most recent call last):
...
raise InvalidDottedName(value).with_field_and_value(self, value)
zope.schema.interfaces.InvalidDottedName: invalid dotted name
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
raise InvalidDottedName(value).with_field_and_value(self, value)
zope.configuration.exceptions.ConfigurationError: ('Invalid value for', 'package', 'invalid dotted name')
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
raise InvalidDottedName(value).with_field_and_value(self, value)
zope.configuration.xmlconfig.ZopeXMLConfigurationError: File "/tmp/d.zcml", line 2.4-2.58
ConfigurationError: ('Invalid value for', 'package', 'invalid dotted name')
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
raise InvalidDottedName(value).with_field_and_value(self, value)
zope.configuration.xmlconfig.ZopeXMLConfigurationError: File "/tmp/c.zcml", line 2.4-2.29
ZopeXMLConfigurationError: File "/tmp/d.zcml", line 2.4-2.58
ConfigurationError: ('Invalid value for', 'package', 'invalid dotted name')
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
raise InvalidDottedName(value).with_field_and_value(self, value)
zope.configuration.xmlconfig.ZopeXMLConfigurationError: File "/tmp/b.zcml", line 2.4-2.29
ZopeXMLConfigurationError: File "/tmp/c.zcml", line 2.4-2.29
ZopeXMLConfigurationError: File "/tmp/d.zcml", line 2.4-2.58
ConfigurationError: ('Invalid value for', 'package', 'invalid dotted name')
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<string>", line 1, in <module>
...
raise InvalidDottedName(value).with_field_and_value(self, value)
zope.configuration.xmlconfig.ZopeXMLConfigurationError: File "/tmp/a.zcml", line 2.4-2.29
ZopeXMLConfigurationError: File "/tmp/b.zcml", line 2.4-2.29
ZopeXMLConfigurationError: File "/tmp/c.zcml", line 2.4-2.29
ZopeXMLConfigurationError: File "/tmp/d.zcml", line 2.4-2.58
ConfigurationError: ('Invalid value for', 'package', 'invalid dotted name')
```
Now we get the simpler:
```
Traceback (most recent call last):
...
File "/Users/jmadden/Projects/GithubSources/zope.schema/src/zope/schema/_field.py", line 670, in _validate
raise InvalidDottedName(value).with_field_and_value(self, value)
zope.schema.interfaces.InvalidDottedName: invalid dotted name
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<string>", line 1, in <module>
...
raise InvalidDottedName(value).with_field_and_value(self, value)
zope.configuration.exceptions.ConfigurationError: Invalid value for 'package': InvalidDottedName('invalid dotted name')
zope.schema.interfaces.InvalidDottedName: invalid dotted name
File "/tmp/d.zcml", line 2.4-2.58
File "/tmp/c.zcml", line 2.4-2.29
File "/tmp/b.zcml", line 2.4-2.29
File "/tmp/a.zcml", line 2.4-2.29
```
-rw-r--r-- | CHANGES.rst | 3 | ||||
-rw-r--r-- | docs/narr.rst | 17 | ||||
-rw-r--r-- | src/zope/configuration/config.py | 49 | ||||
-rw-r--r-- | src/zope/configuration/exceptions.py | 43 | ||||
-rw-r--r-- | src/zope/configuration/tests/test_config.py | 2 | ||||
-rw-r--r-- | src/zope/configuration/tests/test_xmlconfig.py | 18 | ||||
-rw-r--r-- | src/zope/configuration/xmlconfig.py | 67 |
7 files changed, 124 insertions, 75 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 93d7bef..19389a2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,8 @@ Changes 4.2.3 (unreleased) ------------------ -- Nothing changed yet. +- Simplify exception chaining and nested exception error messages. + See `issue 43 <https://github.com/zopefoundation/zope.configuration/issues/43>`_. 4.2.2 (2018-09-27) diff --git a/docs/narr.rst b/docs/narr.rst index fc7d804..fa60dc4 100644 --- a/docs/narr.rst +++ b/docs/narr.rst @@ -1162,16 +1162,15 @@ redefine our directives: .. doctest:: >>> from zope.configuration.xmlconfig import string - >>> from zope.configuration.xmlconfig import ZopeXMLConfigurationError - >>> try: - ... v = string( + >>> from zope.configuration.exceptions import ConfigurationError + >>> v = string( ... '<text xmlns="http://sample.namespaces.zope.org/schema" name="x" />', ... context) - ... except ZopeXMLConfigurationError as e: - ... v = e - >>> print(v) - File "<string>", line 1.0 - ConfigurationError: The directive ('http://sample.namespaces.zope.org/schema', 'text') cannot be used in this context + Traceback (most recent call last): + ... + zope.configuration.exceptions.ConfigurationError: The directive ('http://sample.namespaces.zope.org/schema', 'text') cannot be used in this context + File "<string>", line 1.0 + Let's see what happens if we declare duplicate fields: @@ -1187,7 +1186,7 @@ Let's see what happens if we declare duplicate fields: ... </schema> ... ''', ... context) - ... except ZopeXMLConfigurationError as e: + ... except ConfigurationError as e: ... v = e >>> print(v) File "<string>", line 5.7-5.24 diff --git a/src/zope/configuration/config.py b/src/zope/configuration/config.py index e7ba289..1baf5b3 100644 --- a/src/zope/configuration/config.py +++ b/src/zope/configuration/config.py @@ -27,6 +27,7 @@ from zope.schema import URI from zope.schema import ValidationError from zope.configuration.exceptions import ConfigurationError +from zope.configuration.exceptions import ConfigurationWrapperError from zope.configuration.interfaces import IConfigurationContext from zope.configuration.interfaces import IGroupingContext from zope.configuration.fields import GlobalInterface @@ -41,7 +42,6 @@ __all__ = [ 'ConfigurationContext', 'ConfigurationAdapterRegistry', 'ConfigurationMachine', - 'ConfigurationExecutionError', 'IStackItem', 'SimpleStackItem', 'RootStackItem', @@ -739,17 +739,12 @@ class ConfigurationMachine(ConfigurationAdapterRegistry, ConfigurationContext): ... (2, f, (2,)), ... (3, bad, (), {}, (), 'oops') ... ] - >>> try: - ... v = context.execute_actions() - ... except ConfigurationExecutionError as e: - ... v = e - >>> lines = str(v).splitlines() - >>> 'AttributeError' in lines[0] - True - >>> lines[0].endswith("'function' object has no attribute 'xxx'") - True - >>> lines[1:] - [' in:', ' oops'] + + >>> context.execute_actions() + Traceback (most recent call last): + ... + zope.configuration.config.ConfigurationExecutionError: oops + AttributeError: 'function' object has no attribute 'xxx' Note that actions executed before the error still have an effect: @@ -769,29 +764,31 @@ class ConfigurationMachine(ConfigurationAdapterRegistry, ConfigurationContext): info = action['info'] try: callable(*args, **kw) - except pass_through_exceptions: - raise - except Exception: - t, v, tb = sys.exc_info() + except BaseException as ex: try: - reraise(ConfigurationExecutionError(t, v, info), - None, tb) + if isinstance(ex, ConfigurationError): + ex.append_details(info) + raise + if isinstance(ex, pass_through_exceptions): + raise + if not isinstance(ex, Exception): + # BaseException + raise + + # Wrap it up and raise. + reraise(ConfigurationExecutionError(info, ex), + None, sys.exc_info()[2]) finally: - del t, v, tb + del ex finally: if clear: del self.actions[:] -class ConfigurationExecutionError(ConfigurationError): +class ConfigurationExecutionError(ConfigurationWrapperError): """ An error occurred during execution of a configuration action """ - def __init__(self, etype, evalue, info): - self.etype, self.evalue, self.info = etype, evalue, info - - def __str__(self): # pragma: no cover - return "%s: %s\n in:\n %s" % (self.etype, self.evalue, self.info) ############################################################################## # Stack items @@ -1671,7 +1668,7 @@ def toargs(context, schema, data): try: args[str(name)] = field.fromUnicode(s) except ValidationError as v: - reraise(ConfigurationError("Invalid value for", n, str(v)), + reraise(ConfigurationError("Invalid value for %r: %r" % (n, v)).append_details(v), None, sys.exc_info()[2]) elif field.required: # if the default is valid, we can use that: diff --git a/src/zope/configuration/exceptions.py b/src/zope/configuration/exceptions.py index dce50a7..1262035 100644 --- a/src/zope/configuration/exceptions.py +++ b/src/zope/configuration/exceptions.py @@ -14,6 +14,8 @@ """Standard configuration errors """ +import traceback + __all__ = [ 'ConfigurationError', ] @@ -21,3 +23,44 @@ __all__ = [ class ConfigurationError(Exception): """There was an error in a configuration """ + + # A list of strings or things that can be converted to strings, + # added by append_details as we walk back up the call/include stack. + _details = () + + def append_details(self, info): + if isinstance(info, BaseException): + info = traceback.format_exception_only(type(info), info) + # Trim trailing newline + info[-1] = info[-1].rstrip() + self._details += tuple(info) + else: + self._details += (info,) + return self + + def __str__(self): + s = super(ConfigurationError, self).__str__() + for i in self._details: + s += '\n ' + str(i) + return s + + def __repr__(self): + s = super(ConfigurationError, self).__repr__() + for i in self._details: + s += '\n ' + repr(i) + return s + + +class ConfigurationWrapperError(ConfigurationError): + + USE_INFO_REPR = False + + def __init__(self, info, exception): + super(ConfigurationWrapperError, self).__init__(repr(info) if self.USE_INFO_REPR else info) + self.append_details(exception) + + # This stuff is undocumented and not used but we store + # for backwards compatibility + self.info = info + self.etype = type(exception) + self.evalue = exception diff --git a/src/zope/configuration/tests/test_config.py b/src/zope/configuration/tests/test_config.py index 4bd5930..ec100af 100644 --- a/src/zope/configuration/tests/test_config.py +++ b/src/zope/configuration/tests/test_config.py @@ -1810,7 +1810,7 @@ class Test_toargs(unittest.TestCase): with self.assertRaises(ConfigurationError) as exc: self._callFUT(context, ISchema, {'count': '-1'}) self.assertEqual(exc.exception.args, - ('Invalid value for', 'count', '(-1, 0)')) + ("Invalid value for 'count': TooSmall(-1, 0)",)) class Test_expand_action(unittest.TestCase): diff --git a/src/zope/configuration/tests/test_xmlconfig.py b/src/zope/configuration/tests/test_xmlconfig.py index e75a8f0..9e19b5a 100644 --- a/src/zope/configuration/tests/test_xmlconfig.py +++ b/src/zope/configuration/tests/test_xmlconfig.py @@ -29,6 +29,7 @@ BVALUE = u'bvalue' # pylint:disable=protected-access class ZopeXMLConfigurationErrorTests(unittest.TestCase): + maxDiff = None def _getTargetClass(self): from zope.configuration.xmlconfig import ZopeXMLConfigurationError @@ -38,11 +39,16 @@ class ZopeXMLConfigurationErrorTests(unittest.TestCase): return self._getTargetClass()(*args, **kw) def test___str___uses_repr_of_info(self): - zxce = self._makeOne('info', Exception, 'value') - self.assertEqual(str(zxce), "'info'\n Exception: value") + zxce = self._makeOne('info', Exception('value')) + self.assertEqual( + str(zxce), + "'info'\n Exception: value" + ) + class ZopeSAXParseExceptionTests(unittest.TestCase): + maxDiff = None def _getTargetClass(self): from zope.configuration.xmlconfig import ZopeSAXParseException @@ -53,11 +59,15 @@ class ZopeSAXParseExceptionTests(unittest.TestCase): def test___str___not_a_sax_error(self): zxce = self._makeOne(Exception('Not a SAX error')) - self.assertEqual(str(zxce), "Not a SAX error") + self.assertEqual( + str(zxce), + "Not a SAX error\n Exception: Not a SAX error") def test___str___w_a_sax_error(self): zxce = self._makeOne(Exception('filename.xml:24:32:WAAA')) - self.assertEqual(str(zxce), 'File "filename.xml", line 24.32, WAAA') + self.assertEqual( + str(zxce), + 'File "filename.xml", line 24.32, WAAA\n Exception: filename.xml:24:32:WAAA') class ParserInfoTests(unittest.TestCase): diff --git a/src/zope/configuration/xmlconfig.py b/src/zope/configuration/xmlconfig.py index e979e13..68df11b 100644 --- a/src/zope/configuration/xmlconfig.py +++ b/src/zope/configuration/xmlconfig.py @@ -40,14 +40,13 @@ from zope.configuration.config import GroupingContextDecorator from zope.configuration.config import GroupingStackItem from zope.configuration.config import resolveConflicts from zope.configuration.exceptions import ConfigurationError +from zope.configuration.exceptions import ConfigurationWrapperError from zope.configuration.fields import GlobalObject from zope.configuration.zopeconfigure import IZopeConfigure from zope.configuration.zopeconfigure import ZopeConfigure from zope.configuration._compat import reraise __all__ = [ - 'ZopeXMLConfigurationError', - 'ZopeSAXParseException', 'ParserInfo', 'ConfigurationHandler', 'processxmlfile', @@ -70,7 +69,7 @@ ZCML_NAMESPACE = "http://namespaces.zope.org/zcml" ZCML_CONDITION = (ZCML_NAMESPACE, u"condition") -class ZopeXMLConfigurationError(ConfigurationError): +class ZopeXMLConfigurationError(ConfigurationWrapperError): """ Zope XML Configuration error @@ -80,41 +79,40 @@ class ZopeXMLConfigurationError(ConfigurationError): Example >>> from zope.configuration.xmlconfig import ZopeXMLConfigurationError - >>> v = ZopeXMLConfigurationError("blah", AttributeError, "xxx") + >>> v = ZopeXMLConfigurationError("blah", AttributeError("xxx")) >>> print(v) 'blah' AttributeError: xxx """ - def __init__(self, info, etype, evalue): - self.info, self.etype, self.evalue = info, etype, evalue - def __str__(self): - # Only use the repr of the info. This is because we expect to - # get a parse info and we only want the location information. - return "%s\n %s: %s" % ( - repr(self.info), self.etype.__name__, self.evalue) + USE_INFO_REPR = True + -class ZopeSAXParseException(ConfigurationError): +class ZopeSAXParseException(ConfigurationWrapperError): """ Sax Parser errors, reformatted in an emacs friendly way. Example >>> from zope.configuration.xmlconfig import ZopeSAXParseException - >>> v = ZopeSAXParseException("foo.xml:12:3:Not well formed") + >>> v = ZopeSAXParseException(Exception("foo.xml:12:3:Not well formed")) >>> print(v) File "foo.xml", line 12.3, Not well formed + Exception: foo.xml:12:3:Not well formed """ - def __init__(self, v): - self._v = v - def __str__(self): - v = self._v - s = tuple(str(v).split(':')) + def __init__(self, exception): + s = tuple(str(exception).split(':')) if len(s) == 4: - return 'File "%s", line %s.%s, %s' % s + info = 'File "%s", line %s.%s, %s' % s else: - return str(v) + info = str(exception) + super(ZopeSAXParseException, self).__init__(info, exception) + + @property + def _v(self): + # BWC for testing only + return self.evalue class ParserInfo(object): r""" @@ -238,6 +236,17 @@ class ConfigurationHandler(ContentHandler): def characters(self, text): self.context.getInfo().characters(text) + def _handle_exception(self, ex, info): + if self.testing: + raise + if isinstance(ex, ConfigurationError): + ex.append_details(repr(info)) + raise + else: + exc = ZopeXMLConfigurationError(info, ex) + reraise(exc, + None, sys.exc_info()[2]) + def startElementNS(self, name, qname, attrs): if self.ignore_depth: self.ignore_depth += 1 @@ -268,13 +277,8 @@ class ConfigurationHandler(ContentHandler): try: self.context.begin(name, data, info) - except Exception: - if self.testing: - raise - reraise(ZopeXMLConfigurationError(info, - sys.exc_info()[0], - sys.exc_info()[1]), - None, sys.exc_info()[2]) + except Exception as ex: + self._handle_exception(ex, info) self.context.setInfo(info) @@ -398,13 +402,8 @@ class ConfigurationHandler(ContentHandler): try: self.context.end() - except Exception: - if self.testing: - raise - reraise(ZopeXMLConfigurationError(info, - sys.exc_info()[0], - sys.exc_info()[1]), - None, sys.exc_info()[2]) + except Exception as ex: + self._handle_exception(ex, info) def processxmlfile(file, context, testing=False): |