##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Interface-specific exceptions
"""
__all__ = [
# Invalid tree
'Invalid',
'DoesNotImplement',
'BrokenImplementation',
'BrokenMethodImplementation',
'MultipleInvalid',
# Other
'BadImplements',
'InvalidInterface',
]
[docs]
class Invalid(Exception):
"""A specification is violated
"""
class _TargetInvalid(Invalid):
# Internal use. Subclass this when you're describing
# a particular target object that's invalid according
# to a specific interface.
#
# For backwards compatibility, the *target* and *interface* are
# optional, and the signatures are inconsistent in their ordering.
#
# We deal with the inconsistency in ordering by defining the index
# of the two values in ``self.args``. *target* uses a marker object to
# distinguish "not given" from "given, but None", because the latter
# can be a value that gets passed to validation. For this reason, it must
# always be the last argument (we detect absence by the ``IndexError``).
_IX_INTERFACE = 0
_IX_TARGET = 1
# The exception to catch when indexing self.args indicating that
# an argument was not given. If all arguments are expected,
# a subclass should set this to ().
_NOT_GIVEN_CATCH = IndexError
_NOT_GIVEN = '<Not Given>'
def _get_arg_or_default(self, ix, default=None):
try:
return self.args[ix] # pylint:disable=unsubscriptable-object
except self._NOT_GIVEN_CATCH:
return default
@property
def interface(self):
return self._get_arg_or_default(self._IX_INTERFACE)
@property
def target(self):
return self._get_arg_or_default(self._IX_TARGET, self._NOT_GIVEN)
###
# str
#
# The ``__str__`` of self is implemented by concatenating (%s), in order,
# these properties (none of which should have leading or trailing
# whitespace):
#
# - self._str_subject
# Begin the message, including a description of the target.
# - self._str_description
# Provide a general description of the type of error, including
# the interface name if possible and relevant.
# - self._str_conjunction
# Join the description to the details. Defaults to ": ".
# - self._str_details
# Provide details about how this particular instance of the error.
# - self._str_trailer
# End the message. Usually just a period.
###
@property
def _str_subject(self):
target = self.target
if target is self._NOT_GIVEN:
return "An object"
return "The object {!r}".format(target)
@property
def _str_description(self):
return "has failed to implement interface %s" % (
self.interface or '<Unknown>'
)
_str_conjunction = ": "
_str_details = "<unknown>"
_str_trailer = '.'
def __str__(self):
return "{} {}{}{}{}".format(
self._str_subject,
self._str_description,
self._str_conjunction,
self._str_details,
self._str_trailer
)
class DoesNotImplement(_TargetInvalid):
"""
DoesNotImplement(interface[, target])
The *target* (optional) does not implement the *interface*.
.. versionchanged:: 5.0.0
Add the *target* argument and attribute, and change the resulting
string value of this object accordingly.
"""
_str_details = "Does not declaratively implement the interface"
class BrokenImplementation(_TargetInvalid):
"""
BrokenImplementation(interface, name[, target])
The *target* (optional) is missing the attribute *name*.
.. versionchanged:: 5.0.0
Add the *target* argument and attribute, and change the resulting
string value of this object accordingly.
The *name* can either be a simple string or a ``Attribute`` object.
"""
_IX_NAME = _TargetInvalid._IX_INTERFACE + 1
_IX_TARGET = _IX_NAME + 1
@property
def name(self):
return self.args[1] # pylint:disable=unsubscriptable-object
@property
def _str_details(self):
return "The %s attribute was not provided" % (
repr(self.name) if isinstance(self.name, str) else self.name
)
class BrokenMethodImplementation(_TargetInvalid):
"""
BrokenMethodImplementation(method, message[, implementation, interface, target])
The *target* (optional) has a *method* in *implementation* that violates
its contract in a way described by *mess*.
.. versionchanged:: 5.0.0
Add the *interface* and *target* argument and attribute,
and change the resulting string value of this object accordingly.
The *method* can either be a simple string or a ``Method`` object.
.. versionchanged:: 5.0.0
If *implementation* is given, then the *message* will have the
string "implementation" replaced with an short but informative
representation of *implementation*.
"""
_IX_IMPL = 2
_IX_INTERFACE = _IX_IMPL + 1
_IX_TARGET = _IX_INTERFACE + 1
@property
def method(self):
return self.args[0] # pylint:disable=unsubscriptable-object
@property
def mess(self):
return self.args[1] # pylint:disable=unsubscriptable-object
@staticmethod
def __implementation_str(impl):
# It could be a callable or some arbitrary object, we don't
# know yet.
import inspect # Inspect is a heavy-weight dependency, lots of imports
try:
sig = inspect.signature
formatsig = str
except AttributeError:
sig = inspect.getargspec
f = inspect.formatargspec
formatsig = lambda sig: f(*sig) # pylint:disable=deprecated-method
try:
sig = sig(impl)
except (ValueError, TypeError):
# Unable to introspect. Darn.
# This could be a non-callable, or a particular builtin,
# or a bound method that doesn't even accept 'self', e.g.,
# ``Class.method = lambda: None; Class().method``
return repr(impl)
try:
name = impl.__qualname__
except AttributeError:
name = impl.__name__
return name + formatsig(sig)
@property
def _str_details(self):
impl = self._get_arg_or_default(self._IX_IMPL, self._NOT_GIVEN)
message = self.mess
if impl is not self._NOT_GIVEN and 'implementation' in message:
message = message.replace("implementation", '%r')
message = message % (self.__implementation_str(impl),)
return 'The contract of {} is violated because {}'.format(
repr(self.method) if isinstance(self.method, str) else self.method,
message,
)
class MultipleInvalid(_TargetInvalid):
"""
The *target* has failed to implement the *interface* in
multiple ways.
The failures are described by *exceptions*, a collection of
other `Invalid` instances.
.. versionadded:: 5.0
"""
_NOT_GIVEN_CATCH = ()
def __init__(self, interface, target, exceptions):
super().__init__(interface, target, tuple(exceptions))
@property
def exceptions(self):
return self.args[2] # pylint:disable=unsubscriptable-object
@property
def _str_details(self):
# It would be nice to use tabs here, but that
# is hard to represent in doctests.
return '\n ' + '\n '.join(
x._str_details.strip() if isinstance(x, _TargetInvalid) else str(x)
for x in self.exceptions
)
_str_conjunction = ':' # We don't want a trailing space, messes up doctests
_str_trailer = ''
class InvalidInterface(Exception):
"""The interface has invalid contents
"""
class BadImplements(TypeError):
"""An implementation assertion is invalid
because it doesn't contain an interface or a sequence of valid
implementation assertions.
"""