├── .gitignore
├── .travis.yml
├── LICENSE
├── MANIFEST.in
├── README.rst
├── examples
├── echo-server.py
└── hello-world.py
├── requirements.txt
├── setup.py
├── tornaduv
└── __init__.py
└── tox.ini
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | MANIFEST
3 | .tox/
4 | dist/
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | matrix:
4 | include:
5 | - python: 2.7
6 | env: TOX_ENV=py27
7 | - python: 2.7
8 | env: TOX_ENV=py27-deps
9 | - python: 2.7
10 | env: TOX_ENV=py27-t3
11 |
12 | - python: 3.3
13 | env: TOX_ENV=py33
14 | - python: 3.3
15 | env: TOX_ENV=py33-deps
16 | - python: 3.3
17 | env: TOX_ENV=py33-t3
18 |
19 | - python: 3.4
20 | env: TOX_ENV=py34
21 | - python: 3.4
22 | env: TOX_ENV=py34-deps
23 | - python: 3.4
24 | env: TOX_ENV=py34-t3
25 |
26 | - python: 3.5
27 | env: TOX_ENV=py35
28 | - python: 3.5
29 | env: TOX_ENV=py35-deps
30 |
31 | - python: 3.6
32 | env: TOX_ENV=py36
33 | - python: 3.6
34 | env: TOX_ENV=py36-deps
35 |
36 |
37 | sudo: false
38 |
39 | install:
40 | - pip install tox
41 |
42 | script:
43 | - tox -e $TOX_ENV
44 |
45 | notifications:
46 | email:
47 | on_success: never
48 | on_failure: change
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2011 by Saúl Ibarra Corretgé
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
21 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst LICENSE TODO requirements.txt
2 | recursive-include examples *
3 | recursive-exclude * __pycache__
4 | recursive-exclude * *.py[co]
5 |
6 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ==============================
2 | A Tornado IOLoop based on pyuv
3 | ==============================
4 |
5 | .. image:: https://travis-ci.org/saghul/tornaduv.svg?branch=master
6 | :target: https://travis-ci.org/saghul/tornaduv
7 | :alt: Build status
8 |
9 | .. image:: https://pypip.in/download/tornaduv/badge.png
10 | :target: https://pypi.python.org/pypi/tornaduv/
11 | :alt: Downloads
12 |
13 | .. image:: https://pypip.in/version/tornaduv/badge.png
14 | :target: https://pypi.python.org/pypi/tornaduv/
15 | :alt: Latest Version
16 |
17 | .. image:: https://pypip.in/license/tornaduv/badge.png
18 | :target: https://pypi.python.org/pypi/tornaduv/
19 | :alt: License
20 |
21 |
22 | tornaduv is a `Tornado `_ IOLoop implementation
23 | which uses `pyuv `_ as the networking library instead
24 | of the builtin epoll and kqueue pollers included in Tornado.
25 |
26 | pyuv is a Python interface for libuv, a high performance asynchronous
27 | networking library used as the platform layer for NodeJS.
28 |
29 |
30 | Installation
31 | ============
32 |
33 | tornaduv requires pyuv >= 1.0.0 and Tornado >= 3.0.
34 |
35 | ::
36 |
37 | pip install tornaduv
38 |
39 |
40 | Using it
41 | ========
42 |
43 | In order to use tornaduv, Tornado needs to be instructed to use
44 | our IOLoop. In order to do that add the following lines at the beginning
45 | of your project:
46 |
47 | ::
48 |
49 | from tornado.ioloop import IOLoop
50 | from tornaduv import UVLoop
51 | IOLoop.configure(UVLoop)
52 |
53 |
54 | Testing
55 | =======
56 |
57 | If you want to run the Tornado test suite using tornaduv run the following command:
58 |
59 | ::
60 |
61 | python -m tornado.test.runtests --ioloop='tornaduv.UVLoop' --verbose
62 |
63 |
64 | Authors
65 | =======
66 |
67 | Saúl Ibarra Corretgé
68 | Marc Schlaich
69 |
70 |
71 | License
72 | =======
73 |
74 | tornaduv uses the MIT license, check LICENSE file.
75 |
76 |
--------------------------------------------------------------------------------
/examples/echo-server.py:
--------------------------------------------------------------------------------
1 |
2 | import signal
3 |
4 | from tornado.ioloop import IOLoop
5 | from tornado.tcpserver import TCPServer
6 |
7 | from tornaduv import UVLoop
8 | IOLoop.configure(UVLoop)
9 |
10 |
11 | def handle_signal(sig, frame):
12 | IOLoop.instance().add_callback(IOLoop.instance().stop)
13 |
14 |
15 | class EchoServer(TCPServer):
16 |
17 | def handle_stream(self, stream, address):
18 | self._stream = stream
19 | self._read_line()
20 |
21 | def _read_line(self):
22 | self._stream.read_until('\n', self._handle_read)
23 |
24 | def _handle_read(self, data):
25 | self._stream.write(data)
26 | self._read_line()
27 |
28 |
29 | if __name__ == '__main__':
30 | signal.signal(signal.SIGINT, handle_signal)
31 | signal.signal(signal.SIGTERM, handle_signal)
32 | server = EchoServer()
33 | server.listen(8889)
34 | IOLoop.instance().start()
35 | IOLoop.instance().close()
36 |
37 |
--------------------------------------------------------------------------------
/examples/hello-world.py:
--------------------------------------------------------------------------------
1 |
2 | import signal
3 |
4 | from tornado.ioloop import IOLoop
5 | from tornado.web import Application, RequestHandler
6 |
7 | from tornaduv import UVLoop
8 | IOLoop.configure(UVLoop)
9 |
10 |
11 | def handle_signal(sig, frame):
12 | loop = IOLoop.instance()
13 | loop.add_callback(loop.stop)
14 |
15 | class MainHandler(RequestHandler):
16 | def get(self):
17 | self.write("Hello, world")
18 |
19 | application = Application([
20 | (r"/", MainHandler),
21 | ])
22 |
23 |
24 | if __name__ == "__main__":
25 | signal.signal(signal.SIGINT, handle_signal)
26 | signal.signal(signal.SIGTERM, handle_signal)
27 | application.listen(8080)
28 | loop = IOLoop.instance()
29 | loop.start()
30 | loop.close()
31 |
32 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Tornado>=3.0
2 | pyuv>=1.0.0
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import re
4 | from distutils.core import setup
5 |
6 |
7 | def get_version():
8 | return re.search(r"""__version__\s+=\s+(?P['"])(?P.+?)(?P=quote)""", open('tornaduv/__init__.py').read()).group('version')
9 |
10 | setup(
11 | name = 'tornaduv',
12 | version = get_version(),
13 | url = 'https://github.com/saghul/tornaduv',
14 | author = 'Saúl Ibarra Corretgé',
15 | author_email = 'saghul@gmail.com',
16 | license = 'MIT License',
17 | description = 'Tornado IOLoop implementation with pyuv',
18 | long_description = open('README.rst', 'r').read(),
19 | packages = ['tornaduv'],
20 | install_requires = [i.strip() for i in open("requirements.txt").readlines() if i.strip()],
21 | classifiers=[
22 | 'Development Status :: 4 - Beta',
23 | 'Intended Audience :: Developers',
24 | 'License :: OSI Approved :: MIT License',
25 | 'Operating System :: POSIX',
26 | 'Operating System :: Microsoft :: Windows',
27 | 'Programming Language :: Python',
28 | 'Programming Language :: Python :: 2',
29 | 'Programming Language :: Python :: 2.7',
30 | 'Programming Language :: Python :: 3',
31 | 'Programming Language :: Python :: 3.3',
32 | 'Programming Language :: Python :: 3.4'
33 | ]
34 | )
35 |
--------------------------------------------------------------------------------
/tornaduv/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | import pyuv
3 |
4 | import datetime
5 | import errno
6 | import functools
7 | import logging
8 | import numbers
9 | import os
10 |
11 | try:
12 | import thread
13 | except ImportError:
14 | import _thread as thread # Python 3
15 |
16 | try:
17 | import signal
18 | except ImportError:
19 | signal = None
20 |
21 | from tornado import stack_context
22 | from tornado.ioloop import IOLoop
23 | from tornado.log import gen_log
24 | from tornado.platform.auto import Waker as FDWaker
25 |
26 | __all__ = ('UVLoop')
27 |
28 | __version__ = '0.4.0'
29 |
30 |
31 | class Waker(object):
32 | def __init__(self, loop):
33 | self._async = pyuv.Async(loop, lambda x: None)
34 |
35 | def wake(self):
36 | self._async.send()
37 |
38 |
39 | class UVLoop(IOLoop):
40 |
41 | def initialize(self, **kwargs):
42 | loop = kwargs.pop('loop', None)
43 | self._loop = loop or pyuv.Loop()
44 | self._handlers = {}
45 | self._callbacks = []
46 | self._callback_lock = thread.allocate_lock()
47 | self._timeouts = set()
48 | self._stopped = False
49 | self._running = False
50 | self._closing = False
51 | self._thread_ident = None
52 | self._cb_handle = pyuv.Prepare(self._loop)
53 | self._cb_handle.start(self._prepare_cb)
54 | self._waker = Waker(self._loop)
55 | self._fdwaker = FDWaker()
56 | self._signal_checker = pyuv.util.SignalChecker(
57 | self._loop, self._fdwaker.reader.fileno())
58 | super(UVLoop, self).initialize(**kwargs)
59 |
60 | def close(self, all_fds=False):
61 | with self._callback_lock:
62 | self._closing = True
63 | if all_fds:
64 | for fd in self._handlers:
65 | obj, _ = self._handlers[fd]
66 | if obj is not None and hasattr(obj, 'close'):
67 | try:
68 | obj.close()
69 | except Exception:
70 | gen_log.debug("error closing socket object %s", obj,
71 | exc_info=True)
72 | try:
73 | os.close(fd)
74 | except Exception:
75 | gen_log.debug("error closing fd %s", fd, exc_info=True)
76 |
77 | self._fdwaker.close()
78 | self._close_loop_handles()
79 | # Run the loop so the close callbacks are fired and memory is freed
80 | self._loop.run()
81 | self._loop = None
82 |
83 | def add_handler(self, fd, handler, events):
84 | obj = None
85 | if hasattr(self, 'split_fd'):
86 | fd, obj = self.split_fd(fd)
87 | if fd in self._handlers:
88 | raise IOError("fd %d already registered" % fd)
89 | poll = pyuv.Poll(self._loop, fd)
90 | poll.handler = stack_context.wrap(handler)
91 | self._handlers[fd] = (obj, poll)
92 | poll_events = 0
93 | if events & IOLoop.READ:
94 | poll_events |= pyuv.UV_READABLE
95 | if events & IOLoop.WRITE:
96 | poll_events |= pyuv.UV_WRITABLE
97 | poll.start(poll_events, self._handle_poll_events)
98 |
99 | def update_handler(self, fd, events):
100 | if hasattr(self, 'split_fd'):
101 | fd, _ = self.split_fd(fd)
102 | _, poll = self._handlers[fd]
103 | poll_events = 0
104 | if events & IOLoop.READ:
105 | poll_events |= pyuv.UV_READABLE
106 | if events & IOLoop.WRITE:
107 | poll_events |= pyuv.UV_WRITABLE
108 | poll.start(poll_events, self._handle_poll_events)
109 |
110 | def remove_handler(self, fd):
111 | if hasattr(self, 'split_fd'):
112 | fd, _ = self.split_fd(fd)
113 | data = self._handlers.pop(fd, None)
114 | if data is not None:
115 | _, poll = data
116 | poll.close()
117 | poll.handler = None
118 |
119 | def start(self):
120 | if self._running:
121 | raise RuntimeError('IOLoop is already running')
122 | if not logging.getLogger().handlers:
123 | # The IOLoop catches and logs exceptions, so it's
124 | # important that log output be visible. However, python's
125 | # default behavior for non-root loggers (prior to python
126 | # 3.2) is to print an unhelpful "no handlers could be
127 | # found" message rather than the actual log entry, so we
128 | # must explicitly configure logging if we've made it this
129 | # far without anything.
130 | logging.basicConfig()
131 | if self._stopped:
132 | self._stopped = False
133 | return
134 | old_current = getattr(IOLoop._current, "instance", None)
135 | IOLoop._current.instance = self
136 | self._thread_ident = thread.get_ident()
137 |
138 | # pyuv won't interate the loop if the poll is interrupted by
139 | # a signal, so make sure we can wake it up to catch signals
140 | # registered with the signal module
141 | #
142 | # If someone has already set a wakeup fd, we don't want to
143 | # disturb it. This is an issue for twisted, which does its
144 | # SIGCHILD processing in response to its own wakeup fd being
145 | # written to. As long as the wakeup fd is registered on the IOLoop,
146 | # the loop will still wake up and everything should work.
147 | old_wakeup_fd = None
148 | self._signal_checker.stop()
149 | if hasattr(signal, 'set_wakeup_fd') and os.name == 'posix':
150 | # requires python 2.6+, unix. set_wakeup_fd exists but crashes
151 | # the python process on windows.
152 | try:
153 | old_wakeup_fd = signal.set_wakeup_fd(self._fdwaker.writer.fileno())
154 | if old_wakeup_fd != -1:
155 | # Already set, restore previous value. This is a little racy,
156 | # but there's no clean get_wakeup_fd and in real use the
157 | # IOLoop is just started once at the beginning.
158 | signal.set_wakeup_fd(old_wakeup_fd)
159 | old_wakeup_fd = None
160 | else:
161 | self._signal_checker.start()
162 | except ValueError: # non-main thread
163 | pass
164 |
165 | self._running = True
166 | self._loop.run(pyuv.UV_RUN_DEFAULT)
167 |
168 | # reset the stopped flag so another start/stop pair can be issued
169 | self._running = False
170 | self._stopped = False
171 | IOLoop._current.instance = old_current
172 | if old_wakeup_fd is not None:
173 | signal.set_wakeup_fd(old_wakeup_fd)
174 |
175 | def stop(self):
176 | self._stopped = True
177 | self._loop.stop()
178 | self._waker.wake()
179 |
180 | def add_timeout(self, deadline, callback, *args, **kwargs):
181 | callback = stack_context.wrap(callback)
182 | if callable(callback):
183 | callback = functools.partial(callback, *args, **kwargs)
184 | timeout = _Timeout(deadline, callback, self)
185 | self._timeouts.add(timeout)
186 | return timeout
187 |
188 | def remove_timeout(self, timeout):
189 | self._timeouts.discard(timeout)
190 | if timeout._timer:
191 | timeout._timer.stop()
192 |
193 | def add_callback(self, callback, *args, **kwargs):
194 | with self._callback_lock:
195 | if self._closing:
196 | raise RuntimeError("IOLoop is closing")
197 | empty = not self._callbacks
198 | self._callbacks.append(functools.partial(stack_context.wrap(callback), *args, **kwargs))
199 | if empty or thread.get_ident() != self._thread_ident:
200 | self._waker.wake()
201 |
202 | def add_callback_from_signal(self, callback, *args, **kwargs):
203 | with stack_context.NullContext():
204 | if thread.get_ident() != self._thread_ident:
205 | # if the signal is handled on another thread, we can add
206 | # it normally (modulo the NullContext)
207 | self.add_callback(callback, *args, **kwargs)
208 | else:
209 | # If we're on the IOLoop's thread, we cannot use
210 | # the regular add_callback because it may deadlock on
211 | # _callback_lock. Blindly insert into self._callbacks.
212 | # This is safe because the GIL makes list.append atomic.
213 | # One subtlety is that if the signal interrupted the
214 | # _callback_lock block in IOLoop.start, we may modify
215 | # either the old or new version of self._callbacks,
216 | # but either way will work.
217 | self._callbacks.append(functools.partial(stack_context.wrap(callback), *args, **kwargs))
218 | self._waker.wake()
219 |
220 | def _handle_poll_events(self, handle, poll_events, error):
221 | events = 0
222 | if error is not None:
223 | # Some error was detected, signal readability and writability so that the
224 | # handler gets and handles the error
225 | events |= IOLoop.READ | IOLoop.WRITE
226 | else:
227 | if poll_events & pyuv.UV_READABLE:
228 | events |= IOLoop.READ
229 | if poll_events & pyuv.UV_WRITABLE:
230 | events |= IOLoop.WRITE
231 | fd = handle.fileno()
232 | try:
233 | obj, poll = self._handlers[fd]
234 | callback_fd = fd
235 | if obj is not None and hasattr(obj, 'fileno'):
236 | # socket object was passed to add_handler,
237 | # return it to the callback
238 | callback_fd = obj
239 |
240 | poll.handler(callback_fd, events)
241 | except (OSError, IOError) as e:
242 | if e.args[0] == errno.EPIPE:
243 | # Happens when the client closes the connection
244 | pass
245 | else:
246 | logging.error("Exception in I/O handler for fd %s", fd, exc_info=True)
247 | except Exception:
248 | logging.error("Exception in I/O handler for fd %s", fd, exc_info=True)
249 |
250 | def _prepare_cb(self, handle):
251 | with self._callback_lock:
252 | callbacks = self._callbacks
253 | self._callbacks = []
254 | for callback in callbacks:
255 | self._run_callback(callback)
256 |
257 | def _close_loop_handles(self):
258 | for handle in self._loop.handles:
259 | if not handle.closed:
260 | handle.close()
261 |
262 |
263 | class _Timeout(object):
264 | """An IOLoop timeout, a UNIX timestamp and a callback"""
265 |
266 | __slots__ = ['deadline', 'callback', '_timer']
267 |
268 | def __init__(self, deadline, callback, io_loop):
269 | now = io_loop.time()
270 | if isinstance(deadline, numbers.Real):
271 | self.deadline = deadline
272 | elif isinstance(deadline, datetime.timedelta):
273 | self.deadline = now + _Timeout.timedelta_to_seconds(deadline)
274 | else:
275 | raise TypeError("Unsupported deadline %r" % deadline)
276 | self.callback = callback
277 | timeout = max(self.deadline - now, 0)
278 | self._timer = pyuv.Timer(io_loop._loop)
279 | self._timer.start(self._timer_cb, timeout, 0.0)
280 |
281 | def _timer_cb(self, handle):
282 | self._timer.close()
283 | self._timer = None
284 | io_loop = IOLoop.current()
285 | io_loop._timeouts.remove(self)
286 | io_loop._run_callback(self.callback)
287 |
288 | @staticmethod
289 | def timedelta_to_seconds(td):
290 | """Equivalent to td.total_seconds() (introduced in python 2.7)."""
291 | return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / float(10 ** 6)
292 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 |
2 | [tox]
3 | envlist = py27,py33,py34,py35,py36,py27-deps,py33-deps,py34-deps,py35-deps,py36-deps,py27-t3,py33-t3,py34-t3
4 |
5 |
6 | [testenv]
7 | passenv = TRAVIS
8 | setenv =
9 | PYTHONHASHSEED = random
10 | deps =
11 | tornado
12 | pyuv
13 | commands = python -X faulthandler -m tornado.test.runtests --ioloop='tornaduv.UVLoop' {posargs}
14 |
15 |
16 | [testenv:py27]
17 | commands = python -m tornado.test.runtests --ioloop='tornaduv.UVLoop' {posargs}
18 |
19 |
20 | [testenv:py27-deps]
21 | deps =
22 | {[testenv]deps}
23 | mock
24 | pycurl
25 | futures
26 | # pycares
27 | commands = python -m tornado.test.runtests --ioloop='tornaduv.UVLoop' {posargs}
28 |
29 |
30 | [testenv:py33-deps]
31 | deps =
32 | {[testenv]deps}
33 | pycurl
34 | # pycares
35 |
36 |
37 |
38 | [testenv:py34-deps]
39 | deps =
40 | {[testenv]deps}
41 | pycurl
42 | # pycares
43 |
44 |
45 | [testenv:py35-deps]
46 | deps =
47 | {[testenv]deps}
48 | pycurl
49 | # pycares
50 |
51 |
52 | [testenv:py36-deps]
53 | deps =
54 | {[testenv]deps}
55 | pycurl
56 | # pycares
57 |
58 |
59 | [testenv:py27-t3]
60 | deps =
61 | tornado>=3.0,<4.0
62 | pyuv
63 | commands = python -m tornado.test.runtests --ioloop='tornaduv.UVLoop' {posargs}
64 |
65 |
66 | [testenv:py33-t3]
67 | deps =
68 | tornado>=3.0,<4.0
69 | pyuv
70 |
71 |
72 | [testenv:py34-t3]
73 | deps =
74 | tornado>=3.0,<4.0
75 | pyuv
76 |
--------------------------------------------------------------------------------