Pyjo.Reactor.Twisted - Low-level event reactor with Twisted support

from Pyjo.Reactor.Twisted import Twisted

# Watch if handle becomes readable or writable
reactor = Twisted()
reactor.io(first, lambda reactor, writable: print(
    'First handle is writable' if writable else 'First handle is readable'
))

# Or import the module, as in Google Python Style Guide

import Pyjo.Reactor.Twisted

# Watch if handle becomes readable or writable
reactor = Pyjo.Reactor.Twisted.new()
reactor.io(first, lambda reactor, writable: print(
    'First handle is writable' if writable else 'First handle is readable'
))

# Change to watching only if handle becomes writable
reactor.watch(first, False, True)

# Watch a file descriptor if it becomes readable
reactor.io(fd, lambda reactor, writable: print(
    'Second handle is writable' if writable else 'Second handle is readable'
)).watch(fd, True, False)

# Add a timer
def timeout(reactor):
    reactor.remove(first)
    reactor.remove(fd)
    print('Timeout!')

reactor.timer(15, timeout)

# Start reactor if necessary
if not reactor.is_running:
    reactor.start()

Pyjo.Reactor.Twisted is a low-level event reactor based on the reactor of Twisted, the one that the application installed before, or else the reactor of poll of Twisted, or of select where there is no poll or it does not work well, as on macOS. Its default on Linux, the reactor of epoll, is not installed, as epoll keeps its state in the kernel: a socket that is closed before it is removed leaves it, which Pyjoyment does, and the state is shared with the child of a fork, as a worker of Pyjo.Server.Prefork, so an application that installs it has neither. An application of Twisted that imports twisted.internet.reactor itself calls install() before, which installs the one of poll or select. PYJO_REACTOR selects it for Pyjo.IOLoop:

PYJO_REACTOR=Pyjo.Reactor.Twisted

The reactor of Twisted is unique in a process, so only one object of this class uses it at a time, and new() returns a Pyjo.Reactor.Poll object while it lives, as the constructor of the class does. It runs in the thread that made it, so the threads of Pyjo.Server.Prethread get Pyjo.Reactor.Poll objects. When the reactor of Twisted is running already, as under twistd or Scrapy, start() and one_tick() return right away, and the events are handled by the reactor of Twisted that runs.

Events

Pyjo.Reactor.Twisted inherits all events from Pyjo.Reactor.Poll.

See also

Pyjo.Reactor.Poll, Pyjoyment, https://github.com/pyjoyment/Pyjoyment.

Classes

class Pyjo.Reactor.Twisted.Pyjo_Reactor_Twisted(*args: Any, **kwargs: Any)[source]

Pyjo.Reactor.Twisted inherits all methods from Pyjo.Reactor.Poll and implements the following new ones.

again(id: str, after: float | None = None) → None[source]
reactor.again(id)
reactor.again(id, 0.5)

Restart timer and optionally change the invocation time. Note that this method requires an active timer.

io(handle: Handle, cb: IOCallback) → Self[source]
reactor = reactor.io(handle, cb)

Watch handle for I/O events, invoking the callback whenever handle becomes readable or writable.

classmethod new(*args: Any, **kwargs: Any) → Any[source]
reactor = Pyjo.Reactor.Twisted.new()

Construct a new Pyjo.Reactor.Twisted object, or a Pyjo.Reactor.Poll object while another one uses the reactor of Twisted.

one_tick() → None[source]
reactor.one_tick()

Run reactor until an event occurs or no events are being watched anymore, or return right away while the reactor of Twisted runs.

recurring(after: float, cb: TimerCallback) → str[source]
id = reactor.recurring(0.25, cb)

Create a new recurring timer, invoking the callback repeatedly after a given amount of time in seconds.

remove(remove: Handle | str) → bool[source]
boolean = reactor.remove(handle)
boolean = reactor.remove(id)

Remove handle or timer.

reset() → None[source]
reactor.reset()

Remove all handles and timers.

start() → None[source]
reactor.start()

Start watching for I/O and timer events, this will block until stop() is called or no events are being watched anymore, or return right away while the reactor of Twisted runs.

timer(after: float, cb: TimerCallback) → str[source]
id = reactor.timer(0.5, cb)

Create a new timer, invoking the callback after a given amount of time in seconds.

watch(handle: Handle, read: bool, write: bool) → Self[source]
reactor = reactor.watch(handle, readable, writable)

Change I/O events to watch handle for with true and false values. Note that this method requires an active I/O watcher.

Pyjo.Reactor.Twisted.install() → None[source]
Pyjo.Reactor.Twisted.install()

Install the reactor of Twisted that Pyjo.Reactor.Twisted prefers, of poll, or of select where there is no poll or it does not work well, as on macOS, in the place of the default of Twisted, epoll on Linux. An application of Twisted calls it before it imports twisted.internet.reactor, which installs the default. It does nothing where a reactor of Twisted is installed already.

import Pyjo.Reactor.Twisted

Pyjo.Reactor.Twisted.install()

from twisted.internet import reactor

Examples

examples/hello_world_twisted.py runs Pyjo.HelloWorld on the server of Pyjoyment in the event loop that Twisted runs, with reactor.run(), in a with block, which closes the listen sockets of the server at its end:

import os


# The reactor of Pyjoyment on the reactor of Twisted, selected before Pyjo.IOLoop is loaded
os.environ['PYJO_REACTOR'] = 'Pyjo.Reactor.Twisted'

import Pyjo.Reactor.Twisted  # noqa: E402


# The reactor of Twisted of poll or select, before Twisted is imported, which would install its default, epoll on Linux
Pyjo.Reactor.Twisted.install()

import Pyjo.HelloWorld  # noqa: E402
import Pyjo.Server.Daemon  # noqa: E402

from twisted.internet import reactor  # noqa: E402


# The server of Pyjoyment, which closes its listen sockets at the end of the block
with Pyjo.Server.Daemon.new(app=Pyjo.HelloWorld.new(), listen=['http://*:3000']) as daemon:
    daemon.start()

    # Twisted runs the event loop, with the server of Pyjoyment in it
    reactor.run()
$ python examples/hello_world_twisted.py
Web application available at http://127.0.0.1:3000
$ curl http://127.0.0.1:3000/
Your Pyjo is working!

examples/user_agent_twisted.py is an application of Twisted, a server of twisted.web, whose timer requests it every second with the user agent of Pyjoyment, without blocking the reactor of Twisted. The user agent is in a with block, which closes its connections at its end, so the callback shows the error of a request that it interrupts:

import os


# The reactor of Pyjoyment on the reactor of Twisted, selected before Pyjo.IOLoop is loaded
os.environ['PYJO_REACTOR'] = 'Pyjo.Reactor.Twisted'

import Pyjo.Reactor.Twisted  # noqa: E402


# The reactor of Twisted of poll or select, before Twisted is imported, which would install its default, epoll on Linux
Pyjo.Reactor.Twisted.install()

import Pyjo.UserAgent  # noqa: E402

from twisted.internet import reactor, task  # noqa: E402
from twisted.web import resource, server  # noqa: E402


# An application of Twisted
class Hello(resource.Resource):
    isLeaf = True

    def render_GET(self, request):
        return b'Hello from Twisted!'


reactor.listenTCP(3000, server.Site(Hello()))

# The user agent of Pyjoyment, which closes its connections at the end of the block
with Pyjo.UserAgent.new() as ua:
    # A request every second, which does not block the reactor of Twisted; a request that the end of the block
    # interrupts gets an error
    def fetch():
        def done(ua, tx):
            print(tx.error['message'] if tx.error else f'{tx.res.code} {tx.res.text}')

        ua.get('http://127.0.0.1:3000/', done)

    task.LoopingCall(fetch).start(1)
    reactor.run()
$ python examples/user_agent_twisted.py
200 Hello from Twisted!
200 Hello from Twisted!
200 Hello from Twisted!

examples/loopback_twisted.py is a server of Pyjoyment and its user agent on the reactor of Twisted, a blocking request, which runs the event loop until the response, and the class of the reactor:

import os


# The reactor of Pyjoyment on the reactor of Twisted, selected before Pyjo.IOLoop is loaded
os.environ['PYJO_REACTOR'] = 'Pyjo.Reactor.Twisted'

import Pyjo.HelloWorld  # noqa: E402
import Pyjo.IOLoop  # noqa: E402
import Pyjo.Server.Daemon  # noqa: E402
import Pyjo.UserAgent  # noqa: E402


# The server, on a port that the system picks, and the user agent, in the same event loop, which close their sockets at
# the end of the block
with (
    Pyjo.Server.Daemon.new(app=Pyjo.HelloWorld.new(), listen=['http://127.0.0.1'], silent=True) as daemon,
    Pyjo.UserAgent.new(ioloop=Pyjo.IOLoop.singleton) as ua,
):
    # A blocking request, which runs the event loop of the server on the reactor of Twisted until the response
    tx = ua.get(f'http://127.0.0.1:{daemon.start().ports()[0]}/')
    print(type(Pyjo.IOLoop.singleton.reactor).__name__)
    print(tx.result.code, tx.result.text)
$ python examples/loopback_twisted.py
Pyjo_Reactor_Twisted
200 Your Pyjo is working!

The first two call install() before they import Twisted, which would install its default, epoll on Linux, in the place of poll or select; the third does not import Twisted, so the reactor installs it.