From 6a7c967c4df0db7ddcc1dcdeb09fbe2ce637d988 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Fri, 28 Sep 2018 08:51:27 -0500 Subject: [PATCH] Conflict errors should also report any details. Also tweak config.py docs to link to exception classes. --- src/zope/configuration/config.py | 57 ++++++++++++--------- src/zope/configuration/tests/test_config.py | 8 +++ 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/zope/configuration/config.py b/src/zope/configuration/config.py index 6ce6d06..4b843a9 100644 --- a/src/zope/configuration/config.py +++ b/src/zope/configuration/config.py @@ -74,10 +74,14 @@ class ConfigurationContext(object): - """Mix-in that implements IConfigurationContext + """ + Mix-in for implementing. + :class:`zope.configuration.interfaces.IConfigurationContext`. - Subclasses provide a ``package`` attribute and a ``basepath`` - attribute. If the base path is not None, relative paths are + Note that this class itself does not actually declare that it + implements that interface; the subclass must do that. In addition, + subclasses must provide a ``package`` attribute and a ``basepath`` + attribute. If the base path is not None, relative paths are converted to absolute paths using the the base path. If the package is not none, relative imports are performed relative to the package. @@ -91,34 +95,38 @@ class ConfigurationContext(object): attribute. The include path is appended to each action and is used when - resolving conflicts among actions. Normally, only the a + resolving conflicts among actions. Normally, only the a ConfigurationMachine provides the actions attribute. Decorators simply use the actions of the context they decorate. The - ``includepath`` attribute is a tuple of names. Each name is + ``includepath`` attribute is a tuple of names. Each name is typically the name of an included configuration file. The ``info`` attribute contains descriptive information helpful - when reporting errors. If not set, it defaults to an empty string. - - The actions attribute is a sequence of dictionaries where each dictionary - has the following keys: + when reporting errors. If not set, it defaults to an empty string. - - ``discriminator``, a value that identifies the action. Two actions - that have the same (non None) discriminator conflict. + The actions attribute is a sequence of dictionaries where each + dictionary has the following keys: - - ``callable``, an object that is called to execute the action, + - ``discriminator``, a value that identifies the action. Two + actions that have the same (non None) discriminator + conflict. - - ``args``, positional arguments for the action + - ``callable``, an object that is called to execute the + action, - - ``kw``, keyword arguments for the action + - ``args``, positional arguments for the action - - ``includepath``, a tuple of include file names (defaults to ()) + - ``kw``, keyword arguments for the action - - ``info``, an object that has descriptive information about - the action (defaults to '') + - ``includepath``, a tuple of include file names (defaults to + ()) + - ``info``, an object that has descriptive information about + the action (defaults to '') """ + # pylint:disable=no-member + def __init__(self): super(ConfigurationContext, self).__init__() self._seen_files = set() @@ -666,8 +674,8 @@ class ConfigurationMachine(ConfigurationAdapterRegistry, ConfigurationContext): info = '' #: These `Exception` subclasses are allowed to be raised from `execute_actions` - #: without being re-wrapped into a `ConfigurationExecutionError`. (`BaseException` - #: instances are never wrapped.) + #: without being re-wrapped into a `~.ConfigurationError`. (`BaseException` + #: and other `~.ConfigurationError` instances are never wrapped.) #: #: Users of instances of this class may modify this before calling `execute_actions` #: if they need to propagate specific exceptions. @@ -727,9 +735,8 @@ def execute_actions(self, clear=True, testing=False): [('f', (1,), {}), ('f', (2,), {})] If the action raises an error, we convert it to a - `ConfigurationExecutionError`. + `~.ConfigurationError`. - >>> from zope.configuration.config import ConfigurationExecutionError >>> output = [] >>> def bad(): ... bad.xxx @@ -751,7 +758,7 @@ def execute_actions(self, clear=True, testing=False): >>> output [('f', (1,), {}), ('f', (2,), {})] - If the exception was already a `ConfigurationError`, it is raised + If the exception was already a `~.ConfigurationError`, it is raised as-is with the action's ``info`` added. >>> def bad(): @@ -1808,9 +1815,10 @@ def bypath(ainfo): class ConfigurationConflictError(ConfigurationError): def __init__(self, conflicts): + super(ConfigurationConflictError, self).__init__() self._conflicts = conflicts - def __str__(self): #pragma NO COVER + def _with_details(self, opening, detail_formatter): r = ["Conflicting configuration actions"] for discriminator, infos in sorted(self._conflicts.items()): r.append(" For: %s" % (discriminator, )) @@ -1818,7 +1826,8 @@ def __str__(self): #pragma NO COVER for line in text_type(info).rstrip().split(u'\n'): r.append(u" " + line) - return "\n".join(r) + opening = "\n".join(r) + return super(ConfigurationConflictError, self)._with_details(opening, detail_formatter) ############################################################################## diff --git a/src/zope/configuration/tests/test_config.py b/src/zope/configuration/tests/test_config.py index ec100af..4a62ea0 100644 --- a/src/zope/configuration/tests/test_config.py +++ b/src/zope/configuration/tests/test_config.py @@ -1974,6 +1974,14 @@ def _b(): "For: ('a', 1)\n conflict!\n conflict2!", str(exc.exception)) + exc.exception.add_details('a detail') + + self.assertEqual( + "Conflicting configuration actions\n " + "For: ('a', 1)\n conflict!\n conflict2!\n" + " a detail", + str(exc.exception)) + def test_wo_discriminators_final_sorting_order(self): from zope.configuration.config import expand_action def _a():