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
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/effectse vuelve dependencia. Sinsubscribemanual. - Perezoso y cacheado: un
computedsolo 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 (tareaasyncioo hilo), nunca heredado. - Sin fugas: los
computedabandonados se recolectan (refs fuente→observador débiles); loseffectviven hasta sudispose(). - ~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ó sudispose, un nodo a medio suscribir sería imposible de liberar. - Un
on_cleanupque 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 undispose(), 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
computedque lanza mientras lo evalúa un effect se resetea aclean; una lectura directa de esecomputed—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. (Uncomputedleído directo, sineffectde 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 (fnde 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). batchdifiere 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: unawaitdentro de unbatch()difiere los effects de esa tarea hasta salir; las demás tareas no se ven afectadas.) Con frameworks async sin tareasasyncio(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
RuntimeErrorcon 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 comoRecursionErrorantes de alcanzar la detección.) - Memoria: un
effectnunca dispuesto vive (y retiene su clausura) para siempre — guarda el handle dedisposeo créalo dentro de unroot(). Loscomputedabandonados 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, pasaequals=propio oequals=False.
Licencia
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
51858df7f8365e1a873ebbbf6af8bdf45202cb2e6364ad7fe1d224e8629b967c
|
|
| MD5 |
9a73dd72a0aa0eee204d5d72f42f3dd6
|
|
| BLAKE2b-256 |
f3f8db324ffcdbc3383bfabcaabfb3cec819732af6871f8ed31f104eec81c3a1
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
97c84e9abae30664a2999dfc16526bab5b0445b436aea3a23e9648829a9ee56a
|
|
| MD5 |
d78b14e7521bf737adbbca6801dc5dd1
|
|
| BLAKE2b-256 |
a29a35136b70891648f08ed6f87dfe3cd810237cbd33d13bec514ca3ef49b6bd
|