# -*- coding: utf-8 -*-
import inspect
import functools
import six
from .log import log
from .empty import empty
from . import operator_dsl as OperatorDsl
from .exceptions import GrappaAssertionError
class OperatorTypes(object):
"""
OperatorTypes is used as struct to store the operator types flags.
"""
# Attribute operators have no validation logic and only defines chained
# API DSL keywords, typically for test declaration purposes.
ATTRIBUTE = 'attribute'
# Accessor operators do not accept expectation arguments and only does
# static validation logic. E.g: `true/false` operators.
ACCESSOR = 'accessor'
# Matcher operators accepts expectation arguments and does the validation
# based on them. E.g: `equal`.
MATCHER = 'matcher'
[docs]class Operator(object):
"""
Operator implements a base class with common logic and required interface
that is used by specific operator implementations.
Any operator should inherit from this class.
Attributes:
Dsl (grappa.operators_dsl): DSL for operator error messages templating
Type (grappa.operators.OperatorTypes): support operators types
kind (str): stores the operator kind
operators (list|tuple): operator keywords
aliases (list|tuple): chain attributes aliases for expressivity
information (list|tuple): optional additional help in case of error
subject_message (str|Operator.Dsl.Message): optional subject message
expected_message (str|Operator.Dsl.Message): optional expected message
operator_name (str): invokation operator name keyword
suboperators (list|tuple[grappa.Operator]): optional child operators
"""
# Shortcut alias for operator DSL module
Dsl = OperatorDsl
# Shortcut to the supported operator types
Type = OperatorTypes
# Stores operator kind
kind = OperatorTypes.MATCHER
# Operator keywords
operators = tuple()
# Chain alias keywords
aliases = tuple()
# Enable/disable operator chaining
chainable = True
# Stores assertion error tips
information = []
# Stores test execution context
ctx = None
# Stores the selected operator name when executed
operator_name = None
# Stores optional suboperators (not supported yet)
suboperators = []
# Stores optionally yielded operator result value
value = empty
# Stores operator assertion expectation
expected = empty
# Default subject error template
subject_message = Dsl.Message(
'a value of "{type}" type with content "{value}"'
'a value of "{type}" type with content "{value}"'
)
# Default expected error template
expected_message = Dsl.Message(
'a value of "{type}" type that satisfies assertion with "{value}"',
'a value of "{type}" type that does not satisfies assertion '
'with "{value}"',
)
def __init__(self, context=None, operator_name=None, fn=None,
kind=None, aliases=None, operators=None, suboperators=None):
if inspect.isfunction(fn):
self.match = fn
if kind:
self.kind = kind
if operators:
self.operators = operators
if aliases:
self.aliases = aliases
if context:
self.ctx = context
if operator_name:
self.operator_name = operator_name
if suboperators:
self.suboperators = suboperators
[docs] def match(self, *args, **kw):
raise NotImplementedError('"match" method must be implemented')
def __call__(self, *args, **kw):
"""
Overloads function invokation in current operator and creates a new
cloned operator instance.
"""
operator = Operator()
operator.__dict__ = self.__dict__.copy()
operator.__init__(*args, **kw)
return operator
[docs] def observe(matcher):
"""
Internal decorator to trigger operator hooks before/after
matcher execution.
"""
@functools.wraps(matcher)
def observer(self, subject, *expected, **kw):
# Trigger before hook, if present
if hasattr(self, 'before'):
self.before(subject, *expected, **kw)
# Trigger matcher method
result = matcher(self, subject, *expected, **kw)
# After error hook
if result is not True and hasattr(self, 'after_error'):
self.after_error(result, subject, *expected, **kw)
# After success hook
if result is True and hasattr(self, 'after_success'):
self.after_success(subject, *expected, **kw)
# Enable diff comparison on error, if needed
if not hasattr(self, 'show_diff'):
self.show_diff = all([
isinstance(subject, six.string_types),
all([isinstance(x, six.string_types) for x in expected]),
])
return result
return observer
def _make_error(self, reasons=None, error=None):
return GrappaAssertionError(
error=error,
reasons=reasons,
operator=self
)
[docs] @observe
def run_matcher(self, subject, *expected, **kw):
"""
Runs the operator matcher test function.
"""
# Update assertion expectation
self.expected = expected
_args = (subject,)
if self.kind == OperatorTypes.MATCHER:
_args += expected
try:
result = self.match(*_args, **kw)
except Exception as error:
return self._make_error(error=error)
reasons = []
if isinstance(result, tuple):
result, reasons = result
if result is False and self.ctx.negate:
return True
if result is True and not self.ctx.negate:
return True
return self._make_error(reasons=reasons)
[docs] def run(self, *args, **kw):
"""
Runs the current operator with the subject arguments to test.
This method is implemented by matchers only.
"""
log.debug('[operator] run "{}" with arguments: {}'.format(
self.__class__.__name__, args
))
if self.kind == OperatorTypes.ATTRIBUTE:
return self.match(self.ctx)
else:
return self.run_matcher(*args, **kw)
def __enter__(self):
raise NotImplementedError('operator cannot be used as "with" statement') # noqa
def __exit__(self, etype, value, traceback):
self.__enter__()