Skip to main content

Thread-safe, zero-dependency fine-grained reactivity for Python: signal / computed / effect with glitch-free pull propagation, resilient error recovery, and per-task/thread scoping.

Project description

nervio

CI

Reactividad de grano fino para Python, sin dependencias y segura entre hilos. signal / computed / effect con tracking automático de dependencias, propagación pull glitch-free (estados clean/check/dirty, en la línea de SolidJS y Reactively), recuperación de errores auditada y ámbitos por tarea asyncio/hilo.

  • Tracking automático: lo que leas dentro de un computed/effect se vuelve dependencia. Sin subscribe manual.
  • Perezoso y cacheado: un computed solo se recalcula cuando lo lees y una dependencia real cambió.
  • Glitch-free: en diamantes (A→B, A→C, B+C→D), D se recalcula una vez y nunca ve estados intermedios inconsistentes.
  • Seguro entre hilos: un mismo grafo puede compartirse entre hilos (lock global reentrante; update() atómico), y el estado de scheduling va por ejecutor (tarea asyncio o hilo), nunca heredado.
  • Sin fugas: los computed abandonados se recolectan (refs fuente→observador débiles); los effect viven hasta su dispose().
  • ~580 líneas, un solo módulo, tipado (PEP 561).
pip install nervio

En 30 segundos

from nervio import signal, computed, effect, batch

precio   = signal(100)
cantidad = signal(2)
total    = computed(lambda: precio.value * cantidad.value)   # perezoso

effect(lambda: print(f"Total: {total.value}"))   # imprime "Total: 200" ya mismo

precio.value = 150          # -> "Total: 300"
with batch():               # varias escrituras, un solo recálculo
    precio.value = 200
    cantidad.value = 3      # -> "Total: 600" (una vez)

API

signal(v, *, equals=None) Estado reactivo. .value (lee y suscribe), .value = x / .set(x) / .update(fn) (escribe), .peek() (lee sin suscribir). equals=False propaga siempre (útil para objetos mutados in-place).
computed(fn, *, equals=None) Valor derivado perezoso y cacheado. .value / .peek(). Sin referencias (tuyas ni de nodos aguas abajo) se recolecta solo: no hace falta disponerlo.
effect(fn) -> dispose Corre fn ahora y ante cada cambio de sus dependencias. Queda anclado internamente: sigue corriendo aunque descartes el handle; solo dispose() (o el del root que lo contiene) lo libera.
batch() Context manager: agrupa escrituras; los effects corren una sola vez al salir.
untrack(fn) Ejecuta fn leyendo señales sin registrarlas como dependencias.
on_cleanup(fn) Dentro de un effect/root: registra limpieza que corre antes del próximo re-run y al disponer.
root() -> dispose Context manager que agrupa effects/computeds creados dentro para disponerlos juntos.

Dependencias dinámicas, limpieza, alcance

from nervio import signal, effect, on_cleanup, root

modo = signal("a"); a = signal(1); b = signal(2)

# Las dependencias se recalculan en cada corrida: aquí se suscribe a `a` O a `b`, nunca a ambas.
effect(lambda: print(a.value if modo.value == "a" else b.value))

# Limpieza por corrida (cerrar sockets, cancelar timers, etc.)
def watcher():
    conn = abrir(modo.value)
    on_cleanup(conn.close)        # corre antes del próximo run y al disponer
effect(watcher)

# Disponer un subgrafo completo de una vez
with root() as dispose:
    effect(lambda: ...)
    effect(lambda: ...)
# ...más tarde:
dispose()

Cómo funciona

Escribir una señal solo marca el subgrafo (fase barata: observadores directos → dirty, el resto → check). Nada se recalcula ahí. El recálculo ocurre al leer un computed o al vaciar la cola de effects: un nodo en check pregunta a sus fuentes y solo se recalcula si alguna resultó dirty; si ninguna cambió, vuelve a clean sin trabajo. Eso da el mínimo de recálculos y consistencia sin glitches dentro de cada ejecutor. Todo el estado vivo —observador, dueño, cola de effects y profundidad de batch— vive en un ámbito por ejecutor (la tarea asyncio actual o, en su defecto, el hilo), que nunca se hereda: una tarea creada con create_task dentro de un batch o de un effect estrena ámbito limpio (nada de batches heredados que nunca cierran ni suscripciones fantasma). Las mutaciones del grafo (escrituras, tracking, recomputos, flush, disposición) se serializan con un lock global reentrante, así varios hilos pueden compartir un mismo grafo sin corromperlo. Las referencias tienen dirección: dependiente→fuente es fuerte (lo que un effect vivo necesita no puede morir) y fuente→observador es débil con compactación perezosa (un computed abandonado se recolecta solo); los effects se anclan en un registro interno hasta su dispose().

Errores

Si un effect —o un computed del que depende— lanza una excepción, el error se propaga a quien disparó el recálculo (la escritura .value / .set, o la salida del batch), pero el grafo no queda corrupto: el effect se recupera y vuelve a ejecutarse en el siguiente cambio de una dependencia, y las dependencias registradas antes del fallo se conservan (ninguna escritura futura queda muda). Un effect que falla tampoco impide que corran los demás de la misma cola. Esto vale también para BaseException (KeyboardInterrupt, CancelledError): se re-lanzan de inmediato, pero el effect no muere.

  • Si la primera corrida de un effect(fn) lanza, el effect no queda registrado (se libera y la excepción se propaga): como el llamador aún no recibió su dispose, un nodo a medio suscribir sería imposible de liberar.
  • Un on_cleanup que lanza tampoco envenena: corren todos los cleanups del run (el roto no frena a los demás ni queda re-registrado), el primer error se propaga una sola vez, y el effect se recupera en el siguiente cambio. Si ocurre durante un dispose(), el desmontaje se completa igual (nada queda a medio disponer) y el error se re-lanza al final.
  • Si varios effects fallan en el mismo flush, se re-lanza el primero; los demás corren igual pero sus errores no se reportan. En multihilo, el error aflora en el hilo escritor que disparó el flush, aunque el effect lo haya creado otro hilo.
  • Trade-off: un computed que lanza mientras lo evalúa un effect se resetea a clean; una lectura directa de ese computed —en la ventana entre el fallo y el siguiente cambio— devuelve su último valor bueno en vez de reintentar. Tras cualquier cambio de una fuente recalcula fresco. (Un computed leído directo, sin effect de por medio, conserva el reintento en cada lectura.)

Límites (a propósito)

  • Hilos: seguro, pero serializado. Un mismo grafo puede mutarse desde varios hilos: un lock global reentrante serializa escrituras, tracking, recomputos y disposición (update() es leer-modificar-escribir atómico: sin updates perdidos). El costo: el lock se sostiene mientras corre tu código (fn de effects/computeds, equals, cleanups) — no bloquees ahí esperando a otro hilo que también use señales (deadlock clásico), ni hagas I/O lenta (frena el grafo entero). peek() de señales es lock-free (puede ver el valor inmediatamente anterior a una escritura en curso).
  • batch difiere effects; no es una transacción con aislamiento. Otros hilos pueden leer los valores intermedios entre las escrituras de un batch ajeno (la garantía glitch-free es por-ejecutor, no inter-hilo). Un effect ya ensuciado por un batch abierto corre cuando ese batch cierre, aunque otro hilo vuelva a escribir sus señales: mantén los batch cortos y síncronos. Si el ejecutor dueño muere con el batch abierto (hilo o tarea abandonados), sus effects pendientes los adopta la siguiente escritura — no se pierden.
  • El batch pertenece al ejecutor exacto que lo abre. Las tareas creadas dentro no lo heredan, y el código corrido en otras tareas del mismo hilo —p.ej. dentro de un asyncio.run(...)— tampoco lo ve. (Async: un await dentro de un batch() difiere los effects de esa tarea hasta salir; las demás tareas no se ven afectadas.) Con frameworks async sin tareas asyncio (trio, curio), el ámbito es el hilo: todas las tareas de ese hilo comparten batch y cola.
  • Profundidad sin límite en grafo caliente: marcado, resolución y recuperación son iterativos — cadenas de miles de computeds funcionan para escrituras y actualizaciones. Solo la primera evaluación de una cadena fría anida llamadas de usuario y queda limitada por el stack de Python (~200 niveles): caliéntala leyendo los computeds conforme la construyes.
  • Ciclos: detectados y rechazados. Un computed/effect que participa (directa o indirectamente) en su propio recálculo lanza RuntimeError con mensaje claro; los ciclos por ping-pong de effects que se escriben señales entre sí los corta un tope duro de re-cálculos. No diseñes ciclos: el error existe para encontrarlos, no es una semántica. (En cadenas frías muy profundas un ciclo puede aflorar como RecursionError antes de alcanzar la detección.)
  • Memoria: un effect nunca dispuesto vive (y retiene su clausura) para siempre — guarda el handle de dispose o créalo dentro de un root(). Los computed abandonados se recolectan solos. (En intérpretes sin recolección por conteo de referencias —PyPy— la liberación puede diferirse hasta el siguiente ciclo de GC; las refs muertas se compactan solas en la siguiente escritura.)
  • Para arrays de numpy u objetos con == no booleano, pasa equals= propio o equals=False.

Licencia

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nervio-0.2.0.tar.gz (21.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nervio-0.2.0-py3-none-any.whl (14.0 kB view details)

Uploaded Python 3

File details

Details for the file nervio-0.2.0.tar.gz.

File metadata

  • Download URL: nervio-0.2.0.tar.gz
  • Upload date:
  • Size: 21.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for nervio-0.2.0.tar.gz
Algorithm Hash digest
SHA256 51858df7f8365e1a873ebbbf6af8bdf45202cb2e6364ad7fe1d224e8629b967c
MD5 9a73dd72a0aa0eee204d5d72f42f3dd6
BLAKE2b-256 f3f8db324ffcdbc3383bfabcaabfb3cec819732af6871f8ed31f104eec81c3a1

See more details on using hashes here.

File details

Details for the file nervio-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: nervio-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 14.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for nervio-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 97c84e9abae30664a2999dfc16526bab5b0445b436aea3a23e9648829a9ee56a
MD5 d78b14e7521bf737adbbca6801dc5dd1
BLAKE2b-256 a29a35136b70891648f08ed6f87dfe3cd810237cbd33d13bec514ca3ef49b6bd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page