Crochet

repository·master·Indexed 42 days ago

https://github.com/itamarst/crochet

A library that enables the use of Twisted's asynchronous reactor from regular blocking Python code. It bridges the gap between asynchronous Twisted implementations and blocking frameworks like Django, Flask, or standard threaded applications by providing a blocking interface to Twisted operations via decorators like @wait_for and @run_in_reactor.

Tokens
5K
Snippets
21
Records
30
Agent score
54%

What's inside Crochet

  1. Use Crochet to integrate Twisted with blocking applications

    master

    Crochet allows blocking and threaded applications (such as Flask or Django) to utilize the Twisted networking framework without manually managing the Twisted reactor or event loop. It provides a completely blocking interface to Twisted operations.

    # Example usage pattern (based on blockingdns.py)
    # This allows you to perform Twisted-based network operations
    # in a blocking manner.
    import crochet
    crochet.setup()
    
    # After setup, you can use Twisted components directly
    # without calling reactor.run()
  2. How to structure code when using Crochet to hide Twisted details

    master

    When integrating event-driven Twisted code into synchronous application code, you should maintain three distinct layers to avoid leaking Twisted or Crochet implementation details (like EventualResult objects) into your main application logic:

    1. Twisted code layer: Contains the actual Twisted-based logic. This code must only be called within the reactor thread.
    2. Crochet wrapper layer: Uses @wait_for or @run_in_reactor to bridge threads. This layer executes in the reactor thread, but its caller resides in the application thread. This layer is responsible for converting EventualResult objects into standard synchronous return values and wrapping any Twisted-specific objects returned by the lower layer.
    3. Application code layer: The high-level logic that runs in the application thread(s). It should interact with a normal, synchronous API and remain unaware of Twisted or Crochet.

    By using this middle wrapper layer, your application code remains clean and doesn't need to handle asynchronous primitives or thread-safety issues related to Twisted objects.

    # Example pattern described in the documentation
    
    # 1. Twisted code (Reactor thread only)
    class _ExchangeRate:
        def get_rate(self):
            # Twisted logic here
            pass
    
    # 2. Crochet wrapper (Hides Crochet/Twisted from App)
    class ExchangeRate:
        def __init__(self):
            self._twisted_exchange = _ExchangeRate()
    
        @crochet.wait_for
        def get_rate(self):
            return self._twisted_exchange.get_rate()
    
    # 3. Application code (Synchronous/Blocking)
    exchange = ExchangeRate()
    rate = exchange.get_rate()  # Looks like a normal blocking call
  3. Understand the difference between Crochet and async/await

    master

    While Python's async/await syntax provides an illusion of blocking, it requires the async keyword to percolate through your entire call stack. You can only use await inside functions marked as async.

    In contrast, Crochet allows for true blocking integration. You can block on an asynchronous event using a standard, normal Python function call without needing to mark the function or its callers as async. This allows you to restrict asynchronous behavior to specific parts of your program without changing the signature of your entire codebase.

  4. Configure Crochet for uWSGI, multiprocessing, or Celery

    master

    Because uWSGI, multiprocessing, and Celery use fork() by default on Unix, they clone the parent process in a way that breaks Crochet. You can resolve this using one of two strategies:

    • uWSGI: Use the --lazy-apps command-line option.
    • multiprocessing: Use the spawn or forkserver start methods (available in Python 3).
    • Celery: (N/A - see Strategy 2)

    Strategy 2: Defer crochet.setup() to the child process

    If you cannot avoid fork(), ensure crochet.setup() is only called after the process has branched:

    • uWSGI: Call crochet.setup() inside the WSGI application function.
    • multiprocessing: Call crochet.setup() only inside the child process.
    • Celery: Call crochet.setup() only inside your tasks.
  5. Initialize Crochet with setup()

    master

    To use Crochet, you must first start the Twisted reactor in a background thread managed by Crochet. Call crochet.setup() to perform this initialization.

    Since Crochet is designed to be used as a library, it is safe to call setup() multiple times; only the first call will actually perform the setup, and subsequent calls will be ignored. This allows multiple libraries to call crochet.setup() without conflict.

    from crochet import setup
    setup()
  6. Unit test decorated Twisted functions

    master

    When testing code decorated with @wait_for or @run_in_reactor, you can bypass the Crochet layer to test the underlying Twisted logic directly. The decorated function exposes the original, undecorated function via the __wrapped__ attribute.

    from crochet import wait_for
    
    @wait_for
    def add(a, b):
        # Twisted logic here
        return a + b
    
    # In your test:
    # This returns an EventualResult (requires crochet setup)
    res = add(1, 2)
    
    # This calls the underlying function directly (no crochet needed)
    val = add.__wrapped__(1, 2)
    assert val == 3
  7. Minimize the use of @wait_for and @run_in_reactor

    master

    To improve testability and maintainability, keep the code inside @wait_for and @run_in_reactor wrappers to a minimum.

    Because this code straddles two different threads (the application thread and the reactor thread), it is inherently more difficult to unit test. The recommended pattern is to push the core Twisted logic into a low-level layer and use a separate higher-level layer to hide those Twisted details from your application code. This separation makes the core logic easier to test in isolation.

  8. Configure mypy to use the Crochet plugin

    master

    Crochet provides type hints for Python 3.6+, but because of limitations in how Callable generics are constructed, the arguments of functions decorated with @run_in_reactor cannot be automatically checked. To enable proper type checking for these decorated functions, you must install the crochet.mypy plugin in your mypy configuration (e.g., in mypy.ini).

    [mypy]
    plugins = crochet.mypy
  9. Use Crochet in applications that run their own Twisted reactor

    master

    If your application is already running the Twisted reactor (for example, using Twisted as a WSGI container), Crochet's default behavior of starting a new reactor in a thread will cause conflicts.

    To prevent Crochet from starting its own reactor, call crochet.no_setup() before importing any libraries that might call crochet.setup().

    from crochet import no_setup
    no_setup()
    
    # Now import libraries that use crochet internally
    import blocking_library
    
    from twisted.internet import reactor
    # ... setup app ...
    reactor.run()
  10. Handle Deferred return types in @run_in_reactor functions

    master

    Because Twisted's Deferred does not yet support generic type arguments for its eventual result, the type checker cannot automatically determine the return type of a @run_in_reactor function that returns a Deferred.

    To resolve this, you must manually cast the Deferred to the expected eventual result type using typing.cast. This ensures that the EventualResult returned by the function is correctly typed for the caller.

    import typing
    from twisted.internet import reactor, Deferred
    from crochet import run_in_reactor
    
    @run_in_reactor
    def get_time_in_x_seconds(delay: float) -> float:
        def get_time() -> float:
            return reactor.seconds()  # type: ignore
    
        if delay < 0.001:
            return get_time()
        else:
            d = Deferred()
    
            def complete():
                d.callback(get_time())
    
            reactor.callLater(delay, complete)  # type: ignore
            # Cast the Deferred to the expected eventual result type
            return typing.cast(float, d)
    
    # The client code now correctly sees the return type of .wait() as float
    t1: float = get_time_in_x_seconds(2).wait(3)