Observable Descriptors¶
Classes for creating observable attributes in Store classes and other contexts.
FynX Observable Descriptors - Reactive Attribute Descriptors¶
ObservableValue acts as a transparent wrapper that makes reactive values behave like regular Python values. You access the value directly through familiar attribute syntax, while the wrapper tracks changes and triggers updates automatically behind the scenes. The value appears ordinary, but the system maintains reactive behavior.
This module provides descriptor classes that enable transparent reactive programming in class attributes. These descriptors bridge regular Python attribute access with reactive capabilities, allowing Store classes to provide familiar syntax alongside automatic dependency tracking and change propagation.
Transparent Reactivity¶
FynX's descriptors enable transparent reactivity—code that looks like regular
attribute access while maintaining automatic dependency tracking. You write
store.counter = 5 and the system handles subscriptions, notifications, and
computed updates. That transparency means existing code works without modification.
Instead of explicit reactive patterns:
# Traditional reactive approach
store.counter.subscribe(lambda v: print(v))
store.counter.set(5)
# Manual dependency tracking
def update_total():
total = store.price.value * store.quantity.value
You write natural attribute access:
# Transparent reactive approach
print(store.counter) # Direct access
store.counter = 5 # Automatic updates
# Automatic dependency tracking
total = store.price * store.quantity # Reactive computation
The descriptor system handles the reactive machinery behind the scenes. When you
access store.counter, the descriptor returns an ObservableValue that wraps the
actual Observable. That wrapper behaves like the value for most operations—equality,
string conversion, iteration—while also providing reactive methods like subscription
and operator overloading. We call this transparent reactivity—the value appears
ordinary, but the system tracks dependencies and propagates changes automatically.
How It Works¶
The descriptor system operates through two components working together:
-
SubscriptableDescriptor: Attached to class attributes by StoreMeta, creates and manages the underlying Observable instances at the class level. The StoreMeta metaclass converts
observable()instances (which return Observable objects) into SubscriptableDescriptor instances during class creation. -
ObservableValue: Returned when accessing descriptor attributes, provides transparent value access through ValueMixin while maintaining reactive capabilities. This wrapper subscribes to the underlying Observable to keep its displayed value synchronized.
When you write class UserStore(Store): name = observable("Alice"), the StoreMeta
metaclass intercepts that Observable instance and replaces it with a SubscriptableDescriptor.
Accessing UserStore.name then triggers the descriptor's __get__ method, which
creates or retrieves the class-level Observable and returns an ObservableValue wrapper.
That wrapper delegates value operations to the underlying value while preserving
reactive behavior. Result: attribute access looks normal, but the system maintains
dependency graphs and triggers updates automatically.
Common Patterns¶
Store Attributes:
from fynx import Store, observable
class UserStore(Store):
name = observable("Alice")
age = observable(30)
# Access like regular attributes
print(UserStore.name) # "Alice"
UserStore.age = 31 # Triggers reactive updates
# But also provides reactive methods
UserStore.name.subscribe(lambda n: print(f"Name: {n}"))
Transparent Integration:
from fynx import Store, observable
class AppStore(Store):
is_enabled = observable(True)
items = observable([1, 2, 3])
name = observable("Alice")
age = observable(30)
# Works with existing Python constructs
if AppStore.is_enabled:
print("Enabled")
for item in AppStore.items:
print(item)
# String formatting
message = f"User: {AppStore.name}, Age: {AppStore.age}"
Reactive Operators:
from fynx import Store, observable
class UserStore(Store):
first_name = observable("John")
last_name = observable("Doe")
age = observable(20)
name = observable("John")
# All operators work transparently
full_name = UserStore.first_name + UserStore.last_name >> (lambda f, l: f"{f} {l}")
is_adult = UserStore.age >> (lambda a: a >= 18)
valid_user = UserStore.name & is_adult
Implementation Details¶
The descriptor protocol uses __get__, __set__, and __set_name__ to integrate
with Python's attribute system. Observables are stored at the class level (as
_{attr_name}_observable attributes) to ensure shared state across all access.
ObservableValue instances are created on-demand when attributes are accessed, and
they subscribe to the underlying Observable to maintain synchronization.
The StoreMeta metaclass performs the conversion from Observable to SubscriptableDescriptor
during class creation. When you define name = observable("Alice") in a Store subclass,
StoreMeta detects the Observable instance and replaces it with a SubscriptableDescriptor
that wraps the original Observable. That descriptor then manages the class-level storage
and returns ObservableValue instances on access.
Performance Considerations¶
The system reuses Observable instances across attribute access, storing them as class attributes. ObservableValue wrappers are created on-demand and subscribe to updates, but the underlying Observable instances persist. This design minimizes memory overhead while maintaining reactive behavior.
Limitations¶
Descriptor behavior requires class-level attribute assignment—instance-specific reactive attributes are not supported. Some advanced Python features (like certain metaclass interactions) may not work as expected with wrapped values, though common operations (equality, iteration, string conversion) work transparently.
See Also¶
fynx.store: Store classes that use these descriptorsfynx.observable: Core observable classesfynx.computed: Creating derived reactive values
ObservableValue ¶
A transparent wrapper that combines direct value access with observable capabilities.
ObservableValue acts as a bridge between regular Python value access and reactive programming. This wrapper behaves like the underlying value in most contexts—equality, string conversion, iteration, indexing—while also providing access to observable methods like subscription and operator overloading. The value appears ordinary, but the wrapper maintains reactive behavior behind the scenes.
This class enables Store classes and other descriptor-based reactive systems to
provide both familiar value access (store.attr = value) and reactive capabilities
(store.attr.subscribe(callback)) through a single attribute. The wrapper subscribes
to the underlying Observable during initialization, keeping its displayed value
synchronized automatically.
The ValueMixin provides transparent behavior: __str__ delegates to the value,
__eq__ compares against the value (delegating to the underlying Observable's equality),
__iter__ iterates over collections or wraps scalars in a single-item list, __len__
returns 0 for non-collections or the collection length otherwise, and __contains__
works for collections but returns False for scalars. Reactive operators (+, >>, &)
unwrap ObservableValue operands and delegate to the underlying Observable.
Example
from fynx import Store, observable
class CounterStore(Store):
count = observable(0)
# ObservableValue provides both value access and reactive methods
counter = CounterStore.count
# Direct value access (like a regular attribute)
print(counter) # 0
print(counter == 0) # True
print(len(counter)) # 0 (returns 0 for non-collections)
# Iteration: scalars wrap in single-item list, collections iterate normally
for x in counter:
print(x) # 0 (scalar wrapped as [0])
# Observable methods
counter.set(5) # Update the value
counter.subscribe(lambda x: print(f"Count: {x}"))
# Reactive operators
doubled = counter >> (lambda x: x * 2)
Note
ObservableValue instances are typically created automatically by SubscriptableDescriptor when accessing observable attributes on Store classes. You usually won't instantiate this class directly.
See Also
SubscriptableDescriptor: Creates ObservableValue instances for class attributes Observable: The underlying reactive value class Store: Uses ObservableValue for transparent reactive attributes
SubscriptableDescriptor ¶
Descriptor that creates reactive class attributes with transparent observable behavior.
SubscriptableDescriptor enables Store classes and other reactive containers to define attributes that behave like regular Python attributes while providing full reactive capabilities. When accessed, it returns an ObservableValue instance that combines direct value access with observable methods.
This descriptor is the foundation for FynX's transparent reactive programming model.
The StoreMeta metaclass converts observable() instances (which return Observable
objects) into SubscriptableDescriptor instances during class creation. When you write
name = observable("Alice") in a Store subclass, StoreMeta detects the Observable
and replaces it with a SubscriptableDescriptor that wraps the original Observable.
The descriptor stores observables at the class level (as _{attr_name}_observable
attributes) to ensure shared state across all access. On first access via __get__,
it creates or retrieves the class-level Observable and returns an ObservableValue wrapper.
Subsequent accesses reuse the same observable instance. The __set__ method delegates
to the observable's set() method, triggering reactive updates.
How It Works
- StoreMeta converts
observable()instances to SubscriptableDescriptor during class creation, storing the original Observable for later use - On first attribute access,
__get__creates a class-level Observable instance (or uses the original if provided) and stores it as_{attr_name}_observable - Returns an ObservableValue wrapper for transparent reactive access
- Subsequent accesses reuse the same observable instance from the class
Example
from fynx import Store, observable
class UserStore(Store):
# StoreMeta converts this Observable to a SubscriptableDescriptor
name = observable("Alice")
age = observable(30)
# Access returns ObservableValue instances
user_name = UserStore.name # ObservableValue wrapping Observable
user_age = UserStore.age # ObservableValue wrapping Observable
# Behaves like regular attributes
print(user_name) # "Alice"
UserStore.name = "Bob" # Updates the observable
print(user_name) # "Bob"
# But also provides reactive methods
UserStore.name.subscribe(lambda n: print(f"Name changed to: {n}"))
Note
This descriptor is typically used indirectly through the observable() function
in Store classes, which returns an Observable that StoreMeta converts to a
SubscriptableDescriptor. Direct instantiation is usually not needed.
See Also
ObservableValue: The wrapper returned by this descriptor observable: Function that creates Observable instances (converted by StoreMeta) Store: Uses this descriptor for reactive class attributes
__get__ ¶
Get the observable value for this attribute.
This method is called when the attribute is accessed. It creates or retrieves the class-level Observable instance and returns an ObservableValue wrapper that provides transparent value access while maintaining reactive capabilities.
The observable is stored at the class level as _{attr_name}_observable to
ensure shared state across all access. If the original Observable was provided
during descriptor creation (by StoreMeta), it is reused; otherwise, a new
Observable is created with the initial value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instance
|
Optional[object]
|
The instance that accessed the attribute (unused for class-level access) |
required |
owner
|
Optional[Type]
|
The class that owns this descriptor |
required |
Returns:
| Type | Description |
|---|---|
Any
|
An ObservableValue instance wrapping the class-level Observable |
Raises:
| Type | Description |
|---|---|
AttributeError
|
If the descriptor is not properly initialized (owner is None) |
__set__ ¶
Set the value on the observable.
This method is called when the attribute is assigned a new value. It delegates
to the underlying Observable's set() method, which triggers reactive updates
and notifies all observers.
The observable is created if it doesn't exist (using the same logic as __get__),
ensuring that assignment works even before the attribute has been accessed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instance
|
Optional[object]
|
The instance that assigned the value (unused for class-level assignment) |
required |
value
|
Optional[T]
|
The new value to set on the observable |
required |
Raises:
| Type | Description |
|---|---|
AttributeError
|
If the descriptor is not properly initialized and no instance is provided to determine the owner class |
__set_name__ ¶
Called when the descriptor is assigned to a class attribute.
This method is invoked automatically by Python when the descriptor is assigned
to a class attribute. It stores the attribute name and owner class for later
use in __get__ and __set__ methods.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
owner
|
Type
|
The class that owns this descriptor |
required |
name
|
str
|
The name of the attribute this descriptor is assigned to |
required |