Commit 45a1583b authored by qsr's avatar qsr Committed by Commit bot

mojo: Add promises for python bindings.

This CL introduces promise for the python bindings. The API is following
ECMAScript 6.

BUG=418109
R=sdefresne@chromium.org,cmasone@chromium.org

Review URL: https://codereview.chromium.org/610613002

Cr-Commit-Position: refs/heads/master@{#297402}
parent e749bca9
...@@ -611,6 +611,7 @@ ...@@ -611,6 +611,7 @@
'public/python/mojo/bindings/__init__.py', 'public/python/mojo/bindings/__init__.py',
'public/python/mojo/bindings/descriptor.py', 'public/python/mojo/bindings/descriptor.py',
'public/python/mojo/bindings/messaging.py', 'public/python/mojo/bindings/messaging.py',
'public/python/mojo/bindings/promise.py',
'public/python/mojo/bindings/reflection.py', 'public/python/mojo/bindings/reflection.py',
'public/python/mojo/bindings/serialization.py', 'public/python/mojo/bindings/serialization.py',
], ],
......
...@@ -50,6 +50,7 @@ copy("bindings") { ...@@ -50,6 +50,7 @@ copy("bindings") {
"mojo/bindings/__init__.py", "mojo/bindings/__init__.py",
"mojo/bindings/descriptor.py", "mojo/bindings/descriptor.py",
"mojo/bindings/messaging.py", "mojo/bindings/messaging.py",
"mojo/bindings/promise.py",
"mojo/bindings/reflection.py", "mojo/bindings/reflection.py",
"mojo/bindings/serialization.py", "mojo/bindings/serialization.py",
] ]
......
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Promise used by the python bindings.
The API is following the ECMAScript 6 API for promises.
"""
class Promise(object):
"""The promise object."""
STATE_PENDING = 0
STATE_FULLFILLED = 1
STATE_REJECTED = 2
STATE_BOUND = 3
def __init__(self, generator_function):
"""
Constructor.
Args:
generator_function: A function taking 2 arguments: resolve and reject.
When |resolve| is called, the promise is fullfilled with the given value.
When |reject| is called, the promise is rejected with the given value.
A promise can only be resolved or rejected once, all following calls will
have no effect.
"""
self._onCatched = []
self._onFulfilled = []
self._onRejected = []
self._state = Promise.STATE_PENDING
self._result = None
if generator_function:
generator_function(self._Resolve, self._Reject)
@staticmethod
def Resolve(value):
"""
If value is a promise, make a promise that have the same behavior as value,
otherwise make a promise that fulfills to value.
"""
if isinstance(value, Promise):
return value
return Promise(lambda x, y: x(value))
@staticmethod
def Reject(reason):
"Make a promise that rejects to reason."""
return Promise(lambda x, y: y(reason))
@staticmethod
def All(*iterable):
"""
Make a promise that fulfills when every item in the array fulfills, and
rejects if (and when) any item rejects. Each array item is passed to
Promise.resolve, so the array can be a mixture of promise-like objects and
other objects. The fulfillment value is an array (in order) of fulfillment
values. The rejection value is the first rejection value.
"""
def GeneratorFunction(resolve, reject):
state = {
'rejected': False,
'nb_resolved': 0,
}
promises = [Promise.Resolve(x) for x in iterable]
results = [None for x in promises]
def OnFullfilled(i):
def OnFullfilled(res):
if state['rejected']:
return
results[i] = res
state['nb_resolved'] = state['nb_resolved'] + 1
if state['nb_resolved'] == len(results):
resolve(results)
return OnFullfilled
def OnRejected(reason):
if state['rejected']:
return
state['rejected'] = True
reject(reason)
for (i, promise) in enumerate(promises):
promise.Then(OnFullfilled(i), OnRejected)
return Promise(GeneratorFunction)
@staticmethod
def Race(*iterable):
"""
Make a Promise that fulfills as soon as any item fulfills, or rejects as
soon as any item rejects, whichever happens first.
"""
def GeneratorFunction(resolve, reject):
state = {
'ended': False
}
def OnEvent(callback):
def OnEvent(res):
if state['ended']:
return
state['ended'] = True
callback(res)
return OnEvent
for promise in [Promise.Resolve(x) for x in iterable]:
promise.Then(OnEvent(resolve), OnEvent(reject))
return Promise(GeneratorFunction)
@property
def state(self):
if isinstance(self._result, Promise):
return self._result.state
return self._state
def Then(self, onFullfilled=None, onRejected=None):
"""
onFulfilled is called when/if this promise resolves. onRejected is called
when/if this promise rejects. Both are optional, if either/both are omitted
the next onFulfilled/onRejected in the chain is called. Both callbacks have
a single parameter, the fulfillment value or rejection reason. |Then|
returns a new promise equivalent to the value you return from
onFulfilled/onRejected after being passed through Resolve. If an
error is thrown in the callback, the returned promise rejects with that
error.
"""
if isinstance(self._result, Promise):
return self._result.Then(onFullfilled, onRejected)
def GeneratorFunction(resolve, reject):
if self._state == Promise.STATE_PENDING:
self._onFulfilled.append(_Delegate(resolve, reject, onFullfilled))
self._onRejected.append(_Delegate(reject, reject, onRejected))
if self._state == self.STATE_FULLFILLED:
_Delegate(resolve, reject, onFullfilled)(self._result)
if self._state == self.STATE_REJECTED:
recover = reject
if onRejected:
recover = resolve
_Delegate(recover, reject, onRejected)(self._result)
return Promise(GeneratorFunction)
def Catch(self, onCatched):
"""Equivalent to |Then(None, onCatched)|"""
return self.Then(None, onCatched)
def _Resolve(self, value):
if self.state != Promise.STATE_PENDING:
return
self._result = value
if isinstance(value, Promise):
self._state = Promise.STATE_BOUND
self._result.Then(_IterateAction(self._onFulfilled),
_IterateAction(self._onRejected))
return
self._state = Promise.STATE_FULLFILLED
for f in self._onFulfilled:
f(value)
self._onFulfilled = None
self._onRejected = None
def _Reject(self, reason):
if self.state != Promise.STATE_PENDING:
return
self._result = reason
self._state = Promise.STATE_REJECTED
for f in self._onRejected:
f(reason)
self._onFulfilled = None
self._onRejected = None
def _IterateAction(iterable):
def _Run(x):
for f in iterable:
f(x)
return _Run
def _Delegate(resolve, reject, action):
def _Run(x):
try:
if action:
resolve(action(x))
else:
resolve(x)
except Exception as e:
reject(e)
return _Run
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import unittest
# pylint: disable=F0401
from mojo.bindings import promise
class PromiseTest(unittest.TestCase):
def setUp(self):
self.accumulated = []
def _AddToAccumulated(self, res):
self.accumulated.append(res)
return res
def testResolve(self):
p = promise.Promise.Resolve(0)
self.assertEquals(p.state, promise.Promise.STATE_FULLFILLED)
p.Then(self._AddToAccumulated)
self.assertEquals(self.accumulated, [0])
def testResolveToPromise(self):
p = promise.Promise.Resolve(0)
self.assertEquals(p.state, promise.Promise.STATE_FULLFILLED)
q = promise.Promise.Resolve(p)
self.assertEquals(p.state, promise.Promise.STATE_FULLFILLED)
q.Then(self._AddToAccumulated)
self.assertEquals(self.accumulated, [0])
def testReject(self):
p = promise.Promise.Reject(0)
self.assertEquals(p.state, promise.Promise.STATE_REJECTED)
p.Then(onRejected=self._AddToAccumulated)
self.assertEquals(self.accumulated, [0])
def testGeneratorFunctionResolve(self):
(p, resolve, _) = _GetPromiseAndFunctions()
self.assertEquals(p.state, promise.Promise.STATE_PENDING)
p.Then(self._AddToAccumulated)
resolve(0)
self.assertEquals(p.state, promise.Promise.STATE_FULLFILLED)
self.assertEquals(self.accumulated, [0])
def testGeneratorFunctionReject(self):
(p, _, reject) = _GetPromiseAndFunctions()
self.assertEquals(p.state, promise.Promise.STATE_PENDING)
p.Then(None, self._AddToAccumulated)
reject(0)
self.assertEquals(p.state, promise.Promise.STATE_REJECTED)
self.assertEquals(self.accumulated, [0])
def testGeneratorFunctionResolveToPromise(self):
(p1, resolve, _) = _GetPromiseAndFunctions()
p2 = promise.Promise(lambda x, y: x(p1))
self.assertEquals(p2.state, promise.Promise.STATE_PENDING)
p2.Then(self._AddToAccumulated)
resolve(promise.Promise.Resolve(0))
self.assertEquals(self.accumulated, [0])
def testComputation(self):
(p, resolve, _) = _GetPromiseAndFunctions()
p.Then(lambda x: x+1).Then(lambda x: x+2).Then(self._AddToAccumulated)
self.assertEquals(self.accumulated, [])
resolve(0)
self.assertEquals(self.accumulated, [3])
def testRecoverAfterException(self):
(p, resolve, _) = _GetPromiseAndFunctions()
p.Then(_ThrowException).Catch(self._AddToAccumulated)
self.assertEquals(self.accumulated, [])
resolve(0)
self.assertEquals(len(self.accumulated), 1)
self.assertIsInstance(self.accumulated[0], RuntimeError)
self.assertEquals(self.accumulated[0].message, 0)
def testMultipleRejectResolve(self):
(p, resolve, reject) = _GetPromiseAndFunctions()
p.Then(self._AddToAccumulated, self._AddToAccumulated)
resolve(0)
self.assertEquals(self.accumulated, [0])
resolve(0)
self.assertEquals(self.accumulated, [0])
reject(0)
self.assertEquals(self.accumulated, [0])
self.accumulated = []
(p, resolve, reject) = _GetPromiseAndFunctions()
p.Then(self._AddToAccumulated, self._AddToAccumulated)
reject(0)
self.assertEquals(self.accumulated, [0])
resolve(0)
self.assertEquals(self.accumulated, [0])
reject(0)
self.assertEquals(self.accumulated, [0])
def testAll(self):
promises_and_functions = [_GetPromiseAndFunctions() for x in xrange(10)]
promises = [x[0] for x in promises_and_functions]
all_promise = promise.Promise.All(*promises)
res = []
def AddToRes(values):
res.append(values)
all_promise.Then(AddToRes, AddToRes)
for i, (_, resolve, _) in enumerate(promises_and_functions):
self.assertEquals(len(res), 0)
resolve(i)
self.assertEquals(len(res), 1)
self.assertEquals(res[0], [i for i in xrange(10)])
self.assertEquals(all_promise.state, promise.Promise.STATE_FULLFILLED)
def testAllFailure(self):
promises_and_functions = [_GetPromiseAndFunctions() for x in xrange(10)]
promises = [x[0] for x in promises_and_functions]
all_promise = promise.Promise.All(*promises)
res = []
def AddToRes(values):
res.append(values)
all_promise.Then(AddToRes, AddToRes)
for i in xrange(10):
if i <= 5:
self.assertEquals(len(res), 0)
else:
self.assertEquals(len(res), 1)
if i != 5:
promises_and_functions[i][1](i)
else:
promises_and_functions[i][2]('error')
self.assertEquals(len(res), 1)
self.assertEquals(res[0], 'error')
self.assertEquals(all_promise.state, promise.Promise.STATE_REJECTED)
def testRace(self):
promises_and_functions = [_GetPromiseAndFunctions() for x in xrange(10)]
promises = [x[0] for x in promises_and_functions]
race_promise = promise.Promise.Race(*promises)
res = []
def AddToRes(values):
res.append(values)
race_promise.Then(AddToRes, AddToRes)
self.assertEquals(len(res), 0)
promises_and_functions[7][1]('success')
self.assertEquals(len(res), 1)
for i, (f) in enumerate(promises_and_functions):
f[1 + (i % 2)](i)
self.assertEquals(len(res), 1)
self.assertEquals(res[0], 'success')
self.assertEquals(race_promise.state, promise.Promise.STATE_FULLFILLED)
def testRaceFailure(self):
promises_and_functions = [_GetPromiseAndFunctions() for x in xrange(10)]
promises = [x[0] for x in promises_and_functions]
race_promise = promise.Promise.Race(*promises)
res = []
def AddToRes(values):
res.append(values)
race_promise.Then(AddToRes, AddToRes)
self.assertEquals(len(res), 0)
promises_and_functions[7][2]('error')
self.assertEquals(len(res), 1)
for i, (f) in enumerate(promises_and_functions):
f[1 + (i % 2)](i)
self.assertEquals(len(res), 1)
self.assertEquals(res[0], 'error')
self.assertEquals(race_promise.state, promise.Promise.STATE_REJECTED)
def _GetPromiseAndFunctions():
functions = {}
def GeneratorFunction(resolve, reject):
functions['resolve'] = resolve
functions['reject'] = reject
p = promise.Promise(GeneratorFunction)
return (p, functions['resolve'], functions['reject'])
def _ThrowException(x):
raise RuntimeError(x)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment