Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
298 changes: 298 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
# pybind

[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![Tests](https://github.com/FancyNeuron/pybind/actions/workflows/python-package.yml/badge.svg)](https://github.com/FancyNeuron/pybind/actions/workflows/python-package.yml)

> Reactive programming for Python with reactive variables and events.

pybind is a reactive programming library that lets you create Variables that automatically update when their dependencies change, plus an event system for notifying observers.

## Installation

```bash
git clone https://github.com/FancyNeuron/pybind.git
cd pybind
pip install -e .
```

## Quick Start

```python
from pybind.int_values import IntVariable
from pybind.str_values import StrVariable

# Create reactive variables
name = StrVariable("Alice")
age = IntVariable(25)

# Create computed values that automatically update
greeting = 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 = 30

print(greeting) # "Bob is 30 years old"
```

## Core Concepts

### Values, Variables and Events

The foundation of pybind 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.

```python
from pybind.values import Constant
from pybind.int_values import IntVariable
from pybind.event import Event

# Variables can be changed
counter = IntVariable(0)
counter.value = 10

# Constants cannot be changed
pi = Constant(3.14159)

# Events notify observers
button_clicked = Event()
button_clicked.observe(lambda: print("Clicked!"))
button_clicked() # Prints: "Clicked!"
```

### Reactive Bindings

Variables can be **bound** to other Values, making them automatically update:

```python
from pybind.int_values import IntVariable

# Create computed values
base = IntVariable(10)
multiplier = IntVariable(3)
result = base * multiplier

# Bind variables to computed values
my_variable = IntVariable(0)
my_variable.bind_to(result)

print(my_variable) # 30

# Updates propagate automatically
base.value = 20
print(my_variable) # 60

# Unbind to break connections
my_variable.unbind()
```

## Advanced Features

### Weak vs Strong Binding

Control memory management with binding strength:

```python
from pybind.str_values import StrVariable

source = StrVariable("hello")
target = StrVariable("")

# Strong binding (default) - keeps source alive
target.bind_to(source, bind_weakly=False)

# Weak binding - allows source to be garbage collected
target.bind_to(source, bind_weakly=True)
```

### Circular Dependency Detection

pybind automatically prevents circular dependencies:

```python
from pybind.int_values import IntVariable

a = IntVariable(1)
b = IntVariable(2)

a.bind_to(b)
# b.bind_to(a) # This would raise RecursionError
```

### Observing Changes

React to value changes with observers:

```python
from pybind.int_values import IntVariable

def on_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"
```

## Event System

pybind includes an event system for notifying observers when things happen.

### Basic Events

```python
from pybind.event import Event

# Create an event
button_clicked = Event()

# Add observers
def handle_click():
print("Button was clicked!")

button_clicked.observe(handle_click)

# Trigger the event
button_clicked() # Prints: "Button was clicked!"
```

### Value Events

Events that pass data to observers:

```python
from pybind.event import ValueEvent

user_logged_in = ValueEvent[str]()

def welcome_user(username: str):
print(f"Welcome, {username}!")

user_logged_in.observe(welcome_user)
user_logged_in("Alice") # Prints: "Welcome, Alice!"
```

### Multi-Parameter Events

Events with multiple parameters:

```python
from pybind.event import BiEvent, TriEvent

# Two parameters
position_changed = BiEvent[int, int]()
position_changed.observe(lambda x, y: print(f"Position: ({x}, {y})"))
position_changed(10, 20) # Prints: "Position: (10, 20)"

# Three parameters
rgb_changed = TriEvent[int, int, int]()
rgb_changed.observe(lambda r, g, b: print(f"Color: rgb({r}, {g}, {b})"))
rgb_changed(255, 128, 0) # Prints: "Color: rgb(255, 128, 0)"
```

### Weak Observation

Prevent memory leaks with weak observers:

```python
from pybind.event import Event

event = Event()

def temporary_handler():
print("Handling event")

# Weak observation - handler can be garbage collected
event.weak_observe(temporary_handler)
```

## Example Application

Here's a practical example showing how to create automatically positioned windows:

```python
from pybind.int_values import IntVariable

class Window:
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):
return f"Window(x={self.x.value}, y={self.y.value}, width={self.width.value}, height={self.height.value})"

# Create two windows
main_window = Window(100, 50, 800, 600)
sidebar_window = Window(0, 0, 200, 400)

# Automatically position sidebar to the right of main window
margin = IntVariable(10)
sidebar_window.x.bind_to(main_window.x + main_window.width + margin)
sidebar_window.y.bind_to(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 sidebar
main_window.x.value = 200
main_window.y.value = 100

print(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 position
margin.value = 20
print(sidebar_window) # Window(x=1020, y=100, width=200, height=400)
```

## API Reference

### Core Classes

- **`Value[T]`** - Type for all reactive values, useful for typing function parameters
- **`Variable[T]`** - Type for mutable values, useful for typing function parameters
- **`Constant[T]`** - Immutable value

### Type-Specific Classes

- **`IntValue`**, **`IntVariable`** - Integer values with arithmetic operations
- **`FloatValue`**, **`FloatVariable`** - Float values with arithmetic operations
- **`StrValue`**, **`StrVariable`** - String values with concatenation
- **`BoolValue`** - Boolean values with logical operations

### Event Classes

- **`Event`** - Basic event with no parameters
- **`ValueEvent[T]`** - Event that passes one value
- **`BiEvent[S, T]`** - Event that passes two values
- **`TriEvent[S, T, U]`** - Event that passes three values

## Development

### Running Tests

```bash
pytest
```

### Type Checking

```bash
mypy src
```

### Linting

```bash
flake8 .
```

---

Author: Georg Plaz
10 changes: 9 additions & 1 deletion src/pybind/bool_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from abc import ABC

from pybind.values import Value, DerivedValue, _S, _T
from pybind.values import Value, DerivedValue, _S, _T, Constant


class BoolValue(Value[bool], ABC):
Expand All @@ -16,3 +16,11 @@ def __init__(self, value: Value[bool]):

def transform(self, value: bool) -> bool:
return not value


class BoolConstant(BoolValue, Constant[bool]):
pass


TRUE = BoolConstant(True)
FALSE = BoolConstant(False)
7 changes: 5 additions & 2 deletions src/pybind/float_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
from pybind.observables import Observer, ValueObserver

from pybind.values import Value, CombinedMixedValues, SimpleVariable, CombinedTwoValues, _create_value_getter, \
DerivedValue, DerivedValueBase

DerivedValue, DerivedValueBase, Constant

FloatLike = int | Value[int] | float | Value[float]

Expand Down Expand Up @@ -60,6 +59,10 @@ def __neg__(self) -> FloatValue:
return NegateFloatValue(self)


class FloatConstant(FloatValue, Constant[float]):
pass


def _create_float_getter(value: float | Value[int] | Value[float]) -> Callable[[], float]:
if isinstance(value, Value):
return lambda: value.value
Expand Down
6 changes: 5 additions & 1 deletion src/pybind/int_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from pybind.float_values import FloatValue, MultiplyFloatValues, DivideValues, SubtractFloatValues, \
AddFloatValues, CompareNumbersValues
from pybind.values import Value, CombinedMixedValues, SimpleVariable, CombinedTwoValues, DerivedValue
from pybind.values import Value, CombinedMixedValues, SimpleVariable, CombinedTwoValues, DerivedValue, Constant
from pybind.bool_values import BoolValue

IntLike = int | Value[int]
Expand Down Expand Up @@ -108,6 +108,10 @@ def __neg__(self) -> IntValue:
return NegateIntValue(self)


class IntConstant(IntValue, Constant[int]):
pass


class IntVariable(SimpleVariable[int], IntValue):
pass

Expand Down
11 changes: 9 additions & 2 deletions src/pybind/str_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
from abc import ABC
from typing import Any

from pybind.values import Value, DerivedValue, CombinedMixedValues, SimpleVariable
from pybind.values import Value, DerivedValue, CombinedMixedValues, SimpleVariable, Constant

StringLike = str | Value[str]


class StrValue(Value[str], ABC):
def __add__(self, other: Value[str]) -> StrValue:
def __add__(self, other: StringLike) -> StrValue:
return ConcatenateStrValues(self, other)

def __radd__(self, other: StringLike) -> StrValue:
return ConcatenateStrValues(other, self)


class StrConstant(StrValue, Constant[str]):
pass


class StrVariable(SimpleVariable[str], StrValue):
pass
Expand Down
6 changes: 6 additions & 0 deletions src/pybind/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ def to_str(self) -> StrValue:
from pybind.str_values import ToStrValue
return ToStrValue(self)

def __str__(self) -> str:
return str(self.value)

def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.value!r})"


class Variable(Value[_S], Generic[_S], ABC):
@property
Expand Down
Loading