Skip to content

Commit

Permalink
Merge pull request #114 from itamarst/signatures
Browse files Browse the repository at this point in the history
Better signatures on decorated methods, by using wrapt.

Fixes #55.
  • Loading branch information
itamarst authored Aug 9, 2017
2 parents f3cf5d9 + 8502bbd commit 2b87285
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 29 deletions.
5 changes: 1 addition & 4 deletions crochet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,4 @@ def _importReactor():
"wait_for",
"ReactorStopped",
"__version__",
# Backwards compatibility:
"DeferredResult",
"in_reactor",
"wait_for_reactor", ]
]
56 changes: 41 additions & 15 deletions crochet/_eventloop.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from __future__ import absolute_import

import select
import sys
import threading
import weakref
import warnings
Expand All @@ -19,6 +20,8 @@
from twisted.internet.defer import maybeDeferred
from twisted.internet.task import LoopingCall

import wrapt

from ._util import synchronized
from ._resultstore import ResultStore

Expand Down Expand Up @@ -434,10 +437,11 @@ def no_setup(self):
"using crochet are imported and call setup().")
self._common_setup()

def run_in_reactor(self, function):
@wrapt.decorator
def _run_in_reactor(self, function, _, args, kwargs):
"""
A decorator that ensures the wrapped function runs in the reactor
thread.
Implementation: A decorator that ensures the wrapped function runs in
the reactor thread.
When the wrapped function is called, an EventualResult is returned.
"""
Expand All @@ -446,15 +450,29 @@ def runs_in_reactor(result, args, kwargs):
d = maybeDeferred(function, *args, **kwargs)
result._connect_deferred(d)

@wraps(function)
def wrapper(*args, **kwargs):
result = EventualResult(None, self._reactor)
self._registry.register(result)
self._reactor.callFromThread(runs_in_reactor, result, args, kwargs)
return result
result = EventualResult(None, self._reactor)
self._registry.register(result)
self._reactor.callFromThread(runs_in_reactor, result, args, kwargs)
return result

def run_in_reactor(self, function):
"""
A decorator that ensures the wrapped function runs in the
reactor thread.
wrapper.wrapped_function = function
return wrapper
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
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.
return result

def wait_for_reactor(self, function):
"""
Expand Down Expand Up @@ -486,8 +504,8 @@ def wait_for(self, timeout):
"""

def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
@wrapt.decorator
def wrapper(function, _, args, kwargs):
@self.run_in_reactor
def run():
return function(*args, **kwargs)
Expand All @@ -499,8 +517,16 @@ def run():
eventual_result.cancel()
raise

wrapper.wrapped_function = function
return wrapper
result = wrapper(function)
# Expose underling function for testing purposes:
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.
return result

return decorator

Expand Down
20 changes: 11 additions & 9 deletions crochet/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@
Utility functions and classes.
"""

from functools import wraps
import wrapt


@wrapt.decorator
def _synced(method, self, args, kwargs):
"""Underlying synchronized wrapper."""
with self._lock:
return method(*args, **kwargs)


def synchronized(method):
"""
Decorator that wraps a method with an acquire/release of self._lock.
"""

@wraps(method)
def synced(self, *args, **kwargs):
with self._lock:
return method(self, *args, **kwargs)

synced.synchronized = True
return synced
result = _synced(method)
result.synchronized = True
return result
91 changes: 91 additions & 0 deletions crochet/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import tempfile
import os
import imp
import inspect

from twisted.trial.unittest import TestCase
from twisted.internet.defer import succeed, Deferred, fail, CancelledError
Expand Down Expand Up @@ -641,6 +642,19 @@ class RunInReactorTests(TestCase):
Tests for the run_in_reactor decorator.
"""

def test_signature(self):
"""
The function decorated with the run_in_reactor decorator has the same
signature as the original function.
"""
c = EventLoop(lambda: FakeReactor(), lambda f, g: None)

def some_name(arg1, arg2, karg1=2, *args, **kw):
pass
decorated = c.run_in_reactor(some_name)
self.assertEqual(inspect.getargspec(some_name),
inspect.getargspec(decorated))

def test_name(self):
"""
The function decorated with run_in_reactor has the same name as the
Expand Down Expand Up @@ -672,6 +686,48 @@ def func(a, b, c):
func(1, 2, c=3)
self.assertEqual(calls, [(1, 2, 3)])

def test_method(self):
"""
The function decorated with the wait decorator can be a method.
"""
myreactor = FakeReactor()
c = EventLoop(lambda: myreactor, lambda f, g: None)
c.no_setup()
calls = []

class C(object):
@c.run_in_reactor
def func(self, a, b, c):
calls.append((self, a, b, c))

o = C()
o.func(1, 2, c=3)
self.assertEqual(calls, [(o, 1, 2, 3)])

def test_classmethod(self):
"""
The function decorated with the wait decorator can be a classmethod.
"""
myreactor = FakeReactor()
c = EventLoop(lambda: myreactor, lambda f, g: None)
c.no_setup()
calls = []

class C(object):
@c.run_in_reactor
@classmethod
def func(cls, a, b, c):
calls.append((cls, a, b, c))

@classmethod
@c.run_in_reactor
def func2(cls, a, b, c):
calls.append((cls, a, b, c))

C.func(1, 2, c=3)
C.func2(1, 2, c=3)
self.assertEqual(calls, [(C, 1, 2, 3), (C, 1, 2, 3)])

def make_wrapped_function(self):
"""
Return a function wrapped with run_in_reactor that returns its first
Expand Down Expand Up @@ -809,6 +865,19 @@ def some_name(argument):

self.assertEqual(some_name.__name__, "some_name")

def test_signature(self):
"""
The function decorated with the wait decorator has the same signature
as the original function.
"""
decorator = self.decorator()

def some_name(arg1, arg2, karg1=2, *args, **kw):
pass
decorated = decorator(some_name)
self.assertEqual(inspect.getargspec(some_name),
inspect.getargspec(decorated))

def test_wrapped_function(self):
"""
The function wrapped by the wait decorator can be accessed via the
Expand Down Expand Up @@ -863,6 +932,28 @@ def func(a, b, c):
func(1, 2, c=3)
self.assertEqual(calls, [(1, 2, 3)])

def test_classmethod(self):
"""
The function decorated with the wait decorator can be a classmethod.
"""
calls = []
decorator = self.decorator()

class C(object):
@decorator
@classmethod
def func(cls, a, b, c):
calls.append((a, b, c))

@classmethod
@decorator
def func2(cls, a, b, c):
calls.append((a, b, c))

C.func(1, 2, c=3)
C.func2(1, 2, c=3)
self.assertEqual(calls, [(1, 2, 3), (1, 2, 3)])

def test_deferred_success_result(self):
"""
If the underlying function returns a Deferred, the wrapper returns a
Expand Down
12 changes: 12 additions & 0 deletions docs/api-reference.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
API Reference
=============

.. autofunction:: crochet.setup()
.. autofunction:: crochet.no_setup()
.. autofunction:: crochet.run_in_reactor(function)
.. autofunction:: crochet.wait_for(timeout)
.. autoclass:: crochet.EventualResult
:members:
.. autofunction:: crochet.retrieve_result(result_id)
.. autoexception:: crochet.TimeoutError
.. autoexception:: crochet.ReactorStopped
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = []
extensions = ["sphinx.ext.autodoc"]

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ Table of Contents
using
workarounds
async
api-reference
news
8 changes: 8 additions & 0 deletions docs/news.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ What's New
1.8.0
^^^^^

New features:

* Signatures on decorated functions now match the original functions.
Thanks to Mikhail Terekhov for the original patch.
* Documentation improvements, including an API reference.

Bug fixes:

* Switched to EPoll reactor for logging thread.
Anecdotal evidence suggests this fixes some issues on AWS Lambda, but it's not clear why.
Thanks to Rolando Espinoza for the patch.
* It's now possible to call ``@run_in_reactor`` and ``@wait_for`` above a ``@classmethod``.
Thanks to vak for the bug report.

1.7.0
^^^^^
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def read(path):
with open(path) as f:
return f.read()


setup(
classifiers=[
'Intended Audience :: Developers',
Expand All @@ -32,6 +33,7 @@ def read(path):
description="Use Twisted anywhere!",
install_requires=[
"Twisted>=15.0",
"wrapt",
],
keywords="twisted threading",
license="MIT",
Expand Down

0 comments on commit 2b87285

Please sign in to comment.