Source code for nti.testing.matchers

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Hamcrest matchers for testing.
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

# stdlib imports
try:
    from collections.abc import Sequence, Mapping
except ImportError: # pragma: no cover
    # Python 2
    from collections import Sequence, Mapping # pylint:disable=deprecated-class
import pprint

import six
from zope.interface.exceptions import Invalid
from zope.interface.verify import verifyObject
from zope.schema import ValidationError
from zope.schema import getValidationErrors


import hamcrest
from hamcrest import assert_that
from hamcrest import has_length
from hamcrest import is_
from hamcrest import is_not
from hamcrest.core.base_description import BaseDescription
from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.library.collection.is_empty import empty

__docformat__ = "restructuredtext en"

__all__ = [
    'has_length',
    'has_attr',
    'is_true',
    'is_false',
    'provides',
    'verifiably_provides',
    'validly_provides',
    'implements',
    'validated_by',
    'not_validated_by',
    'aq_inContextOf',
    'TypeCheckedDict',
]

is_empty = empty # bwc

has_attr = hamcrest.library.has_property

class BoolMatcher(BaseMatcher):
    def __init__(self, value):
        super(BoolMatcher, self).__init__()
        self.value = value

    def _matches(self, item):
        return bool(item) == self.value

    def describe_to(self, description):
        description.append_text('object with bool() value ').append(str(self.value))

    def __repr__(self):
        return 'object with bool() value ' + str(self.value)

[docs]def is_true(): """ Matches an object with a true boolean value. """ return BoolMatcher(True)
[docs]def is_false(): """ Matches an object with a false boolean value. """ return BoolMatcher(False)
class Provides(BaseMatcher): def __init__(self, iface): super(Provides, self).__init__() self.iface = iface def _matches(self, item): return self.iface.providedBy(item) def describe_to(self, description): description.append_text('object providing') \ .append(str(self.iface)) def __repr__(self): return 'object providing' + str(self.iface)
[docs]def provides(iface): """ Matches an object that provides the given interface. """ return Provides(iface)
class VerifyProvides(BaseMatcher): def __init__(self, iface): super(VerifyProvides, self).__init__() self.iface = iface def _matches(self, item): try: verifyObject(self.iface, item) except Invalid: return False else: return True def describe_to(self, description): description.append_text('object verifiably providing ').append_description_of(self.iface) def describe_mismatch(self, item, mismatch_description): md = mismatch_description try: verifyObject(self.iface, item) except Invalid as x: # Beginning in zope.interface 5, the Invalid exception subclasses # like BrokenImplementation, DoesNotImplement, etc, all typically # have a much nicer error message than they used to, better than we # were producing. This is especially true now that MultipleInvalid # is a thing. x = str(x).strip() md.append_text("Using class ").append_description_of(type(item)).append_text(' ') if x.startswith('The object '): x = x[len("The object "):] x = 'the object ' + x x = x.replace('\n ', '\n ') md.append_text(x)
[docs]def verifiably_provides(*ifaces): """ Matches if the object verifiably provides the correct interface(s), as defined by :func:`zope.interface.verify.verifyObject`. This means having the right attributes and methods with the right signatures. .. note:: This does **not** test schema compliance. For that (stricter) test, see :func:`validly_provides`. """ if len(ifaces) == 1: return VerifyProvides(ifaces[0]) return hamcrest.all_of(*[VerifyProvides(x) for x in ifaces])
class VerifyValidSchema(BaseMatcher): def __init__(self, iface): super(VerifyValidSchema, self).__init__() self.iface = iface def _matches(self, item): errors = getValidationErrors(self.iface, item) return not errors def describe_to(self, description): description.append_text('object validly providing ').append(str(self.iface)) def describe_mismatch(self, item, mismatch_description): x = None md = mismatch_description md.append_text(str(type(item))) errors = getValidationErrors(self.iface, item) for attr, exc in errors: try: raise exc except ValidationError: md.append_text(' has attribute "') md.append_text(attr) md.append_text('" with error "') md.append_text(repr(exc)) md.append_text('"\n\t ') except Invalid as x: # pragma: no cover md.append_text(str(x))
[docs]def validly_provides(*ifaces): """ Matches if the object verifiably and validly provides the given schema (interface(s)). Verification is done with :mod:`zope.interface` and :func:`verifiably_provides`, while validation is done with :func:`zope.schema.getValidationErrors`. """ if len(ifaces) == 1: the_schema = ifaces[0] return hamcrest.all_of(verifiably_provides(the_schema), VerifyValidSchema(the_schema)) prov = verifiably_provides(*ifaces) valid = [VerifyValidSchema(x) for x in ifaces] return hamcrest.all_of(prov, *valid)
class Implements(BaseMatcher): def __init__(self, iface): super(Implements, self).__init__() self.iface = iface def _matches(self, item): return self.iface.implementedBy(item) def describe_to(self, description): description.append_text('object implementing') description.append_description_of(self.iface)
[docs]def implements(iface): """ Matches if the object implements (is a factory for) the given interface. .. seealso:: :meth:`zope.interface.interfaces.ISpecification.implementedBy` """ return Implements(iface)
class ValidatedBy(BaseMatcher): def __init__(self, field, invalid=Invalid): super(ValidatedBy, self).__init__() self.field = field self.invalid = invalid def _matches(self, item): try: self.field.validate(item) except self.invalid: return False else: return True def describe_to(self, description): description.append_text('data validated by').append_description_of(self.field) def describe_mismatch(self, item, mismatch_description): ex = None try: self.field.validate(item) except self.invalid as e: ex = e mismatch_description.append_description_of(self.field) mismatch_description.append_text(' failed to validate ') mismatch_description.append_description_of(item) mismatch_description.append_text(' with ') mismatch_description.append_description_of(ex)
[docs]def validated_by(field, invalid=Invalid): """ Matches if the data is validated by the given ``IField``. :keyword exception invalid: The types of exceptions that are considered invalid. Anything other than this is allowed to be raised. .. versionchanged:: 2.0.1 Add ``invalid`` and change it from ``Exception`` to :class:`zope.interface.interfaces.Invalid` """ return ValidatedBy(field, invalid=invalid)
[docs]def not_validated_by(field, invalid=Invalid): """ Matches if the data is NOT validated by the given IField. :keyword exception invalid: The types of exceptions that are considered invalid. Anything other than this is allowed to be raised. .. versionchanged:: 2.0.1 Add ``invalid`` and change it from ``Exception`` to :class:`zope.interface.interfaces.Invalid` """ return is_not(validated_by(field, invalid=invalid))
def _aq_inContextOf_NotImplemented(child, parent): return False try: from Acquisition import aq_inContextOf as _aq_inContextOf except ImportError: # pragma: no cover # acquisition not installed _aq_inContextOf = _aq_inContextOf_NotImplemented class AqInContextOf(BaseMatcher): def __init__(self, parent): super(AqInContextOf, self).__init__() self.parent = parent def _matches(self, item): if hasattr(item, 'aq_inContextOf'): # wrappers return item.aq_inContextOf(self.parent) return _aq_inContextOf(item, self.parent) # not wrapped, but maybe __parent__ chain def describe_to(self, description): description.append_text('object in context of ') description.append_description_of(self.parent) def describe_mismatch(self, item, mismatch_description): if _aq_inContextOf is _aq_inContextOf_NotImplemented: mismatch_description.append_text('Acquisition was not installed.') return mismatch_description.append_description_of(item) mismatch_description.append_text(' was not in the context of ') mismatch_description.append_description_of(self.parent) mismatch_description.append_text('; its lineage is ') lineage = [] while item is not None: try: item = item.__parent__ except AttributeError: item = None if item is not None: lineage.append(item) mismatch_description.append_description_of(lineage)
[docs]def aq_inContextOf(parent): """ Matches if the data is in the acquisition context of the given object. """ return AqInContextOf(parent)
# Patch hamcrest for better descriptions of maps (json data) # and sequences if six.PY3: from io import StringIO else: # pragma: no cover from cStringIO import StringIO # pylint:disable=import-error _orig_append_description_of = BaseDescription.append_description_of def _append_description_of_map(self, value): if not hasattr(value, 'describe_to'): if isinstance(value, (Mapping, Sequence)): sio = StringIO() pprint.pprint(value, sio) self.append(sio.getvalue()) return self return _orig_append_description_of(self, value) BaseDescription.append_description_of = _append_description_of_map
[docs]class TypeCheckedDict(dict): "A dictionary that ensures keys and values are of the required type when set" def __init__(self, key_class=object, val_class=object, notify=None): dict.__init__(self) self.key_class = key_class self.val_class = val_class self.notify = notify def __setitem__(self, key, val): assert_that(key, is_(self.key_class)) assert_that(val, is_(self.val_class)) dict.__setitem__(self, key, val) if self.notify: self.notify(key, val)