Repository: itamarst/crochet

Issue:
Error using "run_in_reactor" in 1.8.0 that worked well in 1.7.0
use case:
```
@defer.inlineCallbacks
def do_stomething():
        yield do_something_else()

crochet.run_in_reactor(do_something)().wait()
```
exception in 1.8.0:
```
    def run_in_reactor(self, function):
        """
            A decorator that ensures the wrapped function runs in the
            reactor thread.
    
            When the wrapped function is called, an EventualResult is returned.
            """
        result = self._run_in_reactor(function)
        # Backwards compatibility; we could have used wrapt's version, but
        # older Crochet code exposed different attribute name:
        try:
>           result.wrapped_function = function
E           AttributeError: 'method' object has no attribute 'wrapped_function'

/usr/local/pyenv/versions/3.6.1/envs/python36/lib/python3.6/site-packages/crochet/_eventloop.py:469: AttributeError
```

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 0aa7ffd..73363d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,5 @@ MANIFEST
 # Documentation
 docs/_build
 
-build/
\ No newline at end of file
+build/
+.tox
\ No newline at end of file
diff --git a/crochet/_eventloop.py b/crochet/_eventloop.py
index 93a21dc..00c99d1 100644
--- a/crochet/_eventloop.py
+++ b/crochet/_eventloop.py
@@ -5,7 +5,6 @@ Expose Twisted's event loop to threaded programs.
 from __future__ import absolute_import
 
 import select
-import sys
 import threading
 import weakref
 import warnings
@@ -463,15 +462,11 @@ class EventLoop(object):
         When the wrapped function is called, an EventualResult is returned.
         """
         result = self._run_in_reactor(function)
-        # Backwards compatibility; we could have used wrapt's version, but
-        # older Crochet code exposed different attribute name:
+        # Backwards compatibility; use __wrapped__ instead.
         try:
             result.wrapped_function = function
         except AttributeError:
-            if sys.version_info[0] == 3:
-                raise
-            # In Python 2 e.g. classmethod has some limitations where you can't
-            # set stuff on it.
+            pass
         return result
 
     def wait_for_reactor(self, function):
@@ -518,14 +513,12 @@ class EventLoop(object):
                     raise
 
             result = wrapper(function)
-            # Expose underling function for testing purposes:
+            # Expose underling function for testing purposes; this attribute is
+            # deprecated, use __wrapped__ instead:
             try:
                 result.wrapped_function = function
             except AttributeError:
-                if sys.version_info[0] == 3:
-                    raise
-                # In Python 2 e.g. classmethod has some limitations where you
-                # can't set stuff on it.
+                pass
             return result
 
         return decorator
diff --git a/docs/api.rst b/docs/api.rst
index 7e99681..15d6700 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -162,14 +162,14 @@ Unit testing
 ^^^^^^^^^^^^
 
 Both ``@wait_for`` and ``@run_in_reactor`` expose the underlying Twisted
-function via a ``wrapped_function`` attribute. This allows unit testing of the
+function via a ``__wrapped__`` attribute. This allows unit testing of the
 Twisted code without having to go through the Crochet layer.
 
 .. literalinclude:: ../examples/testing.py
 
 When run, this gives the following output::
 
-    add() returns EventualResult:
+    add(1, 2) returns EventualResult:
          <crochet._eventloop.EventualResult object at 0x2e8b390>
-    add.wrapped_function() returns result of underlying function:
+    add.__wrapped__(1, 2) returns result of underlying function:
          3
diff --git a/docs/news.rst b/docs/news.rst
index 23365a3..0b664bb 100644
--- a/docs/news.rst
+++ b/docs/news.rst
@@ -1,6 +1,23 @@
 What's New
 ==========
 
+1.9.0
+^^^^^
+
+New features:
+
+* The underlying callable wrapped ``@run_in_reactor`` and ``@wait_for`` is now available via the more standard ``__wrapped__`` attribute.
+
+Backwards incompatibility (in tests):
+
+* This was actually introduced in 1.8.0: ``wrapped_function`` may not always be available on decorated callables.
+  You should use ``__wrapped__`` instead.
+
+Bug fixes:
+
+* Fixed regression in 1.8.0 where bound method couldn't be wrapped.
+  Thanks to 2mf for the bug report.
+
 1.8.0
 ^^^^^
 
@@ -23,7 +40,7 @@ Bug fixes:
 
 Bug fixes:
 
-* If the Python `logging.Handler` throws an exception Crochet no longer goes into a death spiral.
+* If the Python ``logging.Handler`` throws an exception Crochet no longer goes into a death spiral.
   Thanks to Michael Schlenker for the bug report.
 
 Removed features:
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..5b1a12b
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,17 @@
+[tox]
+envlist = py27, py35, lint3
+usedevelop = true
+
+[testenv]
+commands =
+    {envpython} setup.py --version
+    pip install .
+    {envpython} -m unittest {posargs:discover -v crochet.tests}
+
+[testenv:lint3]
+deps = flake8
+       pylint
+       yapf
+basepython = python3.5
+commands = flake8 crochet
+           pylint crochet
\ No newline at end of file


Test patch:
diff --git a/crochet/tests/test_api.py b/crochet/tests/test_api.py
index 3e2220d..b41b20c 100644
--- a/crochet/tests/test_api.py
+++ b/crochet/tests/test_api.py
@@ -728,6 +728,23 @@ class RunInReactorTests(TestCase):
         C.func2(1, 2, c=3)
         self.assertEqual(calls, [(C, 1, 2, 3), (C, 1, 2, 3)])
 
+    def test_wrap_method(self):
+        """
+        The object decorated with the wait decorator can be a method object
+        """
+        myreactor = FakeReactor()
+        c = EventLoop(lambda: myreactor, lambda f, g: None)
+        c.no_setup()
+        calls = []
+
+        class C(object):
+            def func(self, a, b, c):
+                calls.append((a, b, c))
+
+        f = c.run_in_reactor(C().func)
+        f(4, 5, c=6)
+        self.assertEqual(calls, [(4, 5, 6)])
+
     def make_wrapped_function(self):
         """
         Return a function wrapped with run_in_reactor that returns its first
@@ -809,7 +826,8 @@ class RunInReactorTests(TestCase):
     def test_wrapped_function(self):
         """
         The function wrapped by @run_in_reactor can be accessed via the
-        `wrapped_function` attribute.
+        `__wrapped__` attribute and the `wrapped_function` attribute
+        (deprecated, and not always available).
         """
         c = EventLoop(lambda: None, lambda f, g: None)
 
@@ -817,6 +835,7 @@ class RunInReactorTests(TestCase):
             pass
 
         wrapper = c.run_in_reactor(func)
+        self.assertIdentical(wrapper.__wrapped__, func)
         self.assertIdentical(wrapper.wrapped_function, func)
 
 
@@ -881,7 +900,7 @@ class WaitTestsMixin(object):
     def test_wrapped_function(self):
         """
         The function wrapped by the wait decorator can be accessed via the
-        `wrapped_function` attribute.
+        `__wrapped__`, and the deprecated `wrapped_function` attribute.
         """
         decorator = self.decorator()
 
@@ -889,6 +908,7 @@ class WaitTestsMixin(object):
             pass
 
         wrapper = decorator(func)
+        self.assertIdentical(wrapper.__wrapped__, func)
         self.assertIdentical(wrapper.wrapped_function, func)
 
     def test_reactor_thread_disallowed(self):
diff --git a/examples/testing.py b/examples/testing.py
index cded7dc..7db5696 100644
--- a/examples/testing.py
+++ b/examples/testing.py
@@ -14,7 +14,7 @@ def add(x, y):
 
 
 if __name__ == '__main__':
-    print("add() returns EventualResult:")
+    print("add(1, 2) returns EventualResult:")
     print("    ", add(1, 2))
-    print("add.wrapped_function() returns result of underlying function:")
-    print("    ", add.wrapped_function(1, 2))
+    print("add.__wrapped__(1, 2) is the result of the underlying function:")
+    print("    ", add.__wrapped__(1, 2))