diff -Nru doublex-1.6.6/.hgtags doublex-1.7.2/.hgtags --- doublex-1.6.6/.hgtags 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/.hgtags 2013-11-07 21:00:04.000000000 +0000 @@ -9,3 +9,6 @@ bc4f19e466eb484e0046e17ad7821048d35c5a9e v1.6.3 9ad7b1696f919753306228eb2d8df8c92cc5f6ba v1.6.4 1094f91a921bb89a0e3fc2abedda9d91a8fbbd40 v1.6.5 +3fe85008b9ee8ff34a456c71f20afcedf42e8ab4 v1.6.6 +916c8e557345335449a3a85c70b1413a331e2136 v1.7 +c16348f54bf84ca75b10ee1290a272f321d82cf5 v1.7.1 diff -Nru doublex-1.6.6/CHANGES.rst doublex-1.7.2/CHANGES.rst --- doublex-1.6.6/CHANGES.rst 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/CHANGES.rst 2013-11-07 21:00:04.000000000 +0000 @@ -1,44 +1,70 @@ -0.20130215 -========== +20131107 +======== + +- Release 1.7.2 +- [NEW] support for varargs (*args, **kargs) methods +- [NEW] tracer for doubles, methods and properties + +20130712 +======== + +- Release 1.6.8 +- [NEW] with_some_args matcher +- [NEW] set_default_behavior module function to define behavior for non stubbed methods. + +20130513 +======== + +- ANY_ARG is not allowed as keyword value +- ANY_ARG must be the last positional argument value + +20130427 +======== + +- Release 1.6.6 +- [FIXED] stub/empty_stub were missing in pyDoubles wrapper + +20130215 +======== - Release 1.6.3 - async race condition bug fixed -0.20130211 -========== +20130211 +======== - Access to spy invocations with _method_.calls -0.20130110 -========== +20130110 +======== - Release 1.6 - Ad-hoc stub attributes - AttributeFactory callable types: function, method (Closes: #bitbucket:issue/7) - BuiltingSignature for non Python functions -0.20121118 -========== +20121118 +======== - ProxySpy propagates stubbed invocations too -0.20121025 -========== +20121025 +======== - Merge feature-async branch: Spy async checking -0.20121008 -========== +20121008 +======== - release 1.5 to replace pyDoubles -0.20120928 -========== +20120928 +======== - ANY_ARG must be different to any other thing. -0.20120911 -========== +20120911 +======== - API CHANGE: called_with() is now called().with_args() (magmax suggestion) diff -Nru doublex-1.6.6/README.rst doublex-1.7.2/README.rst --- doublex-1.6.6/README.rst 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/README.rst 2013-11-07 21:00:04.000000000 +0000 @@ -1,17 +1,20 @@ -See full documentation on [wiki_] -================================= +doublex +======= -* slides - http://arco.esi.uclm.es/~david.villa/python-doublex/slides -* sources - https://bitbucket.org/DavidVilla/python-doublex -* PyPI project - http://pypi.python.org/pypi/doublex -* pydoubles.org - http://www.pydoubles.org/doublex-documentation/ -* jenkins job - https://fowler.esi.uclm.es/job/python-doublex/ +* `documentation `_ +* `release notes `_ +* `slides `_ +* `sources `_ +* `PyPI project `_ +* `pydoubles.org `_ +* `buildbot job `_ +* `other Python doubles libraries `_ -debian package http://packages.debian.org/source/sid/doublex +debian +------ +* package: http://packages.debian.org/source/sid/doublex * debian dir: ``svn://svn.debian.org/svn/python-modules/packages/doublex/trunk`` * amateur debian package at: ``deb http://babel.esi.uclm.es/arco/ sid main`` * official ubuntu package: https://launchpad.net/ubuntu/+source/doublex - -.. _wiki: https://bitbucket.org/DavidVilla/python-doublex/wiki diff -Nru doublex-1.6.6/ToDo doublex-1.7.2/ToDo --- doublex-1.6.6/ToDo 1970-01-01 00:00:00.000000000 +0000 +++ doublex-1.7.2/ToDo 2013-11-07 21:00:04.000000000 +0000 @@ -0,0 +1,3 @@ +- function doubles (supporting __call__ method) +- orphan spy methods +- double chains diff -Nru doublex-1.6.6/clean.mk doublex-1.7.2/clean.mk --- doublex-1.6.6/clean.mk 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/clean.mk 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ -#!/usr/bin/make -f -# -*- mode:makefile -*- - -clean: - find . -name *.pyc -delete - find . -name *.pyo -delete - find . -name *~ -delete - $(RM) -r dist build *.egg-info - $(RM) -r .svn debian MANIFEST - $(RM) -r *.egg-info diff -Nru doublex-1.6.6/debian/changelog doublex-1.7.2/debian/changelog --- doublex-1.6.6/debian/changelog 2013-04-28 21:15:28.000000000 +0000 +++ doublex-1.7.2/debian/changelog 2013-11-07 20:59:28.000000000 +0000 @@ -1,3 +1,23 @@ +doublex (1.7.2-1) unstable; urgency=low + + * New release + * [/control] using 3.9.4 standards-version + + -- David Villa Alises Wed, 06 Nov 2013 23:40:56 +0100 + +doublex (1.7.1-1) UNRELEASED; urgency=low + + * New release + + -- David Villa Alises Mon, 28 Oct 2013 15:19:12 +0100 + +doublex (1.6.6-4) UNRELEASED; urgency=low + + [ Jakub Wilk ] + * Use canonical URIs for Vcs-* fields. + + -- David Villa Alises Mon, 28 Oct 2013 15:18:09 +0100 + doublex (1.6.6-2) unstable; urgency=low * [/control] python3:Depends for python3-doublex diff -Nru doublex-1.6.6/debian/control doublex-1.7.2/debian/control --- doublex-1.6.6/debian/control 2013-04-27 20:18:16.000000000 +0000 +++ doublex-1.7.2/debian/control 2013-11-07 20:59:28.000000000 +0000 @@ -9,11 +9,11 @@ python-setuptools (>= 0.6b3), python3-all, python3-setuptools -Standards-Version: 3.9.3 +Standards-Version: 3.9.4 X-Python-Version: >= 2.6 Homepage: https://bitbucket.org/DavidVilla/python-doublex -Vcs-Svn: svn://svn.debian.org/python-modules/packages/doublex -Vcs-Browser: http://svn.debian.org/viewsvn/python-modules/packages/doublex/trunk/ +Vcs-Svn: svn://anonscm.debian.org/python-modules/packages/doublex/trunk/ +Vcs-Browser: http://anonscm.debian.org/viewvc/python-modules/packages/doublex/trunk/ Package: python-doublex diff -Nru doublex-1.6.6/debian-vcs.mk doublex-1.7.2/debian-vcs.mk --- doublex-1.6.6/debian-vcs.mk 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/debian-vcs.mk 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -#!/usr/bin/make -f -# -*- mode:makefile -*- - -URL_AUTH=svn+ssh://${ALIOTH_USER}@svn.debian.org/svn/python-modules/packages/doublex/trunk -URL_ANON=svn://svn.debian.org/svn/python-modules/packages/doublex/trunk - -debian: - if [ ! -z "$${ALIOTH_USER}" ]; then \ - svn co ${URL_AUTH} -N; \ - else \ - svn co ${URL_ANON} -N; \ - fi - - mv trunk/.svn . - rmdir trunk - svn up debian - -clean: - $(RM) -r .svn debian MANIFEST diff -Nru doublex-1.6.6/doublex/__init__.py doublex-1.7.2/doublex/__init__.py --- doublex-1.6.6/doublex/__init__.py 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/doublex/__init__.py 2013-11-07 21:00:04.000000000 +0000 @@ -1,5 +1,12 @@ # -*- coding:utf-8; tab-width:4; mode:python -*- +from functools import partial + from .doubles import * from .matchers import * +from .tracer import Tracer from .internal import WrongApiUsage + + +def set_default_behavior(double, func): + double._default_behavior = func diff -Nru doublex-1.6.6/doublex/doubles.py doublex-1.7.2/doublex/doubles.py --- doublex-1.6.6/doublex/doubles.py 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/doublex/doubles.py 2013-11-07 21:00:04.000000000 +0000 @@ -2,7 +2,7 @@ # doublex # -# Copyright © 2012 David Villa Alises +# Copyright © 2012, 2013 David Villa Alises # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,7 +24,7 @@ import hamcrest from .internal import (ANY_ARG, OperationList, Method, MockBase, SpyBase, - AttributeFactory) + AttributeFactory, WrongApiUsage) from .proxy import create_proxy, get_class from .matchers import MockIsExpectedInvocation @@ -35,16 +35,24 @@ class Stub(object): + _default_behavior = lambda x: None + _new_attr_hooks = [] + def __new__(cls, collaborator=None): '''Creates a fresh class clone per instance. This is required due to ad-hoc stub properties are class attributes''' - klass = type(cls.__name__, (cls,), dict(cls.__dict__)) + klass = cls._clone_class() return object.__new__(klass) + @classmethod + def _clone_class(cls): + return type(cls.__name__, (cls,), dict(cls.__dict__)) + def __init__(self, collaborator=None): self._proxy = create_proxy(collaborator) self._stubs = OperationList() self._setting_up = False + self._new_attr_hooks = self._new_attr_hooks[:] self.__class__.__setattr__ = self.__setattr__hook def __enter__(self): @@ -63,15 +71,15 @@ self._prepare_invocation(invocation) - stubbed_retval = None + stubbed_retval = self._default_behavior() if invocation in self._stubs: stubbed = self._stubs.lookup(invocation) - stubbed_retval = stubbed.perform(invocation) + stubbed_retval = stubbed._apply_stub(invocation) actual_retval = self._perform_invocation(invocation) retval = stubbed_retval if stubbed_retval is not None else actual_retval - invocation.context.retval = retval + invocation._context.retval = retval return retval def _prepare_invocation(self, invocation): @@ -117,7 +125,7 @@ def _get_invocations_to(self, name): return [i for i in self._recorded - if self._proxy.same_method(name, i.name)] + if self._proxy.same_method(name, i._name)] class ProxySpy(Spy): @@ -130,7 +138,7 @@ raise TypeError("ProxySpy takes an instance (got %s instead)" % thing) def _perform_invocation(self, invocation): - return invocation.apply_on_collaborator() + return invocation._apply_on_collaborator() class Mock(Spy, MockBase): @@ -152,7 +160,10 @@ def _get_method(self, key): if key not in list(self._methods.keys()): typename = self._proxy.get_attr_typename(key) - assert typename in ['instancemethod', 'function', 'method'], typename + if typename not in ['instancemethod', 'function', 'method']: + raise WrongApiUsage( + "Mimic does not support attribute '%s' (type '%s')" % (key, typename)) + method = Method(self, key) self._methods[key] = method diff -Nru doublex-1.6.6/doublex/internal.py doublex-1.7.2/doublex/internal.py --- doublex-1.6.6/doublex/internal.py 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/doublex/internal.py 2013-11-07 21:00:04.000000000 +0000 @@ -19,7 +19,6 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -import itertools import threading import collections @@ -48,7 +47,7 @@ ANY_ARG = Constant('ANY_ARG') -IMPOSSIBLE = Constant('IMPOSSIBLE') +UNSPECIFIED = Constant('UNSPECIFIED') def add_indent(text, indent=0): @@ -70,11 +69,11 @@ lines = [add_indent(i, indent) for i in self] return str.join('\n', lines) - def count(self, invocation, pred=None): - if pred is None: + def count(self, invocation, predicate=None): + if predicate is None: return list.count(self, invocation) - return sum(1 for i in self if pred(invocation, i)) + return [predicate(invocation, i) for i in self].count(True) class Observable(object): @@ -107,13 +106,13 @@ return retval def _create_invocation(self, args, kargs): - return Invocation.from_args(self.double, self.name, args, kargs) + return Invocation._from_args(self.double, self.name, args, kargs) @property def calls(self): if not isinstance(self.double, SpyBase): raise WrongApiUsage("Only Spy derivates store invocations") - return [x.context for x in self.double._get_invocations_to(self.name)] + return [x._context for x in self.double._get_invocations_to(self.name)] def _was_called(self, context, times): invocation = Invocation(self.double, self.name, context) @@ -122,13 +121,13 @@ def describe_to(self, description): pass - def show(self, indent=0): + def _show(self, indent=0): return add_indent(self, indent) def __repr__(self): return "%s.%s" % (self.double._classname(), self.name) - def show_history(self): + def _show_history(self): method = "method '%s.%s'" % (self.double._classname(), self.name) invocations = self.double._get_invocations_to(self.name) if not invocations: @@ -164,33 +163,34 @@ @total_ordering class Invocation(object): def __init__(self, double, name, context=None): - self.double = double - self.name = name - self.context = context or InvocationContext() - self.delegate = func_returning(None) + self._double = double + self._name = name + self._context = context or InvocationContext() + self._context.signature = double._proxy.get_signature(name) + self.__delegate = func_returning(None) @classmethod - def from_args(cls, double, name, args=(), kargs={}): + def _from_args(cls, double, name, args=(), kargs={}): return Invocation(double, name, InvocationContext(*args, **kargs)) def delegates(self, delegate): if isinstance(delegate, collections.Callable): - self.delegate = delegate + self.__delegate = delegate return try: - self.delegate = iter(delegate).next + self.__delegate = iter(delegate).next except TypeError: reason = "delegates() must be called with callable or iterable instance (got '%s' instead)" % delegate raise WrongApiUsage(reason) def returns(self, value): - self.context.retval = value + self._context.retval = value self.delegates(func_returning(value)) return self def returns_input(self): - if not self.context.args: + if not self._context.args: raise TypeError("%s has no input args" % self) self.delegates(func_returning_input(self)) @@ -204,58 +204,63 @@ raise WrongApiUsage("times must be >= 1. Use is_not(called()) for 0 times") for i in range(1, n): - self.double._manage_invocation(self) + self._double._manage_invocation(self) - # FIXME: rename to apply_stub? - def perform(self, actual_invocation): - context = actual_invocation.context - return self.delegate(*context.args, **context.kargs) + def _apply_stub(self, actual_invocation): + return actual_invocation._context.apply_on(self.__delegate) - def apply_on_collaborator(self): - return self.double._proxy.perform_invocation(self) + def _apply_on_collaborator(self): + return self._double._proxy.perform_invocation(self) def __eq__(self, other): - return self.double._proxy.same_method(self.name, other.name) and \ - self.context.matches(other.context) + return self._double._proxy.same_method(self._name, other._name) and \ + self._context.matches(other._context) def __lt__(self, other): - return any([self.name < other.name, - self.context < other.context]) + return any([self._name < other._name, + self._context < other._context]) def __repr__(self): - return "%s.%s%s" % (self.double._classname(), self.name, self.context) + return "%s.%s%s" % (self._double._classname(), self._name, self._context) - def show(self, indent=0): + def _show(self, indent=0): return add_indent(self, indent) +ANY_ARG_MUST_BE_LAST = "ANY_ARG must be the last positional argument. " +ANY_ARG_WITHOUT_KARGS = "Keyword arguments are not allowed if ANY_ARG is given. " +ANY_ARG_CAN_BE_KARG = "ANY_ARG is not allowed as keyword value. " +ANY_ARG_DOC = "See http://goo.gl/R6mOt" + + @total_ordering class InvocationContext(object): def __init__(self, *args, **kargs): self.update_args(args, kargs) self.retval = None + self.signature = None + self.check_some_args = False def update_args(self, args, kargs): + self._check_ANY_ARG_sanity(args, kargs) self.args = args self.kargs = kargs - def matches(self, other): + def _check_ANY_ARG_sanity(self, args, kargs): try: - if self._assert_args_match(self.args, other.args) is ANY_ARG: - return True + if args.index(ANY_ARG) != len(args) - 1: + raise WrongApiUsage(ANY_ARG_MUST_BE_LAST + ANY_ARG_DOC) - self._assert_kargs_match(self.kargs, other.kargs) - return True - except AssertionError: - return False + if kargs: + raise WrongApiUsage(ANY_ARG_WITHOUT_KARGS + ANY_ARG_DOC) + except ValueError: + pass - @classmethod - def _assert_args_match(cls, args1, args2): - for a, b in itertools.izip_longest(args1, args2, fillvalue=IMPOSSIBLE): - if ANY_ARG in [a, b]: - return ANY_ARG + if ANY_ARG in kargs.values(): + raise WrongApiUsage(ANY_ARG_CAN_BE_KARG + ANY_ARG_DOC) - cls._assert_values_match(a, b) + def apply_on(self, method): + return method(*self.args, **self.kargs) @classmethod def _assert_kargs_match(cls, kargs1, kargs2): @@ -265,13 +270,89 @@ @classmethod def _assert_values_match(cls, a, b): + if all(isinstance(x, tuple) for x in (a, b)): + return cls._assert_tuple_args_match(a, b) + + if all(isinstance(x, dict) for x in (a, b)): + return cls._assert_kargs_match(a, b) + if isinstance(a, BaseMatcher): a, b = b, a hamcrest.assert_that(a, hamcrest.is_(b)) - def __eq__(self, other): - return self.matches(other) + @classmethod + def _assert_tuple_args_match(cls, a, b): + if len(a) != len(b): + a, b = cls._adapt_tuples(a, b) + + for i, j in zip(a, b): + cls._assert_values_match(i, j) + + @classmethod + def _adapt_tuples(cls, a, b): + if len(a) > len(b): + return cls._adapt_tuples(b, a) + + if a[:-1] != ANY_ARG: + raise AssertionError("Incompatible argument list: %s, %s" % (a, b)) + + a = a[:-1] + (hamcrest.anything(),) * (len(b) - len(a)) + return a, b + + def copy(self): + retval = InvocationContext(*self.args, **self.kargs) + retval.signature = self.signature + return retval + + def replace_ANY_ARG(self, actual): + try: + index = self.args.index(ANY_ARG) + except ValueError: + return self + + retval = self.copy() + args = list(self.args[0:index]) + args.extend([hamcrest.anything()] * (len(actual.args) - index)) + retval.args = tuple(args) + retval.kargs = actual.kargs.copy() + return retval + + def matches(self, other): + if ANY_ARG in self.args: + matcher, actual = self, other + else: + matcher, actual = other, self + + matcher = matcher.replace_ANY_ARG(actual) + + if matcher.check_some_args: + matcher.kargs = self.add_unspecifed_args(matcher) + + matcher_call_args = matcher.signature.get_call_args(matcher) + actual_call_args = actual.signature.get_call_args(actual) + + try: + self._assert_kargs_match(matcher_call_args, actual_call_args) + return True + except AssertionError: + return False + + def add_unspecifed_args(self, context): + arg_spec = context.signature.get_arg_spec() + + if arg_spec is None: + raise WrongApiUsage( + 'free spies does not support the with_some_args() matcher') + + if arg_spec.keywords is not None: + raise WrongApiUsage( + 'with_some_args() can not be applied to method %s' % self.signature) + + keys = arg_spec.args + retval = dict((k, hamcrest.anything()) for k in keys) + retval.update(context.kargs) + return retval def __lt__(self, other): if ANY_ARG in other.args or self.args < other.args: @@ -320,18 +401,18 @@ class PropertyInvocation(Invocation): def __eq__(self, other): - return self.name == other.name + return self._name == other._name class PropertyGet(PropertyInvocation): def __init__(self, double, name): super(PropertyGet, self).__init__(double, name) - def apply_on_collaborator(self): - return getattr(self.double._proxy.collaborator, self.name) + def _apply_on_collaborator(self): + return getattr(self._double._proxy.collaborator, self._name) def __repr__(self): - return "get %s.%s" % (self.double._classname(), self.name) + return "get %s.%s" % (self._double._classname(), self._name) class PropertySet(PropertyInvocation): @@ -340,32 +421,41 @@ param = InvocationContext(value) super(PropertySet, self).__init__(double, name, param) - def apply_on_collaborator(self): - return setattr(self.double._proxy.collaborator, self.name, self.value) + def _apply_on_collaborator(self): + return setattr(self._double._proxy.collaborator, self._name, self.value) def __repr__(self): - return "set %s.%s to %s" % (self.double._classname(), - self.name, self.value) + return "set %s.%s to %s" % (self._double._classname(), + self._name, self.value) + + +class Property(property, Observable): + def __init__(self, double, key): + self.double = double + self.key = key + property.__init__(self, self.get_value, self.set_value) + Observable.__init__(self) + def manage(self, invocation): + return self.double._manage_invocation(invocation) -def property_factory(double, key): - def manage(invocation): - return double._manage_invocation(invocation) + def get_value(self, obj): + if not self.double._setting_up: + self.notify() - def get_property(obj): - return manage(PropertyGet(double, key)) + return self.manage(PropertyGet(self.double, self.key)) - def set_property(obj, value): - prop = double._proxy.get_class_attr(key) + def set_value(self, obj, value): + prop = self.double._proxy.get_class_attr(self.key) if prop.fset is None: - raise AttributeError("can't set attribute %s" % key) + raise AttributeError("can't set attribute %s" % self.key) - invocation = manage(PropertySet(double, key, value)) + invocation = self.manage(PropertySet(self.double, self.key, value)) - if double._setting_up: + if self.double._setting_up: invocation.returns(value) - - return property(get_property, set_property) + else: + self.notify(value) class AttributeFactory(object): @@ -374,7 +464,7 @@ typemap = dict( instancemethod = Method, method_descriptor = Method, - property = property_factory, + property = Property, # -- python3 -- method = Method, function = Method, @@ -392,6 +482,9 @@ else: object.__setattr__(double, key, attr) + for hook in double._new_attr_hooks: + hook(attr) + class SpyBase(object): pass diff -Nru doublex-1.6.6/doublex/matchers.py doublex-1.7.2/doublex/matchers.py --- doublex-1.6.6/doublex/matchers.py 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/doublex/matchers.py 2013-11-07 21:00:04.000000000 +0000 @@ -18,10 +18,11 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - +import time import hamcrest +from hamcrest.core.matcher import Matcher from hamcrest.core.base_matcher import BaseMatcher -from hamcrest import assert_that, is_ +from hamcrest import is_, instance_of from .internal import ( Method, InvocationContext, ANY_ARG, MockBase, SpyBase, @@ -31,7 +32,8 @@ 'never', 'verify', 'any_order_verify', 'property_got', 'property_set', - 'assert_that', 'is_'] + 'assert_that', 'wait_that', + 'is_', 'instance_of'] # just hamcrest aliases @@ -40,6 +42,43 @@ any_time = hamcrest.greater_than(0) +class MatcherRequiredError(Exception): + pass + + +def assert_that(actual, matcher=None, reason=''): + if matcher and not isinstance(matcher, Matcher): + raise MatcherRequiredError("%s should be a hamcrest Matcher" % str(matcher)) + return hamcrest.assert_that(actual, matcher, reason) + + +def wait_that(actual, matcher, reason='', delta=1, timeout=5): + ''' + Poll the given matcher each 'delta' seconds until 'matcher' + matches 'actual' or 'timeout' is reached. + ''' + exc = None + init = time.time() + timeout_reached = False + while 1: + try: + if time.time() - init > timeout: + timeout_reached = True + break + + assert_that(actual, matcher, reason) + break + + except AssertionError as e: + time.sleep(delta) + exc = e + + if timeout_reached: + msg = exc.args[0] + ' after {0} seconds'.format(timeout) + exc.args = msg, + raise exc + + class OperationMatcher(BaseMatcher): pass @@ -69,7 +108,7 @@ def describe_to(self, description): description.append_text('these calls:\n') - description.append_text(self.method.show(indent=10)) + description.append_text(self.method._show(indent=10)) description.append_text(str(self.context)) if self._times != any_time: description.append_text(' -- times: %s' % self._times) @@ -82,6 +121,11 @@ self.context.update_args(args, kargs) return self + def with_some_args(self, **kargs): + self.context.update_args(tuple(), kargs) + self.context.check_some_args = True + return self + def async(self, timeout): self._async_timeout = timeout return self @@ -127,7 +171,7 @@ def describe_mismatch(self, actual, description): description.append_text("this call was not expected:\n") - description.append_text(self.invocation.show(indent=10)) + description.append_text(self.invocation._show(indent=10)) class verify(BaseMatcher): @@ -168,11 +212,12 @@ self.operation, 1, cmp_pred=Invocation.__eq__) def times(self, n): - return property_got(self.property_name, n) + self._times = n + return self def describe_to(self, description): description.append_text('these calls:\n') - description.append_text(self.operation.show(indent=10)) + description.append_text(self.operation._show(indent=10)) if self._times != any_time: description.append_text(' -- times: %s' % self._times) @@ -195,14 +240,16 @@ self.operation, self._times, cmp_pred=Invocation.__eq__) def to(self, value): - return property_set(self.property_name, value) + self.value = value + return self def times(self, n): - return property_set(self.property_name, self.value, n) + self._times = n + return self def describe_to(self, description): description.append_text('these calls:\n') - description.append_text(self.operation.show(indent=10)) + description.append_text(self.operation._show(indent=10)) if self._times != any_time: description.append_text(' -- times: %s' % self._times) diff -Nru doublex-1.6.6/doublex/proxy.py doublex-1.7.2/doublex/proxy.py --- doublex-1.6.6/doublex/proxy.py 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/doublex/proxy.py 2013-11-07 21:00:04.000000000 +0000 @@ -2,7 +2,7 @@ # doublex # -# Copyright © 2012 David Villa Alises +# Copyright © 2012,2013 David Villa Alises # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -33,21 +33,45 @@ if collaborator is None: return DummyProxy() - return Proxy(collaborator) + return CollaboratorProxy(collaborator) -class DummyProxy(object): - def get_attr_typename(self, key): - return 'instancemethod' - +class Proxy(object): def assure_signature_matches(self, invocation): pass + def collaborator_classname(self): + return None + + def get_signature(self, method_name): + if self.is_property(method_name): + return PropertySignature(self, method_name) + + if not self.is_method_or_func(method_name): + return BuiltinSignature(self, method_name) + + return MethodSignature(self, method_name) + + def is_property(self, attr_name): + attr = getattr(self.collaborator_class, attr_name) + return isinstance(attr, property) + + def is_method_or_func(self, method_name): + func = getattr(self.collaborator, method_name) + if inspect.ismethod(func): + func = func.im_func + return inspect.isfunction(func) + + +class DummyProxy(Proxy): + def get_attr_typename(self, key): + return 'instancemethod' + def same_method(self, name1, name2): return name1 == name2 - def collaborator_classname(self): - return None + def get_signature(self, method_name): + return DummySignature() def get_class(something): @@ -57,7 +81,7 @@ return something.__class__ -class Proxy(object): +class CollaboratorProxy(Proxy): '''Represent the collaborator object''' def __init__(self, collaborator): self.collaborator = collaborator @@ -76,9 +100,8 @@ return self.collaborator_class.__name__ def assure_signature_matches(self, invocation): - signature = create_signature(self, invocation.name) - signature.assure_match(invocation.context.args, - invocation.context.kargs) + signature = self.get_signature(invocation._name) + signature.assure_matches(invocation._context) def get_attr_typename(self, key): def raise_no_attribute(): @@ -104,41 +127,39 @@ getattr(self.collaborator, name2) def perform_invocation(self, invocation): - method = getattr(self.collaborator, invocation.name) - return method(*invocation.context.args, - **invocation.context.kargs) + method = getattr(self.collaborator, invocation._name) + return invocation._context.apply_on(method) -def create_signature(proxy, method_name): - if is_property(proxy, method_name): - return PropertySignature(proxy, method_name) +class Signature(object): + def __init__(self, proxy, name): + self.proxy = proxy + self.name = name + self.method = getattr(proxy.collaborator, name) - if not is_method_or_func(proxy, method_name): - return BuiltinSignature(proxy, method_name) + def get_arg_spec(self): + pass - return Signature(proxy, method_name) + def get_call_args(self, context): + retval = context.kargs.copy() + for n, i in enumerate(context.args): + retval['_positional_%s' % n] = i + return retval -def is_property(proxy, attr_name): - attr = getattr(proxy.collaborator_class, attr_name) - return isinstance(attr, property) + def __eq__(self, other): + return (self.proxy, self.name, self.method) == \ + (other.proxy, other.name, other.method) -def is_method_or_func(proxy, method_name): - func = getattr(proxy.collaborator, method_name) - if inspect.ismethod(func): - func = func.im_func - return inspect.isfunction(func) +class DummySignature(Signature): + def __init__(self): + pass -class BuiltinSignature(object): +class BuiltinSignature(Signature): "builtin collaborator method signature" - def __init__(self, proxy, name): - self.proxy = proxy - self.name = name - self.method = getattr(proxy.collaborator, name) - - def assure_match(self, args, kargs): + def assure_matches(self, context): doc = self.method.__doc__ if not ')' in doc: return @@ -147,39 +168,52 @@ params = doc[:rpar] nkargs = params.count('=') nargs = params.count(',') + 1 - nkargs - if len(args) != nargs: + if len(context.args) != nargs: raise TypeError('%s.%s() takes exactly %s argument (%s given)' % ( - self.proxy.collaborator_classname(), self.name, nargs, len(args))) + self.proxy.collaborator_classname(), self.name, + nargs, len(context.args))) -class Signature(object): +class MethodSignature(Signature): "colaborator method signature" def __init__(self, proxy, name): - self.proxy = proxy - self.name = name - self.method = getattr(proxy.collaborator, name) + super(MethodSignature, self).__init__(proxy, name) self.argspec = inspect.getargspec(self.method) - def assure_match(self, args, kargs): - if ANY_ARG in args: - return + def get_arg_spec(self): + retval = inspect.getargspec(self.method) + del retval.args[0] + return retval + + def get_call_args(self, context): + args = context.args +# print self.name, args if self.proxy.isclass(): args = (None,) + args # self + retval = getcallargs(self.method, *args, **context.kargs) + del retval['self'] + return retval + + def assure_matches(self, context): + if ANY_ARG in context.args: + return + try: - getcallargs(self.method, *args, **kargs) + self.get_call_args(context) except TypeError as e: raise TypeError("%s.%s" % (self.proxy.collaborator_classname(), e)) def __repr__(self): - return "%s.%s%s" % (self._proxy.collaborator_classname(), + return "%s.%s%s" % (self.proxy.collaborator_classname(), self.name, inspect.formatargspec(*self.argspec)) -class PropertySignature(object): + +class PropertySignature(Signature): def __init__(self, proxy, name): pass - def assure_match(self, args, kargs): + def assure_matches(self, context): pass diff -Nru doublex-1.6.6/doublex/pyDoubles/__init__.py doublex-1.7.2/doublex/pyDoubles/__init__.py --- doublex-1.6.6/doublex/pyDoubles/__init__.py 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/doublex/pyDoubles/__init__.py 2013-11-07 21:00:04.000000000 +0000 @@ -1,8 +1,26 @@ # -*- coding:utf-8; tab-width:4; mode:python -*- +# doublex +# +# Copyright © 2012,2013 David Villa Alises +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + """ -This is a wrapper to offer the pyDoubles API but implemented -over doublex package. +This is a wrapper to offer the pyDoubles API implemented +over the doublex package. """ import hamcrest @@ -17,6 +35,14 @@ ApiMismatch = TypeError +def empty_stub(): + return doublex.Stub() + + +def stub(collaborator=None): + return doublex.Stub(collaborator) + + def empty_spy(): return doublex.Spy() diff -Nru doublex-1.6.6/doublex/test/issue-14.py doublex-1.7.2/doublex/test/issue-14.py --- doublex-1.6.6/doublex/test/issue-14.py 1970-01-01 00:00:00.000000000 +0000 +++ doublex-1.7.2/doublex/test/issue-14.py 2013-11-07 21:00:04.000000000 +0000 @@ -0,0 +1,31 @@ +# Thanks to Guillermo Pascual (@pasku1) + +# When you spy a method that has a decorator and you want to check the +# arguments with a hamcrest matcher, it seems like matchers are +# ignored. + +from functools import wraps +import unittest +from doublex import * +from hamcrest import * + + +class Collaborator(object): + def simple_decorator(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + return func(self, *args, **kwargs) + return wrapper + + @simple_decorator + def method_with_two_arguments(self, one, two): + pass + + +class ExampleTest(unittest.TestCase): + def test_spying_a_method_with_a_decorator(self): + collaborator = Spy(Collaborator) + collaborator.method_with_two_arguments(1, 'foo bar') + + assert_that(collaborator.method_with_two_arguments, + called().with_args(1, ends_with('bar'))) diff -Nru doublex-1.6.6/doublex/test/pyDoubles/unit_tests.py doublex-1.7.2/doublex/test/pyDoubles/unit_tests.py --- doublex-1.6.6/doublex/test/pyDoubles/unit_tests.py 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/doublex/test/pyDoubles/unit_tests.py 2013-11-07 21:00:04.000000000 +0000 @@ -133,7 +133,7 @@ self.spy.hello() args_checker = assert_that_was_called(self.spy.hello) - self.failUnlessRaises(ArgsDontMatch, + self.failUnlessRaises(TypeError, args_checker.with_args, "something") def test_was_called_with_several_parameters(self): @@ -269,13 +269,6 @@ self.assertEquals(1000, self.spy.two_args_method(1, 2)) self.assertEquals(1000, self.spy.two_args_method(1, 5)) -## TODO: implement this: -## pyDoubles did not support this -## def test_any_arg_matcher_with_kwargs(self): -## when(self.spy.kwarg_method).with_args(key_param=ANY_ARG).then_return(1000) -## -## self.assertEquals(1000, self.spy.kwarg_method(key_param=2)) - def test_any_arg_matcher_was_called(self): when(self.spy.two_args_method).with_args(1, 2).then_return(1000) @@ -814,6 +807,22 @@ pass +class StubTests(unittest.TestCase): + def test_free_stub(self): + my_stub = empty_stub() + self.assertTrue(my_stub.hello() is None) + + def test_restricted_stub(self): + my_stub = stub(Collaborator()) + when(my_stub.something).then_return(10) + + self.assertEquals(10, my_stub.something()) + + def test_restricted_stub_method_not_stubbed(self): + my_stub = stub(Collaborator()) + self.assertEquals(None, my_stub.hello()) + + # Create hamcrest matchers instead #class CustomMatchersTest(unittest.TestCase): # diff -Nru doublex-1.6.6/doublex/test/pyDoubles_legacy_tests.py doublex-1.7.2/doublex/test/pyDoubles_legacy_tests.py --- doublex-1.6.6/doublex/test/pyDoubles_legacy_tests.py 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/doublex/test/pyDoubles_legacy_tests.py 2013-11-07 21:00:04.000000000 +0000 @@ -1,6 +1,5 @@ # -*- coding:utf-8; tab-width:4; mode:python -*- - from unittest import TestCase from hamcrest import is_not, all_of, contains_string, has_length @@ -94,7 +93,8 @@ self.spy.hello() args_checker = called().with_args("something") - assert_that(not args_checker.matches(self.spy.hello)) + with self.assertRaises(TypeError): + assert_that(not args_checker.matches(self.spy.hello)) def test_was_called_with_several_parameters(self): self.spy.two_args_method(1, 2) diff -Nru doublex-1.6.6/doublex/test/report_tests.py doublex-1.7.2/doublex/test/report_tests.py --- doublex-1.6.6/doublex/test/report_tests.py 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/doublex/test/report_tests.py 2013-11-07 21:00:04.000000000 +0000 @@ -338,7 +338,7 @@ self.assert_expectation_error(expected_message) -class PropertReportTests(TestCase, MessageMixin): +class PropertyReportTests(TestCase, MessageMixin): def test_expected_get(self): spy = doublex.Spy(ObjCollaborator) diff -Nru doublex-1.6.6/doublex/test/unit_tests.py doublex-1.7.2/doublex/test/unit_tests.py --- doublex-1.6.6/doublex/test/unit_tests.py 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/doublex/test/unit_tests.py 2013-11-07 21:00:04.000000000 +0000 @@ -2,7 +2,7 @@ # doublex # -# Copyright © 2012 David Villa Alises +# Copyright © 2012,2013 David Villa Alises # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,7 +24,8 @@ import itertools import thread import threading - +import io +import copy from hamcrest import is_not, all_of, contains_string, has_length from hamcrest.library.text.stringcontainsinorder import * @@ -32,6 +33,7 @@ from hamcrest.library.number.ordering_comparison import * from doublex import * +from doublex.matchers import MatcherRequiredError from doublex.internal import InvocationContext @@ -93,16 +95,22 @@ with self.stub: self.stub.foo().returns(True) - assert_that(self.stub.bar(), is_(None)) + assert_that(self.stub.unknown(), is_(None)) + + def test_returns_input(self): + with Stub() as stub: + stub.foo(1).returns_input() + + assert_that(stub.foo(1), is_(1)) def test_raises(self): with self.stub: - self.stub.foo().raises(KeyError) + self.stub.foo(2).raises(SomeException) try: - self.stub.foo() - self.fail("It should raise KeyError") - except KeyError: + self.stub.foo(2) + self.fail("It should raise SomeException") + except SomeException: pass @@ -144,19 +152,33 @@ assert_that(str(e), contains_string(expected)) # bitbucket issue #6 - def test_keyworked_or_positional(self): + def test_keyword_or_positional(self): + with self.stub: + self.stub.kwarg_method(1).returns(1000) + self.stub.kwarg_method(2).returns(2000) + self.stub.kwarg_method(key_param=6).returns(6000) + + assert_that(self.stub.kwarg_method(1), is_(1000)) + assert_that(self.stub.kwarg_method(2), is_(2000)) + assert_that(self.stub.kwarg_method(key_param=6), is_(6000)) + assert_that(self.stub.kwarg_method(key_param=6), is_(6000)) + + # FIXME: new on 1.7 + def test_keyworked_or_positional_are_equivalent(self): with self.stub: self.stub.kwarg_method(1).returns(1000) - self.stub.kwarg_method(key_param=2).returns(2000) + self.stub.kwarg_method(key_param=6).returns(6000) assert_that(self.stub.kwarg_method(1), is_(1000)) - assert_that(self.stub.kwarg_method(key_param=2), is_(2000)) + assert_that(self.stub.kwarg_method(key_param=1), is_(1000)) + assert_that(self.stub.kwarg_method(6), is_(6000)) + assert_that(self.stub.kwarg_method(key_param=6), is_(6000)) def test_returning_tuple(self): with self.stub: self.stub.hello().returns((3, 4)) - assert_that(self.stub.hello(), (3, 4)) + assert_that(self.stub.hello(), is_((3, 4))) class AccessingActualAttributes(TestCase): @@ -303,7 +325,7 @@ # assert_that(spy.mixed_method, called().with_args(key_param=True)) -class SpyCallsTests(TestCase): +class Spy_calls_tests(TestCase): def test_list_recorded_calls(self): class Collaborator: def method(self, *args, **kargs): @@ -344,7 +366,7 @@ class SpyTests(TestCase): def setUp(self): - self.spy = Spy(Collaborator()) + self.spy = Spy(Collaborator) def test_from_instance(self): spy = Spy(Collaborator()) @@ -374,6 +396,11 @@ def test_create_from_newstyle_class(self): Spy(ObjCollaborator) + def test_wrong_call_args(self): + self.spy.hello() + with self.assertRaises(TypeError): + assert_that(self.spy.hello, called().with_args('some')) + class BuiltinSpyTests(TestCase): def test_builtin_method(self): @@ -575,19 +602,19 @@ self.spy.method_one(ANY_ARG).returns(2) def test_empty_spy_stub_method(self): - assert_that(self.empty_spy.foo.show_history(), - "method 'Spy.foo' never invoked") + assert_that(self.empty_spy.foo._show_history(), + reason="method 'Spy.foo' never invoked") def test_spy_stub_method(self): - assert_that(self.spy.method_one.show_history(), - "method 'Collaborator.method_one' never invoked") + assert_that(self.spy.method_one._show_history(), + reason="method 'Collaborator.method_one' never invoked") def test_empty_spy_stub_method_invoked(self): self.empty_spy.foo() expected = [ "method 'Spy.foo' was invoked", "foo()"] - assert_that(self.empty_spy.foo.show_history(), + assert_that(self.empty_spy.foo._show_history(), string_contains_in_order(*expected)) def test_spy_stub_method_invoked(self): @@ -595,7 +622,7 @@ expected = [ "method 'Collaborator.method_one' was invoked", 'method_one(1)'] - assert_that(self.spy.method_one.show_history(), + assert_that(self.spy.method_one._show_history(), string_contains_in_order(*expected)) def test_empty_spy_non_stubbed_method_invoked(self): @@ -603,7 +630,7 @@ expected = [ "method 'Spy.bar' was invoked", "bar(1, 3.0, 'text', key1='text', key2=[1, 2])"] - assert_that(self.empty_spy.bar.show_history(), + assert_that(self.empty_spy.bar._show_history(), string_contains_in_order(*expected)) def test_spy_several_invoked_same_method(self): @@ -611,7 +638,7 @@ self.spy.mixed_method(8, False) expected = "method 'Collaborator.mixed_method' was invoked" - assert_that(self.spy.mixed_method.show_history(), + assert_that(self.spy.mixed_method._show_history(), contains_string(expected)) @@ -688,6 +715,11 @@ assert_that(self.stub.foo(1, 2, 3), is_(True)) assert_that(self.stub.foo(1, key1='a'), is_(True)) + def test_ANY_ARG_must_be_last_positional_argument(self): + with self.assertRaises(WrongApiUsage): + with self.stub: + self.stub.method(1, ANY_ARG, 3).returns(True) + class ANY_ARG_SpyTests(TestCase): def setUp(self): @@ -696,10 +728,13 @@ def test_no_args(self): self.spy.foo() assert_that(self.spy.foo, called().with_args(ANY_ARG)) + assert_that(self.spy.foo, never(called().with_args(anything()))) def test_one_arg(self): self.spy.foo(1) + assert_that(self.spy.foo, called()) assert_that(self.spy.foo, called().with_args(ANY_ARG)) + assert_that(self.spy.foo, called().with_args(anything())) def test_one_karg(self): self.spy.foo(key='val') @@ -724,6 +759,43 @@ assert_that(self.spy.foo, called().times(4)) assert_that(self.spy.foo, called().with_args(ANY_ARG).times(4)) + # issue 9 + def test_ANY_ARG_forbbiden_as_keyword_value(self): + person = Spy() + person.set_info(name="John", surname="Doe") + + assert_that(person.set_info, + called().with_args(name=anything(), surname="Doe")) + + with self.assertRaises(WrongApiUsage): + assert_that(person.set_info, + called().with_args(name=ANY_ARG, surname="Doe")) + + def test_ANY_ARG_must_be_last_positional_argument(self): + self.spy.method(1, 2, 3) + + with self.assertRaises(WrongApiUsage): + assert_that(self.spy.method, + called().with_args(1, ANY_ARG, 3)) + + def test_ANY_ARG_must_be_last_positional_argument_with_xarg(self): + self.spy.method(1, 2, 3, name='Bob') + + with self.assertRaises(WrongApiUsage): + assert_that(self.spy.method, + called().with_args(1, ANY_ARG, name='Bob')) + + def test_ANY_ARG_must_be_last_positional_argument__restricted_spy(self): + spy = Spy(Collaborator) + + with self.assertRaises(WrongApiUsage): + assert_that(spy.two_args_method, + called().with_args(ANY_ARG, 2)) + + with self.assertRaises(WrongApiUsage): + assert_that(spy.three_args_method, + called().with_args(1, ANY_ARG, 3)) + class MatcherTests(TestCase): def setUp(self): @@ -776,7 +848,7 @@ assert_that(self.spy.foo, is_not(called().with_args(5))) # = 0 times assert_that(self.spy.foo, called().with_args().times(1)) # = 1 assert_that(self.spy.foo, called().with_args(anything())) # > 0 - assert_that(self.spy.foo, called().with_args(anything()).times(4)) # = 4 + assert_that(self.spy.foo, called().with_args(ANY_ARG).times(4)) # = 4 assert_that(self.spy.foo, called().with_args(1).times(2)) # = 2 assert_that(self.spy.foo, called().with_args(1).times(greater_than(1))) # > 1 assert_that(self.spy.foo, called().with_args(1).times(less_than(5))) # < 5 @@ -804,6 +876,10 @@ assert_that(self.spy.m3, called().with_args(greater_than(1))) assert_that(self.spy.m6, called().with_args(name=contains_string("doe"))) + # new on 1.7 + def test_assert_that_requires_a_matcher(self): + self.assertRaises(MatcherRequiredError, assert_that, self.spy.m1, True) + class StubObserverTests(TestCase): def setUp(self): @@ -915,7 +991,7 @@ def test_mimic_spy_DOES_inherit_collaborator_superclasses(self): spy = Mimic(Spy, self.B) for cls in [self.B, self.A, Spy, Stub, object]: - assert_that(isinstance(spy, cls), cls) + assert_that(spy, instance_of(cls)) def test_mimic_stub_works(self): stub = Mimic(Stub, self.B) @@ -1139,6 +1215,264 @@ assert_that(spy.write, called().async(timeout=1)) +# FIXME: new on 1.7 +class with_some_args_matcher_tests(TestCase): + def test_one_arg(self): + spy = Spy(Collaborator) + spy.mixed_method(5) + assert_that(spy.mixed_method, called().with_args(5)) + assert_that(spy.mixed_method, called().with_args(arg1=5)) + + def test_two_arg(self): + spy = Spy(Collaborator) + spy.two_args_method(5, 10) + assert_that(spy.two_args_method, called().with_args(5, 10)) + assert_that(spy.two_args_method, called().with_args(arg1=5, arg2=10)) + assert_that(spy.two_args_method, called().with_some_args(arg1=5)) + assert_that(spy.two_args_method, called().with_some_args(arg2=10)) + assert_that(spy.two_args_method, called().with_some_args()) + + def test_free_spy(self): + spy = Spy() + spy.foo(1, 3) + + with self.assertRaises(WrongApiUsage): + assert_that(spy.foo, called().with_some_args()) + + +# FIXME: new on 1.7 +class Stub_default_behavior_tests(TestCase): + def test_set_return_globally(self): + StubClone = Stub._clone_class() + set_default_behavior(StubClone, method_returning(20)) + stub = StubClone() + + assert_that(stub.unknown(), is_(20)) + + def test_set_exception_globally(self): + StubClone = Stub._clone_class() + set_default_behavior(StubClone, method_raising(SomeException)) + stub = StubClone() + + with self.assertRaises(SomeException): + stub.unknown() + + def test_set_return_by_instance(self): + stub = Stub() + set_default_behavior(stub, method_returning(20)) + + assert_that(stub.unknown(), is_(20)) + + def test_set_exception_by_instance(self): + stub = Stub() + set_default_behavior(stub, method_raising(SomeException)) + + with self.assertRaises(SomeException): + stub.unknown() + + def test_restricted_stub(self): + stub = Stub(Collaborator) + set_default_behavior(stub, method_returning(30)) + with stub: + stub.hello().returns(1000) + + assert_that(stub.something(), is_(30)) + assert_that(stub.hello(), is_(1000)) + + +# FIXME: new on 1.7 +class Spy_default_behavior_tests(TestCase): + def test_set_return_globally(self): + SpyClone = Spy._clone_class() + set_default_behavior(SpyClone, method_returning(20)) + spy = SpyClone() + + assert_that(spy.unknown(7), is_(20)) + + assert_that(spy.unknown, called().with_args(7)) + assert_that(spy.unknown, never(called().with_args(9))) + + def test_set_return_by_instance(self): + spy = Spy() + set_default_behavior(spy, method_returning(20)) + + assert_that(spy.unknown(7), is_(20)) + + assert_that(spy.unknown, called().with_args(7)) + + +# FIXME: new on 1.7 +class ProxySpy_default_behavior_tests(TestCase): + def test_this_change_proxyspy_default_behavior(self): + spy = ProxySpy(Collaborator()) + assert_that(spy.hello(), is_("hello")) + + set_default_behavior(spy, method_returning(40)) + assert_that(spy.hello(), is_(40)) + + +# FIXME: new on tip +class orphan_methods_tests(TestCase): + def setUp(self): + self.obj = Collaborator() + + def test_stub_method(self): + with Stub() as stub: + stub.method(1).returns(100) + stub.method(2).returns(200) + + self.obj.foo = stub.method + + assert_that(self.obj.foo(0), is_(None)) + assert_that(self.obj.foo(1), is_(100)) + assert_that(self.obj.foo(2), is_(200)) + + def test_spy_method(self): + with Spy() as spy: + spy.method(1).returns(100) + spy.method(2).returns(200) + spy.method(3).raises(SomeException) + + self.obj.foo = spy.method + + assert_that(self.obj.foo(0), is_(None)) + assert_that(self.obj.foo(1), is_(100)) + assert_that(self.obj.foo(2), is_(200)) + with self.assertRaises(SomeException): + self.obj.foo(3) + + assert_that(self.obj.foo, called().times(4)) + assert_that(spy.method, called().times(4)) + +# def test_spy_method__brief_method(self): +# with method() as self.obj.foo: +# self.obj.foo().returns(100) +# self.obj.foo(2).returns(200) +# +# assert_that(self.obj.foo(), is_(100)) +# assert_that(self.obj.foo(2), is_(200)) +# assert_that(self.obj.foo(3), is_(None)) +# +# assert_that(self.obj.foo, called().times(3)) +# assert_that(spy.method, called().times(3)) + + +# FIXME: new on 1.7.2 +class VarArgsTest(TestCase): + def test_stub_args(self): + stub = Stub(Collaborator) + with stub: + stub.varargs(1).returns(10) + stub.varargs(1, 2).returns(200) + stub.varargs(1, 3, ANY_ARG).returns(300) + stub.varargs(2, anything()).returns(400) + + assert_that(stub.varargs(42), is_(None)) + assert_that(stub.varargs(1), is_(10)) + + assert_that(stub.varargs(1, 2), is_(200)) + assert_that(stub.varargs(1, 2, 7), is_(None)) + + assert_that(stub.varargs(1, 3), is_(300)) + assert_that(stub.varargs(1, 3, 7), is_(300)) + + assert_that(stub.varargs(1, 5), is_(None)) + + assert_that(stub.varargs(2), is_(None)) + assert_that(stub.varargs(2, 3), is_(400)) + assert_that(stub.varargs(2, 3, 4), is_(None)) + + def test_spy_args(self): + spy = Spy(Collaborator) + spy.varargs(1, 2, 3) + + assert_that(spy.varargs, called()) + assert_that(spy.varargs, called().with_args(1, 2, 3)) + assert_that(spy.varargs, called().with_args(1, ANY_ARG)) + + def test_spy_kargs(self): + spy = Spy(Collaborator) + spy.varargs(one=1, two=2) + + assert_that(spy.varargs, called()) + assert_that(spy.varargs, called().with_args(one=1, two=2)) + assert_that(spy.varargs, called().with_args(one=1, two=anything())) + + def test_with_some_args_is_not_applicable(self): + spy = Spy(Collaborator) + spy.varargs(one=1, two=2) + + try: + assert_that(spy.varargs, called().with_some_args(one=1)) + self.fail('exception should be raised') + except WrongApiUsage as e: + assert_that(str(e), + contains_string('with_some_args() can not be applied to method Collaborator.varargs(self, *args, **kargs)')) + + +# FIXME: new on 1.7.2 +class TracerTests(TestCase): + def setUp(self): + self.out = io.BytesIO() + self.tracer = Tracer(self.out.write) + + def test_trace_single_method(self): + with Stub() as stub: + stub.foo(ANY_ARG).returns(1) + + self.tracer.trace(stub.foo) + + stub.foo(1, two=2) + + assert_that(self.out.getvalue(), is_("Stub.foo(1, two=2)")) + + def test_trace_single_non_stubbed_method(self): + stub = Stub() + self.tracer.trace(stub.non) + + stub.non(1, "two") + + assert_that(self.out.getvalue(), is_("Stub.non(1, 'two')")) + + def test_trace_all_double_INSTANCE_methods(self): + stub = Stub() + self.tracer.trace(stub) + + stub.bar(2, "three") + + assert_that(self.out.getvalue(), is_("Stub.bar(2, 'three')")) + + def test_trace_all_double_CLASS_methods(self): + self.tracer.trace(Stub) + stub = Stub() + + stub.fuzz(3, "four") + + assert_that(self.out.getvalue(), is_("Stub.fuzz(3, 'four')")) + + def test_trace_get_property(self): + stub = Stub(ObjCollaborator) + self.tracer.trace(stub) + + stub.prop + + assert_that(self.out.getvalue(), + is_("ObjCollaborator.prop gotten")) + + def test_trace_set_property(self): + stub = Stub(ObjCollaborator) + self.tracer.trace(stub) + + stub.prop = 2 + + assert_that(self.out.getvalue(), + is_("ObjCollaborator.prop set to 2")) + + +class SomeException(Exception): + pass + + class Observer(object): def __init__(self): self.state = None @@ -1188,6 +1522,9 @@ def two_args_method(self, arg1, arg2): return arg1 + arg2 + def three_args_method(self, arg1, arg2, arg3): + return arg1 + arg2 + arg3 + def kwarg_method(self, key_param=False): return key_param @@ -1200,4 +1537,7 @@ def method_one(self, arg1): return 1 + def varargs(self, *args, **kargs): + return len(args) + alias_method = one_arg_method diff -Nru doublex-1.6.6/doublex/tracer.py doublex-1.7.2/doublex/tracer.py --- doublex-1.6.6/doublex/tracer.py 1970-01-01 00:00:00.000000000 +0000 +++ doublex-1.7.2/doublex/tracer.py 2013-11-07 21:00:04.000000000 +0000 @@ -0,0 +1,51 @@ +# -*- coding:utf-8; tab-width:4; mode:python -*- + +from .doubles import Stub +from .internal import Method, WrongApiUsage + + +class MethodTracer(object): + def __init__(self, logger, method): + self.logger = logger + self.method = method + + def __call__(self, *args, **kargs): + self.logger(str(self.method._create_invocation(args, kargs))) + + +class PropertyTracer(object): + def __init__(self, logger, prop): + self.logger = logger + self.prop = prop + + def __call__(self, *args, **kargs): + propname = "%s.%s" % (self.prop.double._classname(), self.prop.key) + if args: + self.logger("%s set to %s" % (propname, args[0])) + else: + self.logger("%s gotten" % (propname)) + + +class Tracer(object): + def __init__(self, logger): + self.logger = logger + + def trace(self, target): + if isinstance(target, Method): + self.trace_method(target) + elif isinstance(target, Stub) or issubclass(target, Stub): + self.trace_double(target) + else: + raise WrongApiUsage('Can not trace %s' % target) + + def trace_method(self, method): + method.attach(MethodTracer(self.logger, method)) + + def trace_double(self, double): + def attach_new_method(attr): + if isinstance(attr, Method): + attr.attach(MethodTracer(self.logger, attr)) + else: + attr.attach(PropertyTracer(self.logger, attr)) + + double._new_attr_hooks.append(attach_new_method) diff -Nru doublex-1.6.6/make doublex-1.7.2/make --- doublex-1.6.6/make 1970-01-01 00:00:00.000000000 +0000 +++ doublex-1.7.2/make 2013-11-07 21:00:04.000000000 +0000 @@ -0,0 +1,28 @@ +#!/usr/bin/make -f +# -*- mode:makefile -*- + +URL_AUTH=svn+ssh://${ALIOTH_USER}@svn.debian.org/svn/python-modules/packages/doublex/trunk +URL_ANON=svn://svn.debian.org/svn/python-modules/packages/doublex/trunk + +debian: + if [ ! -z "$${ALIOTH_USER}" ]; then \ + svn co ${URL_AUTH} -N; \ + else \ + svn co ${URL_ANON} -N; \ + fi + + mv trunk/.svn . + rmdir trunk + svn up debian + +wiki: + hg clone ssh://hg@bitbucket.org/DavidVilla/python-doublex/wiki + +clean: + find . -name *.pyc -delete + find . -name *.pyo -delete + find . -name *~ -delete + $(RM) -r dist build *.egg-info + $(RM) -r .svn debian MANIFEST + $(RM) -r *.egg-info + $(RM) -r slides/reveal.js diff -Nru doublex-1.6.6/pydoubles-site/doublex-documentation doublex-1.7.2/pydoubles-site/doublex-documentation --- doublex-1.6.6/pydoubles-site/doublex-documentation 1970-01-01 00:00:00.000000000 +0000 +++ doublex-1.7.2/pydoubles-site/doublex-documentation 2013-11-07 21:00:04.000000000 +0000 @@ -0,0 +1,27 @@ +For the time being you can find the doublex API documentation at: + +https://bitbucket.org/DavidVilla/python-doublex/wiki +

What provides doublex respect to pyDoubles?

+Respect to pyDoubles, doublex...: +
    +
  • Use just hamcrest matchers (for all features).
  • +
  • Only ProxySpy requires an instance. Other doubles accept a class too, and they never instantiate it.
  • +
  • Stub observers: Notify arbitrary hooks when methods are invoked. Useful to add "side effects".
  • +
  • Stub delegates: Use callables, iterables or generators to create stub return values.
  • +
  • Mimic doubles: doubles that inherit the same collaborator subclasses. This provides full LSP for code that make strict type checking.
  • +
+doublex support all the issues notified in the pyDoubles issue tracker: + +And other features requested in the user group: + +  diff -Nru doublex-1.6.6/pydoubles-site/downloads doublex-1.7.2/pydoubles-site/downloads --- doublex-1.6.6/pydoubles-site/downloads 1970-01-01 00:00:00.000000000 +0000 +++ doublex-1.7.2/pydoubles-site/downloads 2013-11-07 21:00:04.000000000 +0000 @@ -0,0 +1,22 @@ +Get latest release from here + +
+gunzip doublex-X.X.tar.gz
+tar xvf doublex-X.X.tar
+cd doublex-X.X/
+sudo python setup.py install
+
+ +Or use pip: + +
$ sudo pip install doubles-X.X.tar.gz
+ +Pydoubles is also available on Pypi: + +
$ sudo pip install doublex
+ +You can also get the latest source code from the mercurial repository. Check out the project: + +
$ hg clone https://bitbucket.org/DavidVilla/python-doublex
+ +Browse the source code, get support and notify bugs in the issue tracker. diff -Nru doublex-1.6.6/pydoubles-site/overview doublex-1.7.2/pydoubles-site/overview --- doublex-1.6.6/pydoubles-site/overview 1970-01-01 00:00:00.000000000 +0000 +++ doublex-1.7.2/pydoubles-site/overview 2013-11-07 21:00:04.000000000 +0000 @@ -0,0 +1,29 @@ +

What is pyDoubles?

+pyDoubles is a test doubles framework for the Python platform. Test doubles frameworks are also called mocking frameworks. pyDoubles can be used as a testing tool or as a Test Driven Development tool. +It generates stubs, spies, and mock objects using a fluent interface that will make your unit tests more readable. Moreover, it's been designed to make your tests less fragile when possible. + +The development of pyDoubles has been completely test-driven from scratch. The project is under continuous evolution, but you can extend the framework with your own requirements. The code is simple and well documented with unit tests. +

What is doublex?

+doublex is a new doubles framework that optionally provides the pyDoubles legacy API. It supports all the pyDoubles features and some more that can not be easely backported. If you are a pyDoubles user you can run your tests using doublex.pyDoubles module. However, we recommed the native doublex API for your new developments. +

Supported test doubles

+Find out what test doubles are according to Gerard Meszaros. pyDoubles offers mainly three kind of doubles: +

Stub

+Replaces the implementation of one or more methods in the object instance which plays the role of collaborator or dependency, returning the value that we explicitly write down in the test. A stub is actually a method but it is also common to use the noun stub for a class with stubbed methods. The stub does not have any kind or memory. + +Stubs are used mainly for state validation or along with spies or mocks. +

Spy

+Replaces the implementation as a stub does, but it is also able to register and remember what methods are called during the test execution and how they are invoked. + +They are used for interaction/behavior verification. +

Mock

+Contains the same features than the Stub and therefore the Spy, but it is very strict in the behavior specification it should expect from the System Under Tests. Before calling any method in the mock object, the framework should be told (in the test) which methods we expect to be called in order for them to succeed. Otherwise, the test will fail with an "UnexpectedBehavior" exception. + +Mock objects are used when we have to be very precise in the behavior specification. They usually make the tests more fragile than a spy but still are necessary in many cases. It is common to use mock objects together with stubs in tests. +

New to test doubles?

+A unit test is comprised of three parts: Arrange/Act/Assert or Given/When/Then or whatever you want to call them. The scenario has to be created, exercised, and eventually we verify that the expected behavior happened. The test doubles framework is used to create the scenario (create the objects), and verify behavior after the execution but it does not make sense to invoke test doubles' methods in the test code. If you call the doubles' methods in the test code, you are testing the framework itself, which has been already tested (better than that, we crafted it using TDD). Make sure the calls to the doubles' methods happen in your production code. +

Why another framework?

+pyDoubles is inspired in mockito and jMock for Java, and also inspired in Rhino.Mocks for .Net. There are other frameworks for Python that work really well, but after some time using them, we were not really happy with the syntax and the readability of the tests. Fragile tests were also a problem. Some well-known frameworks available for Python are: mocker, mockito-python, mock, pymox. + +pyDoubles is open source and free software, released under the Apache License Version 2.0 + +Take a look at the project's blog diff -Nru doublex-1.6.6/pydoubles-site/pydoubles-documentation doublex-1.7.2/pydoubles-site/pydoubles-documentation --- doublex-1.6.6/pydoubles-site/pydoubles-documentation 1970-01-01 00:00:00.000000000 +0000 +++ doublex-1.7.2/pydoubles-site/pydoubles-documentation 2013-11-07 21:00:04.000000000 +0000 @@ -0,0 +1,167 @@ +
class SimpleExample(unittest.TestCase):
+   def test_ask_the_sender_to_send_the_report(self):
+        sender = spy(Sender())
+        service = SavingsService(sender)
+
+        service.analyze_month()
+        assert_that_method(sender.send_email).was_called(
+                        ).with_args('reports@x.com', ANY_ARG)
+

Import the framework in your tests

+
import unittest
+from doublex.pyDoubles import *
+If you are afraid of importing everything from the pyDoubles.framework module, you can use custom imports, although it has been carefully designed to not conflict with your own classes. +
import unittest
+from doublex.pyDoubles import stub, spy, mock
+from doublex.pyDoubles import when, expect_call, assert_that_method
+from doublex.pyDoubles import method_returning, method_raising
+You can import Hamcrest matchers which are fully supported: +
from hamcrest import *
+

Which doubles do you need?

+You can choose to stub out a method in a regular object instance, to stub the whole object, or to create three types of spies and two types of mock objects. +

Stubs

+There are several ways to stub out methods. +
Stub out a single method
+If you just need to replace a single method in the collaborator object and you don't care about the input parameters, you can stub out just that single method: +
collaborator = Collaborator() # create the actual object
+collaborator.some_calculation = method_returning(10)
+Now, when your production code invokes the method "some_calculation" in the collaborator object, the framework will return 10, no matter what parameters are passed in as the input. + +If you want the method to raise an exception when called use this: +
collaborator.some_calculation = method_raising(ApplicationException())
+You can pass in any type of exception. +
Stub out the whole object
+Now the collaborator instance won't be the actual object but a replacement. +
collaborator = stub(Collaborator())
+Any method will return "None" when called with any input parameters. +If you want to change the return value you can use the "when" sentence: +
when(collaborator.some_calculation).then_return(10)
+Now, when your production code invokes "some_calculation" method, the stub will return 10, no matter what arguments are passed in. +You can also specify different return values depending on the input: +
when(collaborator.some_calculation).with_args(5).then_return(10)
+when(collaborator.some_calculation).with_args(10).then_return(20)
+This means that "collaborator.some_calculation(5)" will return 10, and that it will return 20 when the input is 10. You can define as many input/output specifications as you want. +
when(collaborator.some_calculation).with_args(5).then_return(10)
+when(collaborator.some_calculation).then_return(20)
+This time, "collaborator.some_calculation(5)" will return 10, and it will return 20 in any other case. +
Any argument matches
+The special keyword ANY_ARG is a wildcard for any argument in the +stubbed method: +
when(collaborator.some_other_method).with_args(5, ANY_ARG).then_return(10)
+The method "some_other_method" will return 10 as long as the first parameter is 5, no matter what the second parameter is. You can use any combination of "ANY_ARG" arguments. But remember that if all of them are ANY, you shouldn't specify the arguments, just use this: +
when(collaborator.some_other_method).then_return(10)
+It is also possible to make the method return exactly the first parameter passed in: +
when(collaborator.some_other_method).then_return_input()
+So this call: collaborator.some_other_method(10) wil return 10. +
Matchers
+You can also specify that arguments will match a certain function. Say that you want to return a value only if the input argument contains the substring "abc": +
when(collaborator.some_method).with_args(
+        str_containing("abc")).then_return(10)
+In the last release, pyDoubles matchers are just aliases for the hamcrest counterparts. See release notes. +
Hamcrest Matchers
+Since pyDoubles v1.2, we fully support Hamcrest matchers. +They are used exactly like pyDoubles matchers: +
from hamcrest import *
+from doublex.pyDoubles import *
+
+    def test_has_entry_matcher(self):
+        list = {'one':1, 'two':2}
+        when(self.spy.one_arg_method).with_args(
+            has_entry(equal_to('two'), 2)).then_return(1000)
+        assert_that(1000, equal_to(self.spy.one_arg_method(list)))
+
+    def test_all_of_matcher(self):
+        text = 'hello'
+        when(self.spy.one_arg_method).with_args(
+            all_of(starts_with('h'), instance_of(str))).then_return(1000)
+        assert_that(1000, equal_to(self.spy.one_arg_method(text)))
+Note that the tests above are just showhing the pyDoubles framework working together with Hamcrest, they are not good examples of unit tests for your production code. +The method assert_that comes from Hamcrest, as well as the matchers: has_entry, equal_to, all_of, starts_with, instance_of. +Notice that all_of and any_of, allow you to define more than one matcher for a single argument, which is really powerful. +For more informacion on matchers, read this blog post. +
Stub out the whole unexisting object
+If the Collaborator class does not exist yet, or you don't want the framework to check that the call to the stub object method matches the actual API in the actual object, you can use an "empty" stub. +
collaborator = empty_stub()
+when(collaborator.alpha_operation).then_return("whatever")
+The framework is creating the method "alpha_operation" dynamically +and making it return "whatever". + +The use of empty_stub, empty_spy or empty_mock is not recommended because you lose the API match check. We only use them as the construction of the object is too complex among other circumstances. +

Spies

+Please read the documentation above about stubs, because the API to +define method behaviors is the same for stubs and spies. To create +the object: +
collaborator = spy(Collaborator())
+After the execution of the system under test, we want to validate +that certain call was made: +
assert_that_method(collaborator.send_email).was_called()
+That will make the test pass if method "send_email" was invoked one or more times, no matter what arguments were passed in. +We can also be precise about the arguments: +
assert_that_method(collaborator.send_email).was_called().with_args("example@iexpertos.com")
+Notice that you can combine the "when" statement with the called assertion: +
def test_sut_asks_the_collaborator_to_send_the_email(self):
+   sender = spy(Sender())
+   when(sender.send_email).then_return(SUCCESS)
+   object_under_test = Sut(sender)
+
+   object_under_test.some_action()
+
+   assert_that_method(
+ sender.send_email).was_called().with_args("example@iexpertos.com")
+Any other call to any method in the "sender" double will return "None" and will not interrupt the test. We are not telling all that happens between the sender and the SUT, we are just asserting on what we want to verify. + +The ANY_ARG matcher can be used to verify the call as well: +
assert_that_method(collaborator.some_other_method).was_called().with_args(5, ANY_ARG)
+Matchers can also be used in the assertion: +
assert_that_method(collaborator.some_other_method).was_called().with_args(5, str_containing("abc"))
+It is also possible to assert that wasn't called using: +
assert_that_method(collaborator.some_method).was_never_called()
+You can assert on the number of times a call was made: +
assert_that_method(collaborator.some_method).was_called().times(2)
+assert_that_method(collaborator.some_method).was_called(
+     ).with_args(SOME_VALUE, OTHER_VALUE).times(2)
+You can also create an "empty_spy" to not base the object in a +certain instance: +
sender = empty_spy()
+
The ProxySpy
+There is a special type of spy supported by the framework which +is the ProxySpy: +
collaborator = proxy_spy(Collaborator())
+The proxy spy will record any call made to the object but rather than replacing the actual methods in the actual object, it will execute them. So the actual methods in the Collaborator will be invoked by default. You can replace the methods one by one using the "when" statement: +
when(collaborator.some_calculation).then_return(1000)
+Now "some_calculation" method will be a stub method but the remaining methods in the class will be the regular implementation. + +The ProxySpy might be interesting when you don't know what the actual method will return in a given scenario, but still you want to check that some call is made. It can be used for debugging purposes. +

Mocks

+Before calls are made, they have to be expected: +
def test_sut_asks_the_collaborator_to_send_the_email(self):
+   sender = mock(Sender())
+   expect_call(sender.send_email)
+   object_under_test = Sut(sender)
+
+   object_under_test.some_action()
+
+   sender.assert_that_is_satisfied()
+The test is quite similar to the one using a spy. However the framework behaves different. If any other call to the sender is made during "some_action", the test will fail. This makes the test more fragile. However, it makes sure that this interaction is the only one between the two objects, and this might be important for you. +
More precise expectations
+You can also expect the call to have certain input parameters: +
expect_call(sender.send_email).with_args("example@iexpertos.com")
+
Setting the return of the expected call
+Additionally, if you want to return anything when the expected call +occurs, there are two ways: +
expect_call(sender.send_email).returning(SUCCESS)
+Which will return SUCCESS whatever arguments you pass in, or +
expect_call(sender.send_email).with_args("wrong_email").returning(FAILURE)
+Which expects the method to be invoked with "wrong_email" and will return FAILURE. + +Mocks are strict so if you expect the call to happen several times, be explicit with that: +
expect_call(sender.send_email).times(2)
+
expect_call(sender.send_email).with_args("admin@iexpertos.com").times(2)
+Make sure the "times" part is at the end of the sentence: +
expect_call(sender.send_email).with_args("admin@iexpertos.com").returning('OK').times(2)
+As you might have seen, the "when" statement is not used for mocks, only for stubs and spies. Mock objects use the "expect_call" syntax together with the "assert_that_is_satisfied" +(instance method). +

More documentation

+The best and most updated documentation are the unit tests of the framework itself. We encourage the user to read the tests and see what features are supported in every commit into the source code repository: +pyDoublesTests/unit.py + +You can also read about what's new in every release in the blog diff -Nru doublex-1.6.6/pydoubles-site/release-notes doublex-1.7.2/pydoubles-site/release-notes --- doublex-1.6.6/pydoubles-site/release-notes 1970-01-01 00:00:00.000000000 +0000 +++ doublex-1.7.2/pydoubles-site/release-notes 2013-11-07 21:00:04.000000000 +0000 @@ -0,0 +1,38 @@ +

+

doublex 1.6.6

+ +

doublex 1.6.5

+ +
+

doublex 1.6.4

+
    +
  • Asynchronous spy assertion race condition bug fixed.
  • +
  • Reading double attributes returns collaborator.class attribute values by default.
  • +
+
+

doublex 1.6.2

+
    +
  • Invocation stubbed return value is now stored.
  • +
  • New low level spy API: double method  "calls" property provides access to invocations and their argument values. Each 'call' has an "args" sequence and "kargs dictionary". This provides support to perform individual assertions and direct access to invocation argument values. (see test and doc).
  • +
+

doublex 1.6

+
    +
  • First release supporting Python-3 (up to Python-3.2) [fixes issue 7].
  • +
  • Ad-hoc stub attributes (see test).
  • +
  • Partial support for non native Python functions.
  • +
  • ProxySpy propagated stubbed invocations too (see test).
  • +
+

doublex 1.5.1

+This release includes support for asynchronous spy assertions. See this blog post for the time being, soon in the official documentation. +

doublex/pyDoubles 1.5

+Since this release the pyDoubles API is provided as a wrapper to doublex. However, there are small differences. pyDoubles matchers are not supported anymore, although you may get the same feature using standard hamcrest matchers. Anyway, legacy pyDoubles matchers are provided as hamcrest aliases. + +In most cases the only required change in your code is the module name, that change from: +
from pyDoubles.framework.*
+to: +
from doublex.pyDoubles import *
+If you have problems migrating to the new 1.5 release or migrating from pyDoubles to doublex, please ask for help in the discussion forum or in the issue tracker. diff -Nru doublex-1.6.6/pydoubles-site/support doublex-1.7.2/pydoubles-site/support --- doublex-1.6.6/pydoubles-site/support 1970-01-01 00:00:00.000000000 +0000 +++ doublex-1.7.2/pydoubles-site/support 2013-11-07 21:00:04.000000000 +0000 @@ -0,0 +1,9 @@ +

Free support

+Mailing list: http://groups.google.com/group/pydoubles + +Issue tracker, mercurial repository: +https://bitbucket.org/carlosble/pydoubles/overview +Thanks to BitBucket! + +

Commercial support

+The development team of pyDoubles is a software company based in Spain. We are happy to help other companies with the usage and extension of pyDoubles. If you want to have custom features or direct support, please contact us at info@iexpertos.com diff -Nru doublex-1.6.6/setup.py doublex-1.7.2/setup.py --- doublex-1.6.6/setup.py 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/setup.py 2013-11-07 21:00:04.000000000 +0000 @@ -10,7 +10,7 @@ config = dict( name = 'doublex', - version = '1.6.5', + version = '1.7.1', description = 'Test doubles for Python', keywords = ['unit test', 'double', 'stub', 'spy', 'mock'], author = 'David Villa Alises', diff -Nru doublex-1.6.6/wiki.mk doublex-1.7.2/wiki.mk --- doublex-1.6.6/wiki.mk 2013-04-27 20:20:47.000000000 +0000 +++ doublex-1.7.2/wiki.mk 1970-01-01 00:00:00.000000000 +0000 @@ -1,4 +0,0 @@ -#!/usr/bin/make -f - -wiki: - hg clone ssh://hg@bitbucket.org/DavidVilla/python-doublex/wiki