Skip to content

Commit 2b87285

Browse files
authored
Merge pull request #114 from itamarst/signatures
Better signatures on decorated methods, by using wrapt. Fixes #55.
2 parents f3cf5d9 + 8502bbd commit 2b87285

File tree

9 files changed

+168
-29
lines changed

9 files changed

+168
-29
lines changed

crochet/__init__.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,4 @@ def _importReactor():
6666
"wait_for",
6767
"ReactorStopped",
6868
"__version__",
69-
# Backwards compatibility:
70-
"DeferredResult",
71-
"in_reactor",
72-
"wait_for_reactor", ]
69+
]

crochet/_eventloop.py

+41-15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from __future__ import absolute_import
66

77
import select
8+
import sys
89
import threading
910
import weakref
1011
import warnings
@@ -19,6 +20,8 @@
1920
from twisted.internet.defer import maybeDeferred
2021
from twisted.internet.task import LoopingCall
2122

23+
import wrapt
24+
2225
from ._util import synchronized
2326
from ._resultstore import ResultStore
2427

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

437-
def run_in_reactor(self, function):
440+
@wrapt.decorator
441+
def _run_in_reactor(self, function, _, args, kwargs):
438442
"""
439-
A decorator that ensures the wrapped function runs in the reactor
440-
thread.
443+
Implementation: A decorator that ensures the wrapped function runs in
444+
the reactor thread.
441445
442446
When the wrapped function is called, an EventualResult is returned.
443447
"""
@@ -446,15 +450,29 @@ def runs_in_reactor(result, args, kwargs):
446450
d = maybeDeferred(function, *args, **kwargs)
447451
result._connect_deferred(d)
448452

449-
@wraps(function)
450-
def wrapper(*args, **kwargs):
451-
result = EventualResult(None, self._reactor)
452-
self._registry.register(result)
453-
self._reactor.callFromThread(runs_in_reactor, result, args, kwargs)
454-
return result
453+
result = EventualResult(None, self._reactor)
454+
self._registry.register(result)
455+
self._reactor.callFromThread(runs_in_reactor, result, args, kwargs)
456+
return result
457+
458+
def run_in_reactor(self, function):
459+
"""
460+
A decorator that ensures the wrapped function runs in the
461+
reactor thread.
455462
456-
wrapper.wrapped_function = function
457-
return wrapper
463+
When the wrapped function is called, an EventualResult is returned.
464+
"""
465+
result = self._run_in_reactor(function)
466+
# Backwards compatibility; we could have used wrapt's version, but
467+
# older Crochet code exposed different attribute name:
468+
try:
469+
result.wrapped_function = function
470+
except AttributeError:
471+
if sys.version_info[0] == 3:
472+
raise
473+
# In Python 2 e.g. classmethod has some limitations where you can't
474+
# set stuff on it.
475+
return result
458476

459477
def wait_for_reactor(self, function):
460478
"""
@@ -486,8 +504,8 @@ def wait_for(self, timeout):
486504
"""
487505

488506
def decorator(function):
489-
@wraps(function)
490-
def wrapper(*args, **kwargs):
507+
@wrapt.decorator
508+
def wrapper(function, _, args, kwargs):
491509
@self.run_in_reactor
492510
def run():
493511
return function(*args, **kwargs)
@@ -499,8 +517,16 @@ def run():
499517
eventual_result.cancel()
500518
raise
501519

502-
wrapper.wrapped_function = function
503-
return wrapper
520+
result = wrapper(function)
521+
# Expose underling function for testing purposes:
522+
try:
523+
result.wrapped_function = function
524+
except AttributeError:
525+
if sys.version_info[0] == 3:
526+
raise
527+
# In Python 2 e.g. classmethod has some limitations where you
528+
# can't set stuff on it.
529+
return result
504530

505531
return decorator
506532

crochet/_util.py

+11-9
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22
Utility functions and classes.
33
"""
44

5-
from functools import wraps
5+
import wrapt
6+
7+
8+
@wrapt.decorator
9+
def _synced(method, self, args, kwargs):
10+
"""Underlying synchronized wrapper."""
11+
with self._lock:
12+
return method(*args, **kwargs)
613

714

815
def synchronized(method):
916
"""
1017
Decorator that wraps a method with an acquire/release of self._lock.
1118
"""
12-
13-
@wraps(method)
14-
def synced(self, *args, **kwargs):
15-
with self._lock:
16-
return method(self, *args, **kwargs)
17-
18-
synced.synchronized = True
19-
return synced
19+
result = _synced(method)
20+
result.synchronized = True
21+
return result

crochet/tests/test_api.py

+91
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import tempfile
1414
import os
1515
import imp
16+
import inspect
1617

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

645+
def test_signature(self):
646+
"""
647+
The function decorated with the run_in_reactor decorator has the same
648+
signature as the original function.
649+
"""
650+
c = EventLoop(lambda: FakeReactor(), lambda f, g: None)
651+
652+
def some_name(arg1, arg2, karg1=2, *args, **kw):
653+
pass
654+
decorated = c.run_in_reactor(some_name)
655+
self.assertEqual(inspect.getargspec(some_name),
656+
inspect.getargspec(decorated))
657+
644658
def test_name(self):
645659
"""
646660
The function decorated with run_in_reactor has the same name as the
@@ -672,6 +686,48 @@ def func(a, b, c):
672686
func(1, 2, c=3)
673687
self.assertEqual(calls, [(1, 2, 3)])
674688

689+
def test_method(self):
690+
"""
691+
The function decorated with the wait decorator can be a method.
692+
"""
693+
myreactor = FakeReactor()
694+
c = EventLoop(lambda: myreactor, lambda f, g: None)
695+
c.no_setup()
696+
calls = []
697+
698+
class C(object):
699+
@c.run_in_reactor
700+
def func(self, a, b, c):
701+
calls.append((self, a, b, c))
702+
703+
o = C()
704+
o.func(1, 2, c=3)
705+
self.assertEqual(calls, [(o, 1, 2, 3)])
706+
707+
def test_classmethod(self):
708+
"""
709+
The function decorated with the wait decorator can be a classmethod.
710+
"""
711+
myreactor = FakeReactor()
712+
c = EventLoop(lambda: myreactor, lambda f, g: None)
713+
c.no_setup()
714+
calls = []
715+
716+
class C(object):
717+
@c.run_in_reactor
718+
@classmethod
719+
def func(cls, a, b, c):
720+
calls.append((cls, a, b, c))
721+
722+
@classmethod
723+
@c.run_in_reactor
724+
def func2(cls, a, b, c):
725+
calls.append((cls, a, b, c))
726+
727+
C.func(1, 2, c=3)
728+
C.func2(1, 2, c=3)
729+
self.assertEqual(calls, [(C, 1, 2, 3), (C, 1, 2, 3)])
730+
675731
def make_wrapped_function(self):
676732
"""
677733
Return a function wrapped with run_in_reactor that returns its first
@@ -809,6 +865,19 @@ def some_name(argument):
809865

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

868+
def test_signature(self):
869+
"""
870+
The function decorated with the wait decorator has the same signature
871+
as the original function.
872+
"""
873+
decorator = self.decorator()
874+
875+
def some_name(arg1, arg2, karg1=2, *args, **kw):
876+
pass
877+
decorated = decorator(some_name)
878+
self.assertEqual(inspect.getargspec(some_name),
879+
inspect.getargspec(decorated))
880+
812881
def test_wrapped_function(self):
813882
"""
814883
The function wrapped by the wait decorator can be accessed via the
@@ -863,6 +932,28 @@ def func(a, b, c):
863932
func(1, 2, c=3)
864933
self.assertEqual(calls, [(1, 2, 3)])
865934

935+
def test_classmethod(self):
936+
"""
937+
The function decorated with the wait decorator can be a classmethod.
938+
"""
939+
calls = []
940+
decorator = self.decorator()
941+
942+
class C(object):
943+
@decorator
944+
@classmethod
945+
def func(cls, a, b, c):
946+
calls.append((a, b, c))
947+
948+
@classmethod
949+
@decorator
950+
def func2(cls, a, b, c):
951+
calls.append((a, b, c))
952+
953+
C.func(1, 2, c=3)
954+
C.func2(1, 2, c=3)
955+
self.assertEqual(calls, [(1, 2, 3), (1, 2, 3)])
956+
866957
def test_deferred_success_result(self):
867958
"""
868959
If the underlying function returns a Deferred, the wrapper returns a

docs/api-reference.rst

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
API Reference
2+
=============
3+
4+
.. autofunction:: crochet.setup()
5+
.. autofunction:: crochet.no_setup()
6+
.. autofunction:: crochet.run_in_reactor(function)
7+
.. autofunction:: crochet.wait_for(timeout)
8+
.. autoclass:: crochet.EventualResult
9+
:members:
10+
.. autofunction:: crochet.retrieve_result(result_id)
11+
.. autoexception:: crochet.TimeoutError
12+
.. autoexception:: crochet.ReactorStopped

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

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

2828
# Add any paths that contain templates here, relative to this directory.
2929
templates_path = ['_templates']

docs/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ Table of Contents
3232
using
3333
workarounds
3434
async
35+
api-reference
3536
news

docs/news.rst

+8
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@ What's New
44
1.8.0
55
^^^^^
66

7+
New features:
8+
9+
* Signatures on decorated functions now match the original functions.
10+
Thanks to Mikhail Terekhov for the original patch.
11+
* Documentation improvements, including an API reference.
12+
713
Bug fixes:
814

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

1321
1.7.0
1422
^^^^^

setup.py

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def read(path):
1313
with open(path) as f:
1414
return f.read()
1515

16+
1617
setup(
1718
classifiers=[
1819
'Intended Audience :: Developers',
@@ -32,6 +33,7 @@ def read(path):
3233
description="Use Twisted anywhere!",
3334
install_requires=[
3435
"Twisted>=15.0",
36+
"wrapt",
3537
],
3638
keywords="twisted threading",
3739
license="MIT",

0 commit comments

Comments
 (0)