Skip to content

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:

  1. 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.

  2. 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 descriptors
  • fynx.observable: Core observable classes
  • fynx.computed: Creating derived reactive values

ObservableValue

ObservableValue(observable)

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

observable

observable

Get the underlying observable instance.

__eq__

__eq__(other)

Compare wrapped values while remaining unhashable.

SubscriptableDescriptor

SubscriptableDescriptor(initial_value=None, original_observable=None)

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
  1. A SubscriptableDescriptor is attached directly to a class attribute
  2. On first attribute access, __get__ creates a class-level Observable instance (or uses the original if provided) and stores it as _{attr_name}_observable
  3. Returns an ObservableValue wrapper for transparent reactive access
  4. 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__(instance, owner)

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__(instance, value)

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__

__set_name__(owner, 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