Reactive programming for Python with reactive variables and events.
spellbind is a reactive programming library that lets you create Variables that automatically update when their dependencies change, plus an event system for notifying observers.
pip install spellbindfromspellbind.int_valuesimportIntVariablefromspellbind.str_valuesimportStrVariable# Create reactive variablesname=StrVariable("Alice")
age=IntVariable(25)
# Create computed values that automatically updategreeting=name+" is "+age.to_str() +" years old"print(greeting) # "Alice is 25 years old"# Update source values - computed values update automatically!name.value="Bob"age.value=30print(greeting) # "Bob is 30 years old"The foundation of spellbind consists of three key components:
Values are read-only reactive data that can be observed for changes. Variables are mutable Values that can be changed and bound to other Values. Events provide a way to notify observers when something happens.
fromspellbind.valuesimportConstantfromspellbind.int_valuesimportIntVariablefromspellbind.eventimportEvent# Variables can be changedcounter=IntVariable(0)
counter.value=10# Constants cannot be changedpi=Constant(3.14159)
# Events notify observersbutton_clicked=Event()
button_clicked.observe(lambda: print("Clicked!"))
button_clicked() # Prints: "Clicked!"Variables can be bound to other Values, making them automatically update:
fromspellbind.int_valuesimportIntVariable# Create computed valuesbase=IntVariable(10)
multiplier=IntVariable(3)
result=base*multiplier# Bind variables to computed valuesmy_variable=IntVariable(0)
my_variable.bind(result)
print(my_variable) # 30# Updates propagate automaticallybase.value=20print(my_variable) # 60# Unbind to break connectionsmy_variable.unbind()Control memory management with binding strength:
fromspellbind.str_valuesimportStrVariablesource=StrVariable("hello")
target=StrVariable("")
# Strong binding (default) - keeps source alivetarget.bind(source, bind_weakly=False)
# Weak binding - allows source to be garbage collectedtarget.bind(source, bind_weakly=True)spellbind automatically prevents circular dependencies:
fromspellbind.int_valuesimportIntVariablea=IntVariable(1)
b=IntVariable(2)
a.bind(b)
# b.bind_to(a) # This would raise RecursionErrorReact to value changes with observers:
fromspellbind.int_valuesimportIntVariabledefon_value_change(new_value):
print(f"Value changed to: {new_value}")
my_var=IntVariable(42)
my_var.observe(on_value_change)
my_var.value=100# Prints: "Value changed to: 100"spellbind includes an event system for notifying observers when things happen.
fromspellbind.eventimportEvent# Create an eventbutton_clicked=Event()
# Add observersdefhandle_click():
print("Button was clicked!")
button_clicked.observe(handle_click)
# Trigger the eventbutton_clicked() # Prints: "Button was clicked!"Events that pass data to observers:
fromspellbind.eventimportValueEventuser_logged_in=ValueEvent[str]()
defwelcome_user(username: str):
print(f"Welcome, {username}!")
user_logged_in.observe(welcome_user)
user_logged_in("Alice") # Prints: "Welcome, Alice!"Events with multiple parameters:
fromspellbind.eventimportBiEvent, TriEvent# Two parametersposition_changed=BiEvent[int, int]()
position_changed.observe(lambdax, y: print(f"Position: ({x}, {y})"))
position_changed(10, 20) # Prints: "Position: (10, 20)"# Three parametersrgb_changed=TriEvent[int, int, int]()
rgb_changed.observe(lambdar, g, b: print(f"Color: rgb({r}, {g}, {b})"))
rgb_changed(255, 128, 0) # Prints: "Color: rgb(255, 128, 0)"Prevent memory leaks with weak observers:
fromspellbind.eventimportEventevent=Event()
deftemporary_handler():
print("Handling event")
# Weak observation - handler can be garbage collectedevent.weak_observe(temporary_handler)Here's a practical example showing how to create automatically positioned windows:
fromspellbind.int_valuesimportIntVariableclassWindow:
def__init__(self, x: int, y: int, width: int, height: int):
self.x=IntVariable(x)
self.y=IntVariable(y)
self.width=IntVariable(width)
self.height=IntVariable(height)
def__repr__(self):
returnf"Window(x={self.x.value}, y={self.y.value}, width={self.width.value}, height={self.height.value})"# Create two windowsmain_window=Window(100, 50, 800, 600)
sidebar_window=Window(0, 0, 200, 400)
# Automatically position sidebar to the right of main windowmargin=IntVariable(10)
sidebar_window.x.bind(main_window.x+main_window.width+margin)
sidebar_window.y.bind(main_window.y)
print(main_window) # Window(x=100, y=50, width=800, height=600)print(sidebar_window) # Window(x=910, y=50, width=200, height=400)# Moving the main window automatically repositions the sidebarmain_window.x.value=200main_window.y.value=100print(main_window) # Window(x=200, y=100, width=800, height=600)print(sidebar_window) # Window(x=1010, y=100, width=200, height=400)# Changing margin updates sidebar positionmargin.value=20print(sidebar_window) # Window(x=1020, y=100, width=200, height=400)Value[T]- Type for all reactive values, useful for typing function parametersVariable[T]- Type for mutable values, useful for typing function parametersConstant[T]- Immutable value
IntValue,IntVariable- Integer values with arithmetic operationsFloatValue,FloatVariable- Float values with arithmetic operationsStrValue,StrVariable- String values with concatenationBoolValue- Boolean values with logical operations
Event- Basic event with no parametersValueEvent[T]- Event that passes one valueBiEvent[S, T]- Event that passes two valuesTriEvent[S, T, U]- Event that passes three values
pytestmypy srcflake8 .Author: Georg Plaz