summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2018-09-27 18:41:45 -0500
committerJason Madden <jamadden@gmail.com>2018-09-28 06:49:00 -0500
commit43ff10a5e36733baf521f232792f9847265fe723 (patch)
treeab853c32e8c649f4663c39f6e6536e0b147a9849
parent7c98fbbc5b1b864ce76ef235c8152054eaf6b96f (diff)
downloadzope-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.rst3
-rw-r--r--docs/narr.rst17
-rw-r--r--src/zope/configuration/config.py49
-rw-r--r--src/zope/configuration/exceptions.py43
-rw-r--r--src/zope/configuration/tests/test_config.py2
-rw-r--r--src/zope/configuration/tests/test_xmlconfig.py18
-rw-r--r--src/zope/configuration/xmlconfig.py67
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):