Skip to content

Instantly share code, notes, and snippets.

@usrlocalben
Created April 30, 2016 18:44
Show Gist options
  • Save usrlocalben/a3debdbc23e917fbc2fa7320395372ee to your computer and use it in GitHub Desktop.
Save usrlocalben/a3debdbc23e917fbc2fa7320395372ee to your computer and use it in GitHub Desktop.
"""
python + redux == pydux
Benjamin Yates, 2016
Redux: http://redux.js.org
A somewhat literal translation of Redux & counter demo,
incl. apply_middleware, logging middleware, and thunks.
Tornado is used to demonstrate thunks and async actions.
Closures in Python are over references (as opposed to
names as in JavaScript) so single-element arrays are
used to create read/write closures.
extend() provides an alternative to the es6 ...spread
operator, or es5's Object.assign({} ...)
Ensuring that action.type always exists helps minimize
excessive .get()ing (and precious line-length)
"""
from datetime import timedelta
from tornado.ioloop import IOLoop
def main():
"""demo based on the redux counter example.
demonstrates:
- multiple subscribed listeners
- detecting state change in a listener
- unsubscribing a listener
- async 'work' using thunks
- logging middleware
"""
main_loop = IOLoop.instance()
store = create_store(
combine_reducers({'counter': counter}),
enhancer=apply_middleware(thunk_middleware, log_middleware),
)
dispatch, subscribe = store['dispatch'], store['subscribe']
def log_counter():
state = store['get_state']()
msg = '>>> Active Tasks: {}, Counter: {}'.format(
state['counter']['task_count'],
state['counter']['value'])
print msg
last_value = [None] # r/w closure
def four_alert():
state = store['get_state']()
cur_value = state['counter']['value']
if last_value[0] != cur_value:
last_value[0] = cur_value
if cur_value == 4:
print '----- counter is now 4 -----'
subscribe(four_alert)
unsubscribe_log = subscribe(log_counter)
log_counter()
dispatch(increment_counter())
dispatch(increment_counter())
dispatch(increment_counter())
unsubscribe_log()
dispatch(increment_counter()) # not shown
dispatch(increment_counter()) # not shown
unsubscribe_log = subscribe(log_counter)
dispatch(wait_then_decrement_counter(1))
dispatch(wait_then_decrement_counter(2))
dispatch(wait_then_decrement_counter(3))
dispatch(wait_then_decrement_counter(4))
dispatch(wait_then_decrement_counter(5)) # should be back to zero...
dispatch(wait_then_terminate(10))
main_loop.start()
print 'Terminating.'
# demo constants
INCREMENT_COUNTER = 'INCREMENT_COUNTER'
DECREMENT_COUNTER = 'DECREMENT_COUNTER'
INCREMENT_TASK_COUNT = 'INCREMENT_TASK_COUNT'
DECREMENT_TASK_COUNT = 'DECREMENT_TASK_COUNT'
# demo reducer
def counter(state=None, action=None):
if state is None:
state = {'value': 0, 'task_count': 0}
if action['type'] == INCREMENT_COUNTER:
return extend(
state,
{'value': state['value'] + 1},
)
elif action['type'] == DECREMENT_COUNTER:
return extend(
state,
{'value': state['value'] - 1},
)
elif action['type'] == INCREMENT_TASK_COUNT:
return extend(
state,
{'task_count': state['task_count'] + 1},
)
elif action['type'] == DECREMENT_TASK_COUNT:
return extend(
state,
{'task_count': state['task_count'] - 1},
)
else:
return state
# demo actions
def increment_counter():
return {'type': INCREMENT_COUNTER}
def decrement_counter():
return {'type': DECREMENT_COUNTER}
def increment_task_count():
return {'type': INCREMENT_TASK_COUNT}
def decrement_task_count():
return {'type': DECREMENT_TASK_COUNT}
def wait_then_decrement_counter(seconds):
def inner(dispatch, get_state):
main_loop = IOLoop.instance()
dispatch(increment_task_count())
def then():
dispatch(decrement_counter())
dispatch(decrement_task_count())
main_loop.add_timeout(timedelta(seconds=seconds), then)
return inner
def wait_then_terminate(seconds):
def inner(dispatch, get_state):
main_loop = IOLoop.instance()
def then():
main_loop.stop()
main_loop.add_timeout(timedelta(seconds=seconds), then)
return inner
# pydux
def create_store(reducer, enhancer=None):
"""redux in a nutshell. replace_reducer omitted"""
if enhancer is not None:
return enhancer(create_store)(reducer)
state = [{}] # r/w closure
listeners = [[]] # r/w closure
def get_state():
return state[0]
def dispatch(action):
state[0] = reducer(state[0], action)
[listener() for listener in listeners[0]]
def subscribe(listener):
def unsubcribe():
listeners[0] = [item for item in listeners[0] if item != listener]
listeners[0].append(listener)
return unsubcribe
dispatch({'type': None})
return {
'dispatch': dispatch,
'get_state': get_state,
'subscribe': subscribe,
}
def combine_reducers(defs):
"""helper for composing the state-tree of reducers"""
def reducer(state, action):
return {key: subreducer(state.get(key), action)
for key, subreducer in defs.iteritems()}
return reducer
def apply_middleware(*middlewares):
"""returns an enhancer function composed of middleware"""
def inner(create_store):
def create_wrapper(reducer, enhancer=None):
store = create_store(reducer, enhancer)
dispatch = store['dispatch']
middlewareAPI = {
'get_state': store['get_state'],
'dispatch': lambda action: dispatch(action),
}
chain = [mw(middlewareAPI) for mw in middlewares]
dispatch = compose(*chain)(store['dispatch'])
return extend(store, {'dispatch': dispatch})
return create_wrapper
return inner
def compose(*funcs):
"""compose(a,b,c) returns f, where f(x) = a(b(c(x)))"""
if not funcs:
return lambda *args: args
last = funcs[-1]
rest = funcs[0:-1]
return lambda *args: reduce(lambda ax, func: func(ax),
reversed(rest), last(*args))
def thunk_middleware(store):
"""thunks, from https://github.com/aearon/redux-thunk"""
dispatch, get_state = store['dispatch'], store['get_state']
def wrapper(next):
def thunk_dispatch(action):
if hasattr(action, '__call__'):
return action(dispatch, get_state)
return next(action)
return thunk_dispatch
return wrapper
def log_middleware(store):
"""log all actions to console as they are dispatched"""
def wrapper(next):
def log_dispatch(action):
print 'Dispatch Action:', action
return next(action)
return log_dispatch
return wrapper
# extra
def extend(*items):
"""fold-left shallow dictionary merge"""
def merge(ax, item):
ax.update(item)
return ax
return reduce(merge, items, {})
if __name__ == '__main__':
main()
# vim: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment