"""
This module implements helper functions and classes that can be used to define
reducers in the same fashion of redux ones, but using decorators instead of
anonymous functions.
Things you **should never do** inside a reducer:
* Mutate its arguments;
* Perform side effects like API calls and routing transitions;
* Call **non-pure** functions.
**Given the same arguments, it should calculate the next state and return it. No
surprises. No side effects. No API calls. No mutations. Just a calculation.**
Create a reducer
================
A reducer is a function that looks like this:
.. code:: python
def dummy(prev, action):
next = prev
if action.type == ActionType.DUMMY_ACTION_TYPE:
# Do something
return next
In order to decrease the amount of required boilerplate ``revived`` makes use of
a lot of python goodies, especially **decorators**.
While every function can be used as ``reducer`` (as long as it takes the proper
parameters), the easiest way to create a ``reducer`` that handles a specific
type of ``actions`` is to use the :any:`revived.reducer.reducer` decorator.
.. code:: python
@reducer(ActionType.DUMMY_ACTION_TYPE)
def dummy(prev, action):
next = prev
# Do something
return next
Combine reducers
================
You can naively combine several ``reducers`` in this way:
.. code:: python
def dummy(prev, action):
next = prev
if action.type == ActionType.DUMMY_ACTION_TYPE1:
# Do something
return next
elif action.type == ActionType.DUMMY_ACTION_TYPE2:
# Do something different
return next
else:
return next
but this is going to make your ``reducer`` function huge and barely readable.
:any:`revived.reducer` contains utility functions that allows you to create much
more readable ``reducers``.
Reducers can (*and should*) be combined. You can easily do this combination
using :any:`revived.reducer.combine_reducers`.
The following example will produce a ``combined reducer`` where both the
``reducers`` will handle the whole subtree passed to it: exactly the same result of
the previous snippet of code!
.. code:: python
@reducer(ActionType.DUMMY_ACTION_TYPE1)
def dummy1(prev, action):
next = prev
# Do something
return next
@reducer(ActionType.DUMMY_ACTION_TYPE2)
def dummy2(prev, action):
next = prev
# Do something
return next
combined_reducer = combine_reducers(dummy1, dummy2)
**Note**: a ``combined reducer`` is a ``reducer`` and can be combined again with
other reducers allowing you to creare every structure you will ever need in your
app.
Pass a subtree of the state
---------------------------
If you want it is possible to pass to a reducer only a subtree of the state
passed to the ``combined reducer``. To do this you should use keyword arguments
in this way:
.. code:: python
@reducer(ActionType.DUMMY_ACTION_TYPE1)
def dummy1(prev, action):
next = prev
# Do something
return next
@reducer(ActionType.DUMMY_ACTION_TYPE2)
def dummy2(prev, action):
next = prev
# Do something
return next
combined_reducer = combine_reducers(dummy1, dummy_subtree=dummy2)
In this example ``dummy1`` will receive the whole subtree passed to the
``combined_reducer`` while ``dummy2`` will only receive the ``dummy_subtree``
subtree.
Create a reducer module
=======================
A ``reducer module`` is an utility object that behave exactly like a single
``reducer``, but permits to register more ``reducers`` into it. You will use it
to define a bunch of ``reducers`` that are all handling the same subtree of the
``state``.
Note that this is *only a helper construct*, because the following snippet of
code:
.. code:: python
mod = Module()
@mod.reducer(ActionType.DUMMY_ACTION_TYPE1)
def dummy1(prev, action):
next = prev
# Do something
return next
@mod.reducer(ActionType.DUMMY_ACTION_TYPE2)
def dummy2(prev, action):
next = prev
# Do something
return next
has exactly the same result of:
.. code:: python
@reducer(ActionType.DUMMY_ACTION_TYPE1)
def dummy1(prev, action):
next = prev
# Do something
return next
@reducer(ActionType.DUMMY_ACTION_TYPE2)
def dummy2(prev, action):
next = prev
# Do something
return next
module_reducer = combine_reducers(dummy1, dummy2)
And of course **you can combine** a ``reducer module`` with other ``reducers``
and ``reducer modules``.
"""
from .action import Action
from .action import ActionType
from functools import wraps
from typing import Any
from typing import Callable
from typing import List
from typing import Union
Reducer = Callable[[Any, Action], Any]
ReducerList = List[Reducer]
[docs]class Module:
"""Helper class for module creations.
This is just an helper class: you can obtain the same result using the
reducer decorator and then combining all the defined reducers as top-level
reducers. The module instance will work exactly as a reducer function, but
will call all the registered reducers. The call order is not guaranteed.
"""
def __init__(self) -> None:
self._reducers = [] # type: ReducerList
def __call__(self, prev: Any, action: Action):
"""Lets the module work like a reducer.
:param pref: The previous state.
:param action: The action performed.
:returns: The next state.
"""
next = prev
for r in self._reducers:
next = r(next, action)
return next
[docs] def reducer(self, action_type: ActionType) -> Callable[[Reducer], Reducer]:
"""Decorator function to create a reducer.
Creates a reducer attached to the module. This reducer is handling the
specified action type and it is going to be ignored in case the action
is of a different type.
:param action_type: The action type.
:returns: The reducer function.
"""
def wrap(f: Reducer) -> Reducer:
@wraps(f)
def wrapped(prev: Any, action: Action) -> Reducer:
next = prev
if action.type == action_type:
next = f(prev, action)
return next
self._reducers.append(wrapped)
return wrapped
return wrap
[docs]def reducer(action_type: ActionType) -> Callable[[Reducer], Reducer]:
"""Decorator function to create a reducer.
Creates a reducer. This reducer is handling the specified action type and it
is going to be ignored in case the action is of a different type.
:param action_type: The action type. :returns: The reducer function.
:returns: The reducer function.
"""
def wrap(f: Reducer) -> Reducer:
@wraps(f)
def wrapped(prev: Any, action: Action) -> Reducer:
next = prev
if action.type == action_type:
next = f(prev, action)
return next
return wrapped
return wrap
[docs]def combine_reducers(*top_reducers: Union[Reducer, Module], **reducers: Union[Reducer, Module]) -> Reducer:
"""Create a reducer combining the reducers passed as parameters.
It is possible to use this function to combine top-level reducers or to
assign to reducers a specific subpath of the state. The result is a reducer,
so it is possible to combine the resulted function with other reducers
creating at-will complex reducer trees.
:param top_reducers: An optional list of top-level reducers.
:param reducers: An optional list of reducers that will handle a subpath.
:returns: The combined reducer function.
"""
def reduce(prev: Any, action: Action) -> Any:
next = prev
for r in top_reducers:
next = r(next, action)
for key, r in reducers.items():
next[key] = r(next.get(key), action)
return next
return reduce