Skip to main content

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.

Metadata

Release files for nervio 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nervio 0.2.0
File Size Uploaded
nervio-0.2.0.tar.gz 21.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nervio 0.2.0
File Interpreter ABI Platform
nervio-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 35.0 kB

Release files / nervio-0.2.0.tar.gz

Download URL nervio-0.2.0.tar.gz
Size 21.0 kB
Tags Source
SHA-256 checksum
How to use checksums
51858df7f8365e1a873ebbbf6af8bdf45202cb2e6364ad7fe1d224e8629b967c
BLAKE2b-256 checksum
How to use checksums
f3f8db324ffcdbc3383bfabcaabfb3cec819732af6871f8ed31f104eec81c3a1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.2

Release files / nervio-0.2.0-py3-none-any.whl

Download URL nervio-0.2.0-py3-none-any.whl
Size 14.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
97c84e9abae30664a2999dfc16526bab5b0445b436aea3a23e9648829a9ee56a
BLAKE2b-256 checksum
How to use checksums
a29a35136b70891648f08ed6f87dfe3cd810237cbd33d13bec514ca3ef49b6bd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.2

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page