A declarative reactive programming framework.
Project description
Reactives
Reactives lets you write reactive code easily by making any of your objects and functions reactive. A reactive
can be triggered (reactive.react.trigger()
), causing all its reactors to be called. A reactor is any callable
that takes no arguments, and you add it to a reactive via reactive.react(reactor)
. When a reactive is triggered,
its reactors and its reactors' reactors are resolved, and each is called once in order.
Reactives uses a push-pull approach, meaning change notifications are pushed (reactors are called automatically and won't have to pull for changes), but if a reactor needs to know what exactly changed, it must pull this information itself.
Usage
For any type to be reactive, it must have a react
attribute containing a reactives.ReactorController
. The most
common way to achieve this, is to decorate that type with @reactives.reactive
. Some classes are provided that are
reactive and can be instantiated or inherited from directly.
Custom classes
Decorate a class to make its individual instances reactive:
import reactive from reactives
@reactive
class Apple:
pass
apple = Apple()
apple.react(lambda: print('The apple got triggered!'))
apple.react.trigger()
# >>> "The apple got triggered!"
Functions and methods
Decorate a function:
import reactive from reactives
@reactive
def apple():
pass
apple.react(lambda: print('The apple got triggered!'))
apple.react.trigger()
# >>> "The apple got triggered!"
Decorate a method on a reactive class:
import reactive from reactives
@reactive
class Apple:
@reactive
def apple(self):
pass
apple = Apple()
apple.react.getattr('apple').react(lambda: print('The apple got triggered!'))
apple.react.getattr('apple').react.trigger()
# >>> "The apple got triggered!"
Reactive methods must be accessed through their instance, because Apple.apple
would yield the class method.
Both functions and methods can be called automatically when they're triggered. This lets them set up something once, and update that thing when they're triggered:
import reactive from reactives
@reactive(on_trigger_call=True)
def warm_caches():
"""
Warm the application's caches. When triggered (because the cached data has changed), re-warm the caches.
"""
pass
Properties
Decorate a property:
import reactive from reactives
@reactive
class Apple:
@reactive
@property
def apple(self) -> str:
return 'I got you something!'
apple = Apple()
apple.react.getattr('apple').react(lambda: print('The apple got triggered!'))
apple.react.getattr('apple').react.trigger()
# >>> "The apple got triggered!"
Properties can call any callable automatically when they're triggered. This can be useful for computed properties. The callable receives the property's instance as its only argument:
import reactive from reactives
@reactive
class Apple:
def __init__(self):
self._computed_something = None
@reactive(on_trigger=(lambda instance: setattr(instance, '_computed_something', None),))
@property
def apple(self) -> str:
if self._computed_something is None:
self._computed_something = 'I got you something!'
return self._computed_something
Optionally add a setter and/or deleter like you would to any non-reactive property. If either the setter or deleter is called, the property will be triggered.
import reactive from reactives
@reactive
class Apple:
def __init__(self):
self._something = 'I got you something!'
@reactive
@property
def apple(self) -> str:
return self._something
@apple.setter
def apple(self, something: str):
self._something = something
@apple.deleter
def apple(self):
self._something = None
apple = Apple()
apple.react.getattr('apple').react(lambda: print('The apple got triggered!'))
apple.apple = 'I got you something else!'
# >>> "The apple got triggered!"
del apple.apple
# >>> "The apple got triggered!"
Values set through a property may themselves be reactive too. If they are, the property and the value are autowired, which means that the property becomes a reactor to the newly added value. As soon as the value is triggered, so is the property. Therefore, if you want to react to any change to any of the values a property might have, all you need to do is add your reactor to the property.
Lists
ReactiveList
is a reactive version of Python's built-in list
. You can use it in exactly the same way as list
:
from reactives import ReactiveList
fruits = ReactiveList(['apple', 'banana'])
fruits.react(lambda: print('Look at all these delicious fruits!'))
fruits.append('orange')
# >>> "Look at all these delicious fruits!"
Values added to a ReactiveList
may themselves be reactive too. If they are, the list and the value are autowired,
which means that the list becomes a reactor to the newly added value. As soon as the value is triggered,
so is the list. Therefore, if you want to react to any change to any of the values in a ReactiveList
, all you
need to do is add your reactor to the list.
Dictionaries
ReactiveDict
is a reactive version of Python's built-in dict
. You can use it in exactly the same way as dict
:
from reactives import ReactiveDict
fruits = ReactiveDict(apple=5, banana=2)
fruits.react(lambda: print('Look at all these delicious fruits!'))
fruits['orange'] = 4
# >>> "Look at all these delicious fruits!"
Values added to a ReactiveDict
may themselves be reactive too. If they are, the dictionary and the value are
autowired, which means that the dictionary becomes a reactor to the newly added value. As soon as the value is
triggered, so is the dictionary. Therefore, if you want to react to any change to any of the values in a ReactiveDict
,
all you need to do is add your reactor to the dictionary.
Autowiring
We've seen how properties, lists, and dictionaries autowire themselves to
their values. This is possible because properties, lists, and dictionaries know exactly which values move in and out of
them. In other cases, we use scope. Any reactive can start a scope with reactives.Scope.collect()
and collect all
reactives that are called or used during that scope window, and autowire itself to them. Conversely, any reactive can
register itself with the current scope (if there is one) with reactives.Scope.register*()
, and allow reactives
depending on it to autowire themselves. In fact, this is what properties do internally.
Autowiring means that as a developer, you won't need to worry about connecting the parts of your application most of the time.
Development
First, fork and clone the repository, and navigate to its root directory.
Requirements
- Bash (you're all good if
which bash
outputs a path in your terminal)
Installation
If you have tox installed on your machine, tox --develop
will create the necessary
virtual environments and install all development dependencies.
Alternatively, in any existing Python environment, run ./bin/build-dev
.
Testing
In any existing Python environment, run ./bin/test
.
Fixing problems automatically
In any existing Python environment, run ./bin/fix
.
Contributions 🥳
Reactives is Free and Open Source Software. As such you are welcome to report bugs or submit improvements.
Copyright & license
Reactives is copyright Bart Feenstra and contributors, and released under the GNU General Public License, Version 3. In short, that means you are free to use Reactives, but if you distribute Reactives yourself, you must do so under the exact same license, provide that license, and make your source code available.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for reactives-0.1.0-py2.py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 17345cba969b20c1382695056de574ca92389456c5c467cff4414975339ee95e |
|
MD5 | a6fdde38522fd67e4654e97414df0ebf |
|
BLAKE2b-256 | 5d00c5668be64c1e1921c2e06fa0bf970c70c2e388759623dba679dced688182 |