Repository: kaste/mockito-python

Issue:
when2 AttributeError: 'Dummy' has no attribute configured
when2 seems not to work as expected.

```
Python 3.8.10 (default, Sep 28 2021, 16:10:42) 
[GCC 9.3.0] on linux
```
mockito 1,5,3 , also not working with 1.5.1



how to reproduce
```
from mockito import mock,when2
class A:
  def f(self):
    return "no"
a=mock(A)
when2(a.f).thenReturn("yes")
```

gives
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/tmp/PythonVenvs/geniusweb1targz/venv/lib/python3.8/site-packages/mockito/mocking.py", line 299, in __getattr__
    raise AttributeError(
AttributeError: 'Dummy' has no attribute 'f' configured
```

expected: a with mocked f function

Strange is that it works fine with some built-in functions:

```
when2(a.__repr__).thenReturn("yes")
<mockito.invocation.AnswerSelector object at 0x7fb3eceb5cd0>
```


Maybe related to #57

Discussion:
@kaste: `mock(A)` provides a spec (`A`) which makes the mock `strict` by default.  After that accessing any attribute raises immediately.  Typically you would just use `when`.  Actually, I don't know if I personally ever tried/used  `when2` with `mock`s.  But you could try `mock(A, strict=False)` of course.  

Although mocks are dumb, some dunder methods are defined for convenience, `__repr__` amongst them.@kaste: Maybe a special form `when2("a.f")` could be implemented.  @kaste: Just checked!  `when2` does *not* work here, basically because `mock`s are too dynamic.  `when2` was probably designed with patching objects in mind.  Internally `when2` does access e.g. `a.f` *twice* and mocks give different return values on *each* usage.  This could likely be fixed as the second call is ironically just the sanity check, a final assertion, that fails.     @kaste: The form `when2(m.foo)` now works (for dumb mocks).  For strict mocks, it doesn't, as the attribute access of unconfigured attributes already raises by design.  

The general string form, e.g. `when2("os.path.exists"`, is already implemented.  However, the string form always expects that it denotes an importable thing.  E.g. it translates to `from os.path import exists`.   In contrast, `when2("m.foo")` would point to a local variable, and that is -- although doable -- confusing.  Hence it is not implemented as for now. 

Solution patch:
diff --git a/mockito/mocking.py b/mockito/mocking.py
index e1b634e..4076138 100644
--- a/mockito/mocking.py
+++ b/mockito/mocking.py
@@ -18,7 +18,6 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 # THE SOFTWARE.
 
-import functools
 import inspect
 import operator
 from collections import deque
@@ -311,8 +310,20 @@ def __getattr__(self, method_name):
             ):
                 raise AttributeError(method_name)
 
-            return functools.partial(
-                remembered_invocation_builder, theMock, method_name)
+            def ad_hoc_function(*args, **kwargs):
+                return remembered_invocation_builder(
+                    theMock, method_name, *args, **kwargs)
+            ad_hoc_function.__name__ = method_name
+            ad_hoc_function.__self__ = obj  # type: ignore[attr-defined]
+            if spec:
+                try:
+                    original_method = getattr(spec, method_name)
+                    ad_hoc_function.__wrapped__ = original_method  # type: ignore[attr-defined]  # noqa: E501
+                    ad_hoc_function.__doc__ = original_method.__doc__
+                except AttributeError:
+                    pass
+
+            return ad_hoc_function
 
         def __repr__(self):
             name = 'Dummy'
diff --git a/mockito/utils.py b/mockito/utils.py
index 430326b..098693c 100644
--- a/mockito/utils.py
+++ b/mockito/utils.py
@@ -65,8 +65,14 @@ def get_function_host(fn):
 
         obj, name = find_invoking_frame_and_try_parse()
         # safety check!
-        assert getattr(obj, name) == fn
-
+        assert getattr(obj, name) == fn, (
+            "`getattr(obj, name) != fn` where\n"
+            "fn: %s\n"
+            "obj: %s\n"
+            "name: %s\n"
+            "getattr(obj, name): %s\n"
+            % (fn, obj, name, getattr(obj, name))
+        )
 
     return obj, name
 


Test patch:
diff --git a/tests/speccing_test.py b/tests/speccing_test.py
index a3c974c..d84535a 100644
--- a/tests/speccing_test.py
+++ b/tests/speccing_test.py
@@ -33,6 +33,7 @@ def no_arg(self):
         pass
 
     def run(self, arg):
+        """this is a docstring"""
         return arg
 
     def __call__(self, task):
@@ -47,7 +48,6 @@ def testStubCallAndVerify(self):
         assert action.run(11) == 12
         verify(action).run(11)
 
-
     def testShouldScreamWhenStubbingUnknownMethod(self):
         action = mock(Action)
 
@@ -70,8 +70,6 @@ def testPreconfigureMockWithAttributes(self):
         action = mock({'foo': 'bar'}, spec=Action)
 
         assert action.foo == 'bar'
-        with pytest.raises(InvocationError):
-            when(action).remember()
 
     def testPreconfigureWithFunction(self):
         action = mock({
@@ -102,26 +100,47 @@ def testShouldPassIsInstanceChecks(self):
 
         assert isinstance(action, Action)
 
-    def testShouldPassIsInstanceChecks_2(self):
-        action = mock(spec=Action)
+    class TestVariousConstructorForms:
+        def testUsingKeywordArg(self):
+            action = mock(spec=Action)
 
-        assert isinstance(action, Action)
+            assert isinstance(action, Action)
 
-    def testShouldPassIsInstanceChecks_3(self):
-        action = mock({}, Action)
+        def testUsingPositionalArg(self):
+            action = mock(Action)
 
-        assert isinstance(action, Action)
+            assert isinstance(action, Action)
 
-    def testShouldPassIsInstanceChecks_4(self):
-        action = mock({}, spec=Action)
+        def testKeywordAfterConfigurationObject(self):
+            action = mock({}, spec=Action)
 
-        assert isinstance(action, Action)
+            assert isinstance(action, Action)
+
+        def testPositionalArgAfterConfigurationObject(self):
+            action = mock({}, Action)
 
-    def testHasANiceName(self):
+            assert isinstance(action, Action)
+
+    def testMockHasANiceName(self):
         action = mock(Action)
 
         assert repr(action) == "<DummyAction id=%s>" % id(action)
 
+    def testConfiguredMethodHasCorrectName(self):
+        action = mock(Action)
+        when(action).run(11).thenReturn(12)
+        assert action.run.__name__ == 'run'
+
+    def testConfiguredMethodIsBound(self):
+        action = mock(Action)
+        when(action).run(11).thenReturn(12)
+        assert action.run.__self__ == action
+
+    def testConfiguredMethodHasCorrectDocstring(self):
+        action = mock(Action)
+        when(action).run(11).thenReturn(12)
+        assert action.run.__doc__ == 'this is a docstring'
+
 
 class TestSpeccingLoose:
     def testReturnNoneForEveryMethod(self):
diff --git a/tests/when2_test.py b/tests/when2_test.py
index 1f21d51..84deafc 100644
--- a/tests/when2_test.py
+++ b/tests/when2_test.py
@@ -1,7 +1,7 @@
 
 import pytest
 
-from mockito import when2, patch, spy2, verify
+from mockito import mock, when2, patch, spy2, verify
 from mockito.utils import newmethod
 
 
@@ -147,3 +147,19 @@ def testA(self):
             with pytest.raises(TypeError) as exc:
                 ptch(os.path.exists, lambda: 'boo')
             assert str(exc.value) == "could not destructure first argument"
+
+
+class TestFixIssue90:
+    def testAddAttributeToDumbMock(self):
+        m = mock()
+        when2(m.foo).thenReturn('bar')
+        assert m.foo() == 'bar'
+
+    @pytest.mark.xfail(reason=(
+        "strict mocks throw when accessing unconfigured attributes by design"
+    ))
+    def testAddAttributeToStrictMock(self):
+        m = mock(strict=True)
+        when2(m.foo).thenReturn('bar')
+        assert m.foo() == 'bar'
+