Skip to content

Commit

Permalink
Merge 3057234 into b055d1e
Browse files Browse the repository at this point in the history
  • Loading branch information
syrusakbary committed Jun 22, 2018
2 parents b055d1e + 3057234 commit 996a61c
Show file tree
Hide file tree
Showing 15 changed files with 376 additions and 154 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,8 @@ target/

# OS X
.DS_Store
/.vscode
/.pytest_cache
/.pyre
/.mypy_cache
/type_info.json
18 changes: 2 additions & 16 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,6 @@ python:
- 3.5
- 3.6
- pypy
before_install:
- |
if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
export PYENV_ROOT="$HOME/.pyenv"
if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
cd "$PYENV_ROOT" && git pull
else
rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
fi
export PYPY_VERSION="4.0.1"
"$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION"
virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
fi
cache: pip
install:
- pip install -e .[test]
Expand All @@ -34,5 +20,5 @@ matrix:
script: flake8
- python: '3.5'
script: |
pip install mypy
mypy promise/ --check-untyped-defs --ignore-missing-imports
pip install pyre-check
pyre --source-directory promise check
57 changes: 39 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ Its fully compatible with the [Promises/A+ spec](http://promises-aplus.github.io

$ pip install promise


## Usage

The example below shows how you can load the promise library. It then demonstrates creating a promise from scratch. You simply call `Promise(fn)`. There is a complete specification for what is returned by this method in [Promises/A+](http://promises-aplus.github.com/promises-spec/).
The example below shows how you can load the promise library. It then demonstrates creating a promise from scratch. You simply call `Promise(fn)`. There is a complete specification for what is returned by this method in [Promises/A+](http://promises-aplus.github.com/promises-spec/).

```python
from promise import Promise
Expand All @@ -43,26 +42,26 @@ from promise import Promise

### Promise(resolver)

This creates and returns a new promise. `resolver` must be a function. The `resolver` function is passed two arguments:
This creates and returns a new promise. `resolver` must be a function. The `resolver` function is passed two arguments:

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.
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.

### Class Methods

These methods are invoked by calling `Promise.methodName`.
These methods are invoked by calling `Promise.methodName`.

#### Promise.resolve(value)

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).
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)

Returns a rejected promise with the given value.

#### Promise.all(list)

Returns a promise for a list. If it is called with a single argument then this returns a promise for a copy of that list with any promises replaced by their fulfilled values. e.g.
Returns a promise for a list. If it is called with a single argument then this returns a promise for a copy of that list with any promises replaced by their fulfilled values. e.g.

```python
p = Promise.all([Promise.resolve('a'), 'b', Promise.resolve('c')]) \
Expand All @@ -77,38 +76,34 @@ This function wraps the `obj` act as a `Promise` if possible.
Python `Future`s are supported, with a callback to `promise.done` when resolved.
Have the same effects as `Promise.resolve(obj)`.


#### 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
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.


#### Promise.is_thenable(obj)

This function checks if the `obj` is a `Promise`, or could be `cast`ed.


#### Promise.promisify(func)

This function wraps the result of calling `func` in a `Promise` instance.


### Instance Methods

These methods are invoked on a promise instance by calling `myPromise.methodName`

### promise.then(did_fulfill, did_reject)

This method follows the [Promises/A+ spec](http://promises-aplus.github.io/promises-spec/). It explains things very clearly so I recommend you read it.
This method follows the [Promises/A+ spec](http://promises-aplus.github.io/promises-spec/). It explains things very clearly so I recommend you read it.

Either `did_fulfill` or `did_reject` will be called and they will not be called more than once. They will be passed a single argument and will always be called asynchronously (in the next turn of the event loop).
Either `did_fulfill` or `did_reject` will be called and they will not be called more than once. They will be passed a single argument and will always be called asynchronously (in the next turn of the event loop).

If the promise is fulfilled then `did_fulfill` is called. If the promise is rejected then `did_reject` is called.
If the promise is fulfilled then `did_fulfill` is called. If the promise is rejected then `did_reject` is called.

The call to `.then` also returns a promise. If the handler that is called returns a promise, the promise returned by `.then` takes on the state of that returned promise. If the handler that is called returns a value that is not a promise, the promise returned by `.then` will be fulfilled with that value. If the handler that is called throws an exception then the promise returned by `.then` is rejected with that exception.
The call to `.then` also returns a promise. If the handler that is called returns a promise, the promise returned by `.then` takes on the state of that returned promise. If the handler that is called returns a value that is not a promise, the promise returned by `.then` will be fulfilled with that value. If the handler that is called throws an exception then the promise returned by `.then` is rejected with that exception.

#### promise.catch(did_reject)

Expand All @@ -118,7 +113,6 @@ Sugar for `promise.then(None, did_reject)`, to mirror `catch` in synchronous cod

The same semantics as `.then` except that it does not return a promise and any exceptions are re-thrown so that they can be logged (crashing the application in non-browser environments)


# Contributing

After cloning this repo, ensure dependencies are installed by running:
Expand All @@ -139,6 +133,33 @@ You can also run the benchmarks with:
py.test tests --benchmark-only
```

## Generate Annotations

Python typing annotations are very useful for making sure we use the libary the way is intended.

You can autogenerate the types with:

```sh
pip install pyannotate
py.test tests
pyannotate -w # This will replace the files
```

And then, run the static type checker

With `mypy`

```sh
pip install mypy
mypy promise --ignore-missing-imports
```

with `pyre`:

```sh
pip install pyre-check
pyre --source-directory promise check
```

# Notes

Expand Down
30 changes: 30 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Configuration for pytest to automatically collect types.
# Thanks to Guilherme Salgado.
import pytest

try:
import pyannotate_runtime
PYANOTATE_PRESENT = True
except ImportError:
PYANOTATE_PRESENT = False

if PYANOTATE_PRESENT:
def pytest_collection_finish(session):
"""Handle the pytest collection finish hook: configure pyannotate.
Explicitly delay importing `collect_types` until all tests have
been collected. This gives gevent a chance to monkey patch the
world before importing pyannotate.
"""
from pyannotate_runtime import collect_types
collect_types.init_types_collection()

@pytest.fixture(autouse=True)
def collect_types_fixture():
from pyannotate_runtime import collect_types
collect_types.resume()
yield
collect_types.pause()

def pytest_sessionfinish(session, exitstatus):
from pyannotate_runtime import collect_types
collect_types.dump_stats("type_info.json")
59 changes: 36 additions & 23 deletions promise/async_.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# Based on https://github.com/petkaantonov/bluebird/blob/master/src/promise.js
import collections
from collections import deque
if False:
from .promise import Promise
from typing import Callable, Optional, Union # flake8: noqa


class Async(object):

def __init__(self, schedule):
def __init__(self, trampoline_enabled=True):
self.is_tick_used = False
self.late_queue = collections.deque() # type: ignore
self.normal_queue = collections.deque() # type: ignore
self.late_queue = deque() # type: ignore
self.normal_queue = deque() # type: ignore
self.have_drained_queues = False
self.trampoline_enabled = True
self.schedule = schedule
self.trampoline_enabled = trampoline_enabled

def enable_trampoline(self):
self.trampoline_enabled = True
Expand All @@ -21,49 +23,56 @@ def disable_trampoline(self):
def have_items_queued(self):
return self.is_tick_used or self.have_drained_queues

def _async_invoke_later(self, fn):
def _async_invoke_later(self, fn, scheduler):
self.late_queue.append(fn)
self.queue_tick()
self.queue_tick(scheduler)

def _async_invoke(self, fn):
def _async_invoke(self, fn, scheduler):
# type: (Callable, Any) -> None
self.normal_queue.append(fn)
self.queue_tick()
self.queue_tick(scheduler)

def _async_settle_promise(self, promise):
# type: (Promise) -> None
self.normal_queue.append(promise)
self.queue_tick()
self.queue_tick(promise.scheduler)

def invoke_later(self, fn):
if self.trampoline_enabled:
self._async_invoke_later(fn)
self._async_invoke_later(fn, scheduler)
else:
self.schedule.call_later(0.1, fn)
scheduler.call_later(0.1, fn)

def invoke(self, fn):
def invoke(self, fn, scheduler):
# type: (Callable, Any) -> None
if self.trampoline_enabled:
self._async_invoke(fn)
self._async_invoke(fn, scheduler)
else:
self.schedule.call(
scheduler.call(
fn
)

def settle_promises(self, promise):
# type: (Promise) -> None
if self.trampoline_enabled:
self._async_settle_promise(promise)
else:
self.schedule.call(
promise.scheduler.call(
promise._settle_promises
)

def throw_later(self, reason):
def throw_later(self, reason, scheduler):
# type: (Exception, Any) -> None
def fn():
# type: () -> None
raise reason

self.schedule.call(fn)
scheduler.call(fn)

fatal_error = throw_later

def drain_queue(self, queue):
# type: (deque) -> None
from .promise import Promise
while queue:
fn = queue.popleft()
Expand All @@ -73,6 +82,7 @@ def drain_queue(self, queue):
fn()

def drain_queue_until_resolved(self, promise):
# type: (Promise) -> None
from .promise import Promise
queue = self.normal_queue
while queue:
Expand All @@ -89,6 +99,7 @@ def drain_queue_until_resolved(self, promise):
self.drain_queue(self.late_queue)

def wait(self, promise, timeout=None):
# type: (Promise, Optional[float]) -> None
if not promise.is_pending:
# We return if the promise is already
# fulfilled or rejected
Expand All @@ -104,20 +115,22 @@ def wait(self, promise, timeout=None):
# We return if the promise is already
# fulfilled or rejected
return

self.schedule.wait(target, timeout)
target.scheduler.wait(target, timeout)

def drain_queues(self):
# type: () -> None
assert self.is_tick_used
self.drain_queue(self.normal_queue)
self.reset()
self.have_drained_queues = True
self.drain_queue(self.late_queue)

def queue_tick(self):
def queue_tick(self, scheduler):
# type: (Any) -> None
if not self.is_tick_used:
self.is_tick_used = True
self.schedule.call(self.drain_queues)
scheduler.call(self.drain_queues)

def reset(self):
# type: () -> None
self.is_tick_used = False
2 changes: 1 addition & 1 deletion promise/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ def iscoroutine(obj): # type: ignore
from .iterate_promise import iterate_promise
except (SyntaxError, ImportError):

def iterate_promise(promise):
def iterate_promise(promise): # type: ignore
raise Exception(
'You need "yield from" syntax for iterate in a Promise.')
Loading

0 comments on commit 996a61c

Please sign in to comment.