diff --git a/.travis.yml b/.travis.yml index a8aecf1..3bc973c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,11 @@ cache: pip install: - pip install pytest pytest-cov coveralls flake8 - pip install futures +- | + if python -c 'import sys; sys.exit(1 if sys.hexversion<0x03000000 else 0)' + then + pip install pytest-asyncio + fi - pip install -e . script: - flake8 diff --git a/README.md b/README.md index 8413a7a..5843fc4 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ This creates and returns a new promise. `resolver` must be a function. The `re 1. `resolve` should be called with a single argument. If it is called with a non-promise value then the promise is fulfilled with that value. If it is called with a promise (A) then the returned promise takes on the state of that new promise (A). 2. `reject` should be called with a single argument. The returned promise will be rejected with that argument. -### Static Functions +### Class Methods These methods are invoked by calling `Promise.methodName`. @@ -56,7 +56,7 @@ This creates and returns a new promise. `resolver` must be a function. The `re Converts values and foreign promises into Promises/A+ promises. If you pass it a value then it returns a Promise for that value. If you pass it something that is close to a promise (such as a jQuery attempt at a promise) it returns a Promise that takes on the state of `value` (rejected or fulfilled). -#### Promise.reject(value) +#### Promise.rejected(value) Returns a rejected promise with the given value. @@ -71,6 +71,20 @@ p = Promise.all([Promise.resolve('a'), 'b', Promise.resolve('c')]) \ assert p.value is True ``` +#### Promise.promisify(obj) + +This function wraps the `obj` act as a `Promise` if possible. +Python `Future`s are supported, with a callback to `promise.done` when resolved. + + +#### Promise.for_dict(d) + +A special function that takes a dictionary of promises and turns them +into a promise for a dictionary of values. In other words, this turns +an dictionary of promises for values into a promise for a dictionary +of values. + + ### Instance Methods These methods are invoked on a promise instance by calling `myPromise.methodName` @@ -100,12 +114,6 @@ The same semantics as `.then` except that it does not return a promise and any e This function checks if the `obj` is a `Promise`, or could be `promisify`ed. -### promisify(obj) - -This function wraps the `obj` act as a `Promise` if possible. -Python `Future`s are supported, with a callback to `promise.done` when resolved. - - # Notes This package is heavily insipired in [aplus](https://github.com/xogeny/aplus). diff --git a/README.rst b/README.rst index 8d17443..f8ce263 100644 --- a/README.rst +++ b/README.rst @@ -57,8 +57,8 @@ The ``resolver`` function is passed two arguments: 2. ``reject`` should be called with a single argument. The returned promise will be rejected with that argument. -Static Functions -~~~~~~~~~~~~~~~~ +Class Methods +~~~~~~~~~~~~~ These methods are invoked by calling ``Promise.methodName``. @@ -71,8 +71,8 @@ something that is close to a promise (such as a jQuery attempt at a promise) it returns a Promise that takes on the state of ``value`` (rejected or fulfilled). -Promise.reject(value) -^^^^^^^^^^^^^^^^^^^^^ +Promise.rejected(value) +^^^^^^^^^^^^^^^^^^^^^^^ Returns a rejected promise with the given value. @@ -90,6 +90,21 @@ replaced by their fulfilled values. e.g. assert p.value is True +Promise.promisify(obj) +^^^^^^^^^^^^^^^^^^^^^^ + +This function wraps the ``obj`` act as a ``Promise`` if possible. Python +``Future``\ s are supported, with a callback to ``promise.done`` when +resolved. + +Promise.for\_dict(d) +^^^^^^^^^^^^^^^^^^^^ + +A special function that takes a dictionary of promises and turns them +into a promise for a dictionary of values. In other words, this turns an +dictionary of promises for values into a promise for a dictionary of +values. + Instance Methods ~~~~~~~~~~~~~~~~ @@ -141,13 +156,6 @@ is\_thenable(obj) This function checks if the ``obj`` is a ``Promise``, or could be ``promisify``\ ed. -promisify(obj) -~~~~~~~~~~~~~~ - -This function wraps the ``obj`` act as a ``Promise`` if possible. Python -``Future``\ s are supported, with a callback to ``promise.done`` when -resolved. - Notes ===== diff --git a/promise.py b/promise.py index 8136beb..3161945 100644 --- a/promise.py +++ b/promise.py @@ -59,6 +59,12 @@ def __init__(self, fn=None): if fn: self.do_resolve(fn) + def __iter__(self): + yield self.get() + + def __await__(self): + yield self.get() + def do_resolve(self, fn): self._done = False @@ -78,15 +84,15 @@ def reject_fn(x): except Exception as e: self.reject(e) - @staticmethod - def fulfilled(x): - p = Promise() + @classmethod + def fulfilled(cls, x): + p = cls() p.fulfill(x) return p - @staticmethod - def rejected(reason): - p = Promise() + @classmethod + def rejected(cls, reason): + p = cls() p.reject(reason) return p @@ -99,7 +105,7 @@ def fulfill(self, x): raise TypeError("Cannot resolve promise with itself.") elif is_thenable(x): try: - promisify(x).done(self.fulfill, self.reject) + self.promisify(x).done(self.fulfill, self.reject) except Exception as e: self.reject(e) else: @@ -109,7 +115,7 @@ def fulfill(self, x): def _fulfill(self, value): with self._cb_lock: - if self._state != Promise.PENDING: + if self._state != self.PENDING: return self._value = value @@ -141,7 +147,7 @@ def reject(self, reason): assert isinstance(reason, Exception) with self._cb_lock: - if self._state != Promise.PENDING: + if self._state != self.PENDING: return self._reason = reason @@ -321,7 +327,7 @@ def then(self, success=None, failure=None): :type failure: (object) -> object :rtype : Promise """ - ret = Promise() + ret = self.__class__() def call_and_fulfill(v): """ @@ -384,8 +390,8 @@ def then_all(self, *handlers): return promises - @staticmethod - def all(values_or_promises): + @classmethod + def all(cls, values_or_promises): """ A special function that takes a bunch of promises and turns them into a promise for a vector of values. @@ -395,9 +401,9 @@ def all(values_or_promises): promises = list(filter(is_thenable, values_or_promises)) if len(promises) == 0: # All the values or promises are resolved - return Promise.fulfilled(values_or_promises) + return cls.fulfilled(values_or_promises) - all_promise = Promise() + all_promise = cls() counter = CountdownLatch(len(promises)) def handleSuccess(_): @@ -406,10 +412,52 @@ def handleSuccess(_): all_promise.fulfill(values) for p in promises: - promisify(p).done(handleSuccess, all_promise.reject) + cls.promisify(p).done(handleSuccess, all_promise.reject) return all_promise + @classmethod + def promisify(cls, obj): + if isinstance(obj, cls): + return obj + elif is_future(obj): + promise = cls() + obj.add_done_callback(_process_future_result(promise)) + return promise + elif hasattr(obj, "done") and callable(getattr(obj, "done")): + p = cls() + obj.done(p.fulfill, p.reject) + return p + elif hasattr(obj, "then") and callable(getattr(obj, "then")): + p = cls() + obj.then(p.fulfill, p.reject) + return p + else: + raise TypeError("Object is not a Promise like object.") + + @classmethod + def for_dict(cls, m): + """ + A special function that takes a dictionary of promises + and turns them into a promise for a dictionary of values. + In other words, this turns an dictionary of promises for values + into a promise for a dictionary of values. + """ + if not m: + return cls.fulfilled({}) + + keys, values = zip(*m.items()) + dict_type = type(m) + + def handleSuccess(resolved_values): + return dict_type(zip(keys, resolved_values)) + + return cls.all(values).then(handleSuccess) + + +promisify = Promise.promisify +promise_for_dict = Promise.for_dict + def _process_future_result(promise): def handle_future_result(future): @@ -434,41 +482,3 @@ def is_thenable(obj): return isinstance(obj, Promise) or is_future(obj) or ( hasattr(obj, "done") and callable(getattr(obj, "done"))) or ( hasattr(obj, "then") and callable(getattr(obj, "then"))) - - -def promisify(obj): - if isinstance(obj, Promise): - return obj - elif is_future(obj): - promise = Promise() - obj.add_done_callback(_process_future_result(promise)) - return promise - elif hasattr(obj, "done") and callable(getattr(obj, "done")): - p = Promise() - obj.done(p.fulfill, p.reject) - return p - elif hasattr(obj, "then") and callable(getattr(obj, "then")): - p = Promise() - obj.then(p.fulfill, p.reject) - return p - else: - raise TypeError("Object is not a Promise like object.") - - -def promise_for_dict(m): - """ - A special function that takes a dictionary of promises - and turns them into a promise for a dictionary of values. - In other words, this turns an dictionary of promises for values - into a promise for a dictionary of values. - """ - if not m: - return Promise.fulfilled({}) - - keys, values = zip(*m.items()) - dict_type = type(m) - - def handleSuccess(resolved_values): - return dict_type(zip(keys, resolved_values)) - - return Promise.all(values).then(handleSuccess) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2e18e0f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,7 @@ +import sys + +collect_ignore = [] +if sys.version_info[:2] < (3, 4): + collect_ignore.append('test_awaitable.py') +if sys.version_info[:2] < (3, 5): + collect_ignore.append('test_awaitable_35.py') diff --git a/tests/test_awaitable.py b/tests/test_awaitable.py new file mode 100644 index 0000000..5f068ce --- /dev/null +++ b/tests/test_awaitable.py @@ -0,0 +1,9 @@ +import asyncio +import pytest +from promise import Promise + + +@pytest.mark.asyncio +@asyncio.coroutine +def test_await(): + yield from Promise.resolve(None) diff --git a/tests/test_awaitable_35.py b/tests/test_awaitable_35.py new file mode 100644 index 0000000..a64b78c --- /dev/null +++ b/tests/test_awaitable_35.py @@ -0,0 +1,7 @@ +import pytest +from promise import Promise + + +@pytest.mark.asyncio +async def test_await(): + await Promise.resolve(None) diff --git a/tests/test_extra.py b/tests/test_extra.py index 41f3037..2e60a94 100644 --- a/tests/test_extra.py +++ b/tests/test_extra.py @@ -3,7 +3,12 @@ import time import pytest -from promise import Promise, promise_for_dict, is_thenable, promisify +from promise import ( + Promise, + is_thenable, + promisify as free_promisify, + promise_for_dict as free_promise_for_dict, +) from concurrent.futures import Future from threading import Thread @@ -237,7 +242,15 @@ def test_promise_all_if(): # promise_for_dict -def test_dict_promise_when(): +@pytest.fixture(params=[ + Promise.for_dict, + free_promise_for_dict, +]) +def promise_for_dict(request): + return request.param + + +def test_dict_promise_when(promise_for_dict): p1 = Promise() p2 = Promise() d = {"a": p1, "b": p2} @@ -267,7 +280,7 @@ def test_dict_promise_when(): assert {} == pd3.value -def test_dict_promise_if(): +def test_dict_promise_if(promise_for_dict): p1 = Promise() p2 = Promise() d = {"a": p1, "b": p2} @@ -412,26 +425,31 @@ def test_is_thenable_simple_object(): assert not is_thenable(object()) -def test_promisify_promise(): +@pytest.fixture(params=[free_promisify, Promise.promisify]) +def promisify(request): + return request.param + + +def test_promisify_promise(promisify): promise = Promise() assert promisify(promise) == promise -def test_promisify_then_object(): +def test_promisify_then_object(promisify): promise = FakeThenPromise() with pytest.raises(Exception) as excinfo: promisify(promise) assert str(excinfo.value) == "FakeThenPromise raises in 'then'" -def test_promisify_done_object(): +def test_promisify_done_object(promisify): promise = FakeDonePromise() with pytest.raises(Exception) as excinfo: promisify(promise) assert str(excinfo.value) == "FakeDonePromise raises in 'done'" -def test_promisify_future(): +def test_promisify_future(promisify): future = Future() promise = promisify(future) assert promise.is_pending @@ -440,7 +458,7 @@ def test_promisify_future(): assert promise.value == 1 -def test_promisify_future_rejected(): +def test_promisify_future_rejected(promisify): future = Future() promise = promisify(future) assert promise.is_pending @@ -449,7 +467,18 @@ def test_promisify_future_rejected(): assert_exception(promise.reason, Exception, 'Future rejected') -def test_promisify_object(): +def test_promisify_object(promisify): with pytest.raises(TypeError) as excinfo: promisify(object()) assert str(excinfo.value) == "Object is not a Promise like object." + + +def test_promisify_promise_subclass(): + class MyPromise(Promise): + pass + + p = Promise() + p.fulfill(10) + m_p = MyPromise.promisify(p) + assert isinstance(m_p, MyPromise) + assert m_p.get() == p.get()