Observable Descriptors¶
Classes for creating observable attributes in Store classes and other contexts.
FynX Observable Descriptors - reactive attribute descriptors¶
This module provides the descriptor classes that make reactive class
attributes look like regular ones. Write store.counter = 5 and the
descriptor handles subscriptions, notifications, and computed updates behind
the scenes - no explicit .set() or .subscribe() required for basic use.
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
Accessing store.counter returns an ObservableValue that wraps the actual
Observable. It behaves like the wrapped value for equality, string
conversion, and iteration, while still exposing reactive methods like
.subscribe() and the + / >> / & operators.
How it works¶
Two components do this together:
-
Observable descriptors:
observable()returns an Observable that also implements Python's descriptor protocol. When attached to a Store class, it creates or retrieves the owner-specific backing Observable and exposes typed class access. -
ObservableValue: Returned when accessing descriptor attributes, provides transparent value access through ValueMixin while maintaining reactive capabilities. This wrapper reads the underlying Observable directly, so repeated attribute access does not create hidden subscriptions.
When you write class UserStore(Store): name = observable("Alice"), the
Observable itself is the descriptor. Accessing UserStore.name calls its
__get__, which creates or retrieves the class-level Observable and wraps it
in an ObservableValue.
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.
Observable implements the descriptor protocol directly. When you define
name = observable("Alice") in a Store subclass, StoreMeta records the Observable
attribute and keeps an owner-specific backing observable in the Store's _observables
mapping. Descriptor access returns ObservableValue instances.
Performance Considerations¶
Observable instances are reused across attribute access, stored as class
attributes. Only the lightweight ObservableValue wrapper is created on-demand,
and it does not register observer callbacks unless you explicitly call
.subscribe(), so overhead stays close to a regular attribute access.
Limitations¶
Reactive attributes are class-level only - there's no instance-specific variant. Common operations (equality, iteration, string conversion) work transparently, but some advanced Python features involving metaclasses may not interact well with the wrapped values.
See Also¶
fynx.store: Store classes that use these descriptorsfynx.observable: Core observable classesfynx.computed: Creating derived reactive values
ObservableValue ¶
A wrapper that combines direct value access with observable capabilities.
Behaves like the underlying value for equality, string conversion,
iteration, and indexing, while also exposing observable methods like
.subscribe() and the reactive operators. This is what lets Store
attributes support both store.attr = value and
store.attr.subscribe(callback) through the same attribute.
The ValueMixin provides transparent behavior: __str__ delegates to the value,
__eq__ compares against the wrapped value, __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.
Lets Store classes and other reactive containers define attributes that read and write like regular Python attributes but return an ObservableValue - combining direct value access with observable methods.
Observable now implements the descriptor protocol used by Store attributes. This class remains available for direct descriptor use in advanced cases where a standalone descriptor object is useful.
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
- A SubscriptableDescriptor is attached directly to a class attribute
- 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):
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
Store classes usually use Observable's built-in descriptor behavior through
observable(). Direct SubscriptableDescriptor instantiation is usually not
needed, but remains supported for manual descriptor use.
See Also
ObservableValue: The wrapper returned by this descriptor observable: Function that creates typed Observable descriptors 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
|
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 |