diff --git a/.gitignore b/.gitignore index 04f0af5..4ff83cd 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,8 @@ target/ # OS X .DS_Store +/.vscode +/.pytest_cache +/.pyre +/.mypy_cache +/type_info.json diff --git a/.travis.yml b/.travis.yml index d5478cf..ca0ef07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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] @@ -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 diff --git a/README.md b/README.md index 1ad5a8a..9d203ca 100644 --- a/README.md +++ b/README.md @@ -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 @@ -43,18 +42,18 @@ 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) @@ -62,7 +61,7 @@ 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')]) \ @@ -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) @@ -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: @@ -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 diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..a36e6ba --- /dev/null +++ b/conftest.py @@ -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") diff --git a/promise/async_.py b/promise/async_.py index 5bb6bef..2dcae67 100644 --- a/promise/async_.py +++ b/promise/async_.py @@ -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 @@ -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() @@ -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: @@ -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 @@ -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 diff --git a/promise/compat.py b/promise/compat.py index 9b0b4ed..199eab9 100644 --- a/promise/compat.py +++ b/promise/compat.py @@ -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.') diff --git a/promise/dataloader.py b/promise/dataloader.py index b0932cc..e8b5763 100644 --- a/promise/dataloader.py +++ b/promise/dataloader.py @@ -1,12 +1,15 @@ from collections import Iterable, namedtuple from functools import partial -from typing import List, Sized # flake8: noqa - -from .promise import Promise, async_instance +from .promise import Promise, async_instance, get_default_scheduler +if False: + from typing import ( + List, Sized, Callable, Optional, Tuple, Union, Iterator, Hashable + ) # flake8: noqa def get_chunks(iterable_obj, chunk_size=1): + # type: (List[Loader], int) -> Iterator chunk_size = max(1, chunk_size) return (iterable_obj[i:i + chunk_size] for i in range(0, len(iterable_obj), chunk_size)) @@ -20,7 +23,16 @@ class DataLoader(object): max_batch_size = None # type: int cache = True - def __init__(self, batch_load_fn=None, batch=None, max_batch_size=None, cache=None, get_cache_key=None, cache_map=None): + def __init__(self, + batch_load_fn=None, # type: Callable + batch=None, # type: Optional[Any] + max_batch_size=None, # type: Optional[int] + cache=None, # type: Optional[Any] + get_cache_key=None, # type: Optional[Any] + cache_map=None, # type: Optional[Any] + scheduler=None, # type: Optional[Any] + ): + # type: (...) -> None if batch_load_fn is not None: self.batch_load_fn = batch_load_fn @@ -45,11 +57,13 @@ def __init__(self, batch_load_fn=None, batch=None, max_batch_size=None, cache=No self._promise_cache = cache_map or {} self._queue = [] # type: List[Loader] + self._scheduler = scheduler def get_cache_key(self, key): # type: ignore return key def load(self, key=None): + # type: (Hashable) -> Promise ''' Loads a key, returning a `Promise` for the value represented by that key. ''' @@ -68,7 +82,8 @@ def load(self, key=None): return cached_promise # Otherwise, produce a new Promise for this value. - promise = Promise(partial(self.do_resolve_reject, key)) + + promise = Promise(partial(self.do_resolve_reject, key)) # type: ignore # If caching, cache this promise. if self.cache: @@ -77,6 +92,7 @@ def load(self, key=None): return promise def do_resolve_reject(self, key, resolve, reject): + # type: (Hashable, Callable, Callable) -> None # Enqueue this Promise to be dispatched. self._queue.append(Loader( key=key, @@ -89,12 +105,14 @@ def do_resolve_reject(self, key, resolve, reject): if len(self._queue) == 1: if self.batch: # If batching, schedule a task to dispatch the queue. - enqueue_post_promise_job(partial(dispatch_queue, self)) + enqueue_post_promise_job( + partial(dispatch_queue, self), self._scheduler) else: # Otherwise dispatch the (queue of one) immediately. dispatch_queue(self) def load_many(self, keys): + # type: (Iterable[Hashable]) -> Promise ''' Loads multiple keys, promising an array of values @@ -116,6 +134,7 @@ def load_many(self, keys): return Promise.all([self.load(key) for key in keys]) def clear(self, key): + # type: (Hashable) -> DataLoader ''' Clears the value at `key` from the cache, if it exists. Returns itself for method chaining. @@ -125,6 +144,7 @@ def clear(self, key): return self def clear_all(self): + # type: () -> DataLoader ''' Clears the entire cache. To be used when some event results in unknown invalidations across this particular `DataLoader`. Returns itself for @@ -134,6 +154,7 @@ def clear_all(self): return self def prime(self, key, value): + # type: (Hashable, Any) -> DataLoader ''' Adds the provied key and value to the cache. If the key already exists, no change is made. Returns itself for method chaining. @@ -179,14 +200,20 @@ def prime(self, key, value): # Private: cached resolved Promise instance resolved_promise = None -def enqueue_post_promise_job(fn): + +def enqueue_post_promise_job(fn, scheduler): + # type: (Callable, Any) -> None global resolved_promise if not resolved_promise: resolved_promise = Promise.resolve(None) - resolved_promise.then(lambda v: async_instance.invoke(fn)) + if not scheduler: + scheduler = get_default_scheduler() + resolved_promise.then(lambda v: async_instance.invoke( + fn, scheduler)) def dispatch_queue(loader): + # type: (DataLoader) -> None ''' Given the current state of a Loader instance, perform a batch load from its current queue. @@ -211,6 +238,7 @@ def dispatch_queue(loader): def dispatch_queue_batch(loader, queue): + # type: (DataLoader, List[Loader]) -> None # Collect all keys to be loaded in this dispatch keys = [l.key for l in queue] @@ -218,15 +246,17 @@ def dispatch_queue_batch(loader, queue): try: batch_promise = loader.batch_load_fn(keys) except Exception as e: - return failed_dispatch( + failed_dispatch( loader, queue, - Exception("Data loader batch_load_fn function raised an Exception: {}".format(repr(e))) + Exception( + "Data loader batch_load_fn function raised an Exception: {}".format(repr(e))) ) + return None # Assert the expected response from batch_load_fn if not batch_promise or not isinstance(batch_promise, Promise): - return failed_dispatch( + failed_dispatch( loader, queue, TypeError(( @@ -235,6 +265,7 @@ def dispatch_queue_batch(loader, queue): 'not return a Promise: {}.' ).format(batch_promise)) ) + return None def batch_promise_resolved(values): # type: (Sized) -> None @@ -264,10 +295,12 @@ def batch_promise_resolved(values): else: l.resolve(value) - batch_promise.then(batch_promise_resolved).catch(partial(failed_dispatch, loader, queue)) + batch_promise.then(batch_promise_resolved).catch( + partial(failed_dispatch, loader, queue)) def failed_dispatch(loader, queue, error): + # type: (DataLoader, Iterable[Loader], Exception) -> None ''' Do not cache individual loads if the entire batch dispatch fails, but still reject each request so they do not hang. diff --git a/promise/iterate_promise.py b/promise/iterate_promise.py index aeb7d36..ba64f9c 100644 --- a/promise/iterate_promise.py +++ b/promise/iterate_promise.py @@ -1,8 +1,12 @@ # flake8: noqa +if False: + from .promise import Promise + from typing import Iterator def iterate_promise(promise): + # type: (Promise) -> Iterator if not promise.is_fulfilled: - yield from promise.future + yield from promise.future # type: ignore assert promise.is_fulfilled return promise.get() diff --git a/promise/promise.py b/promise/promise.py index a23f427..7e6371d 100644 --- a/promise/promise.py +++ b/promise/promise.py @@ -4,33 +4,40 @@ from threading import RLock from types import TracebackType -from six import reraise -from typing import (List, Any, Callable, Dict, Iterator, Optional, # flake8: noqa - Union) - +from six import reraise # type: ignore from .async_ import Async from .compat import (Future, ensure_future, iscoroutine, # type: ignore - iterate_promise) + iterate_promise) # type: ignore from .utils import deprecated, integer_types, string_types, text_type, binary_type, warn from .promise_list import PromiseList from .schedulers.immediate import ImmediateScheduler +from typing import TypeVar, Generic # from .schedulers.gevent import GeventScheduler # from .schedulers.asyncio import AsyncioScheduler # from .schedulers.thread import ThreadScheduler -async_instance = Async(ImmediateScheduler()) +if False: + from typing import (List, Any, Callable, Dict, Iterator, Optional, # flake8: noqa + Tuple, Union, Generic, Hashable) + + +default_scheduler = ImmediateScheduler() + +async_instance = Async() def get_default_scheduler(): - return async_instance.schedule + # type: () -> ImmediateScheduler + return default_scheduler def set_default_scheduler(scheduler): - async_instance.schedule = scheduler + global default_scheduler + default_scheduler = scheduler IS_PYTHON2 = version_info[0] == 2 -DEFAULT_TIMEOUT = None # type: float +DEFAULT_TIMEOUT = None # type: Optional[float] MAX_LENGTH = 0xFFFF | 0 CALLBACK_SIZE = 3 @@ -57,10 +64,15 @@ def set_default_scheduler(scheduler): def make_self_resolution_error(): + # type: () -> TypeError return TypeError("Promise is self") -def try_catch(handler, *args, **kwargs): +def try_catch(handler, # type: Callable + *args, # type: Any + **kwargs # type: Any + ): + # type: (...) -> Union[Tuple[Any, None], Tuple[None, Tuple[Exception, Optional[TracebackType]]]] try: return (handler(*args, **kwargs), None) except Exception as e: @@ -68,7 +80,10 @@ def try_catch(handler, *args, **kwargs): return (None, (e, tb)) -class Promise(object): +T = TypeVar('T') + + +class Promise(Generic[T]): """ This is the Promise class that complies Promises/A+ specification. @@ -85,17 +100,18 @@ class Promise(object): _is_following = False _is_async_guaranteed = False _length = 0 - _handlers = None # type: Dict[int, Union[Callable, None]] - _fulfillment_handler0 = None # type: Union[Callable, partial] - _rejection_handler0 = None # type: Union[Callable, partial] - _promise0 = None # type: Promise + _handlers = None # type: Dict[int, Union[Callable, Promise, None]] + _fulfillment_handler0 = None # type: Any + _rejection_handler0 = None # type: Any + _promise0 = None # type: Optional[Promise] _future = None # type: Future - _traceback = None # type: TracebackType + _traceback = None # type: Optional[TracebackType] # _trace = None _is_waiting = False + _scheduler = None - def __init__(self, executor=None): - # type: (Promise, Union[Callable, partial]) -> None + def __init__(self, executor=None, scheduler=None): + # type: (Optional[Callable[[Callable[[T], None], Callable[[Exception], None]], None]], Any) -> None """ Initialize the Promise into a pending state. """ @@ -113,6 +129,8 @@ def __init__(self, executor=None): # self._event_instance = None # type: Event # self._is_waiting = False + self._scheduler = scheduler + if executor is not None: self._resolve_from_executor(executor) @@ -120,16 +138,23 @@ def __init__(self, executor=None): # self.reject = self._deprecated_reject # self.resolve = self._deprecated_resolve + @property + def scheduler(self): + # type: () -> ImmediateScheduler + return self._scheduler or default_scheduler + @property def future(self): # type: (Promise) -> Future if not self._future: - self._future = Future() - self._then(self._future.set_result, self._future.set_exception) + self._future = Future() # type: ignore + self._then(self._future.set_result, # type: ignore + self._future.set_exception) # type: ignore return self._future def __iter__(self): - return iterate_promise(self._target()) + # type: () -> Iterator + return iterate_promise(self._target()) # type: ignore __await__ = __iter__ @@ -150,6 +175,7 @@ def _deprecated_resolve(self, value): self.do_resolve(value) def _resolve_callback(self, value): + # type: (T) -> None if value is self: return self._reject_callback(make_self_resolution_error(), False) @@ -177,6 +203,7 @@ def _resolve_callback(self, value): self._reject(promise._reason(), promise._target()._traceback) def _settled_value(self, _raise=False): + # type: (bool) -> Any assert not self._is_following if self._state == STATE_FULFILLED: @@ -188,6 +215,7 @@ def _settled_value(self, _raise=False): return self._fulfillment_handler0 def _fulfill(self, value): + # type: (T) -> None if value is self: err = make_self_resolution_error() # self._attach_extratrace(err) @@ -202,13 +230,15 @@ def _fulfill(self, value): async_instance.settle_promises(self) def _reject(self, reason, traceback=None): + # type: (Exception, Optional[TracebackType]) -> None self._state = STATE_REJECTED self._fulfillment_handler0 = reason self._traceback = traceback if self._is_final: assert self._length == 0 - return async_instance.fatal_error(reason) + async_instance.fatal_error(reason, self.scheduler) + return if self._length > 0: async_instance.settle_promises(self) @@ -221,11 +251,13 @@ def _reject(self, reason, traceback=None): async_instance.settle_promises(self) def _ensure_possible_rejection_handled(self): + # type: () -> None # self._rejection_is_unhandled = True # async_instance.invoke_later(self._notify_unhandled_rejection, self) pass def _reject_callback(self, reason, synchronous=False, traceback=None): + # type: (Exception, bool, Optional[TracebackType]) -> None assert isinstance( reason, Exception), "A promise was rejected with a non-error: {}".format(reason) # trace = ensure_error_object(reason) @@ -234,6 +266,7 @@ def _reject_callback(self, reason, synchronous=False, traceback=None): self._reject(reason, traceback) def _clear_callback_data_index_at(self, index): + # type: (int) -> None assert not self._is_following assert index > 0 base = index * CALLBACK_SIZE - CALLBACK_SIZE @@ -242,6 +275,7 @@ def _clear_callback_data_index_at(self, index): self._handlers[base + CALLBACK_REJECT_OFFSET] = None def _fulfill_promises(self, length, value): + # type: (int, T) -> None for i in range(1, length): handler = self._fulfillment_handler_at(i) promise = self._promise_at(i) @@ -249,13 +283,20 @@ def _fulfill_promises(self, length, value): self._settle_promise(promise, handler, value, None) def _reject_promises(self, length, reason): + # type: (int, Exception) -> None for i in range(1, length): handler = self._rejection_handler_at(i) promise = self._promise_at(i) self._clear_callback_data_index_at(i) self._settle_promise(promise, handler, reason, None) - def _settle_promise(self, promise, handler, value, traceback): + def _settle_promise(self, + promise, # type: Optional[Promise] + handler, # type: Optional[Callable] + value, # type: Union[T, Exception] + traceback, # type: Optional[TracebackType] + ): + # type: (...) -> None assert not self._is_following is_promise = isinstance(promise, self.__class__) async_guaranteed = self._is_async_guaranteed @@ -264,22 +305,30 @@ def _settle_promise(self, promise, handler, value, traceback): handler(value) # , promise else: if async_guaranteed: - promise._is_async_guaranteed = True - self._settle_promise_from_handler(handler, value, promise) + promise._is_async_guaranteed = True # type: ignore + self._settle_promise_from_handler( # type: ignore + handler, value, promise) # type: ignore elif is_promise: if async_guaranteed: - promise._is_async_guaranteed = True + promise._is_async_guaranteed = True # type: ignore if self._state == STATE_FULFILLED: - promise._fulfill(value) + promise._fulfill(value) # type: ignore else: - promise._reject(value, self._traceback) - - def _settle_promise0(self, handler, value, traceback): + promise._reject(value, self._traceback) # type: ignore + + def _settle_promise0(self, + handler, # type: Optional[Callable] + value, # type: Any + traceback, # type: Optional[TracebackType] + ): + # type: (...) -> None promise = self._promise0 self._promise0 = None - self._settle_promise(promise, handler, value, traceback) + self._settle_promise(promise, handler, value, # type: ignore + traceback) def _settle_promise_from_handler(self, handler, value, promise): + # type: (Callable, Any, Promise) -> None value, error_with_tb = try_catch(handler, value) # , promise if error_with_tb: @@ -289,21 +338,31 @@ def _settle_promise_from_handler(self, handler, value, promise): promise._resolve_callback(value) def _promise_at(self, index): + # type: (int) -> Optional[Promise] assert index > 0 assert not self._is_following - return self._handlers.get(index * CALLBACK_SIZE - CALLBACK_SIZE + CALLBACK_PROMISE_OFFSET) + return self._handlers.get( # type: ignore + index * CALLBACK_SIZE - CALLBACK_SIZE + CALLBACK_PROMISE_OFFSET + ) def _fulfillment_handler_at(self, index): + # type: (int) -> Optional[Callable] assert not self._is_following assert index > 0 - return self._handlers.get(index * CALLBACK_SIZE - CALLBACK_SIZE + CALLBACK_FULFILL_OFFSET) + return self._handlers.get( # type: ignore + index * CALLBACK_SIZE - CALLBACK_SIZE + CALLBACK_FULFILL_OFFSET + ) def _rejection_handler_at(self, index): + # type: (int) -> Optional[Callable] assert not self._is_following assert index > 0 - return self._handlers.get(index * CALLBACK_SIZE - CALLBACK_SIZE + CALLBACK_REJECT_OFFSET) + return self._handlers.get( # type: ignore + index * CALLBACK_SIZE - CALLBACK_SIZE + CALLBACK_REJECT_OFFSET + ) def _migrate_callback0(self, follower): + # type: (Promise) -> None self._add_callbacks( follower._fulfillment_handler0, follower._rejection_handler0, @@ -317,7 +376,12 @@ def _migrate_callback_at(self, follower, index): follower._promise_at(index), ) - def _add_callbacks(self, fulfill, reject, promise): + def _add_callbacks(self, + fulfill, # type: Optional[Callable] + reject, # type: Optional[Callable] + promise, # type: Optional[Promise] + ): + # type: (...) -> int assert not self._is_following if self._handlers is None: @@ -356,22 +420,26 @@ def _add_callbacks(self, fulfill, reject, promise): return index def _target(self): + # type: () -> Promise ret = self while (ret._is_following): ret = ret._followee() return ret def _followee(self): + # type: () -> Promise assert self._is_following assert isinstance(self._rejection_handler0, Promise) return self._rejection_handler0 def _set_followee(self, promise): + # type: (Promise) -> None assert self._is_following assert not isinstance(self._rejection_handler0, Promise) self._rejection_handler0 = promise def _settle_promises(self): + # type: () -> None length = self._length if length > 0: if self._state == STATE_REJECTED: @@ -388,13 +456,16 @@ def _settle_promises(self): self._length = 0 def _resolve_from_executor(self, executor): + # type: (Callable[[Callable[[T], None], Callable[[Exception], None]], None]) -> None # self._capture_stacktrace() synchronous = True def resolve(value): + # type: (T) -> None self._resolve_callback(value) def reject(reason): + # type: (Exception) -> None self._reject_callback(reason, synchronous) error = None @@ -412,23 +483,28 @@ def reject(reason): @classmethod def wait(cls, promise, timeout=None): - return async_instance.wait(promise, timeout) + # type: (Promise, Optional[float]) -> None + async_instance.wait(promise, timeout) def _wait(self, timeout=None): + # type: (Optional[float]) -> None self.wait(self, timeout) def get(self, timeout=None): + # type: (Optional[float]) -> T target = self._target() self._wait(timeout or DEFAULT_TIMEOUT) return self._target_settled_value(_raise=True) def _target_settled_value(self, _raise=False): + # type: (bool) -> Any return self._target()._settled_value(_raise) _value = _reason = _target_settled_value value = reason = property(_target_settled_value) def __repr__(self): + # type: () -> str hex_id = hex(id(self)) if self._is_following: return "".format( @@ -449,6 +525,8 @@ def __repr__(self): repr(self._fulfillment_handler0) ) + return "" + @property def is_pending(self): # type: (Promise) -> bool @@ -468,14 +546,18 @@ def is_rejected(self): return self._target()._state == STATE_REJECTED def catch(self, on_rejection): - # type: (Promise, Union[Callable, partial]) -> Promise + # type: (Promise, Callable[[Exception], Any]) -> Promise """ This method returns a Promise and deals with rejected cases only. It behaves the same as calling Promise.then(None, on_rejection). """ return self.then(None, on_rejection) - def _then(self, did_fulfill=None, did_reject=None): + def _then(self, + did_fulfill=None, # type: Optional[Callable[[T], None]] + did_reject=None, # type: Optional[Callable] + ): + # type: (...) -> Promise promise = self.__class__() target = self._target() @@ -495,6 +577,7 @@ def _then(self, did_fulfill=None, did_reject=None): async_instance.invoke( partial(target._settle_promise, promise, handler, value, traceback), + promise.scheduler # target._settle_promise instead? # settler, # target, @@ -507,7 +590,7 @@ def _then(self, did_fulfill=None, did_reject=None): do_reject = _reject_callback def then(self, did_fulfill=None, did_reject=None): - # type: (Promise, Union[Callable, partial], Union[Callable, partial]) -> Promise + # type: (Promise, Callable[[T], Any], Optional[Callable]) -> Promise """ This method takes two optional arguments. The first argument is used if the "self promise" is fulfilled and the other is @@ -537,11 +620,12 @@ def then(self, did_fulfill=None, did_reject=None): return self._then(did_fulfill, did_reject) def done(self, did_fulfill=None, did_reject=None): + # type: (Optional[Callable], Optional[Callable]) -> None promise = self._then(did_fulfill, did_reject) promise._is_final = True def done_all(self, handlers=None): - # type: (Promise, List[Callable]) -> None + # type: (Promise, Optional[List[Union[Dict[str, Optional[Callable]], Tuple[Callable, Callable], Callable]]]) -> None """ :type handlers: list[(Any) -> object] | list[((Any) -> object, (Any) -> object)] """ @@ -551,11 +635,10 @@ def done_all(self, handlers=None): for handler in handlers: if isinstance(handler, tuple): s, f = handler - self.done(s, f) elif isinstance(handler, dict): - s = handler.get('success') - f = handler.get('failure') + s = handler.get('success') # type: ignore + f = handler.get('failure') # type: ignore self.done(s, f) else: @@ -593,25 +676,27 @@ def then_all(self, handlers=None): @classmethod def _try_convert_to_promise(cls, obj): + # type: (Any) -> Promise _type = obj.__class__ if issubclass(_type, Promise): if cls is not Promise: - return cls(obj.then) + return cls(obj.then, obj._scheduler) return obj - if iscoroutine(obj): - obj = ensure_future(obj) + if iscoroutine(obj): # type: ignore + obj = ensure_future(obj) # type: ignore _type = obj.__class__ if is_future_like(_type): def executor(resolve, reject): + # type: (Callable, Callable) -> None if obj.done(): _process_future_result(resolve, reject)(obj) else: obj.add_done_callback( _process_future_result(resolve, reject)) # _process_future_result(resolve, reject)(obj) - promise = cls(executor) + promise = cls(executor) # type: Promise promise._future = obj return promise @@ -619,7 +704,8 @@ def executor(resolve, reject): @classmethod def reject(cls, reason): - ret = cls() + # type: (Exception) -> Promise + ret = cls() # type: Promise # ret._capture_stacktrace(); # ret._rejectCallback(reason, true); ret._reject_callback(reason, True) @@ -631,7 +717,7 @@ def reject(cls, reason): def resolve(cls, obj): # type: (Any) -> Promise if not cls.is_thenable(obj): - ret = cls() + ret = cls() # type: Promise # ret._capture_stacktrace() ret._state = STATE_FULFILLED ret._rejection_handler0 = obj @@ -644,6 +730,7 @@ def resolve(cls, obj): @classmethod def promisify(cls, f): + # type: (Callable) -> Callable[..., Promise] if not callable(f): warn( "Promise.promisify is now a function decorator, please use Promise.resolve instead.") @@ -651,34 +738,39 @@ def promisify(cls, f): @wraps(f) def wrapper(*args, **kwargs): + # type: (*Any, **Any) -> Promise def executor(resolve, reject): + # type: (Callable, Callable) -> Optional[Any] return resolve(f(*args, **kwargs)) return cls(executor) return wrapper - _safe_resolved_promise = None + _safe_resolved_promise = None # type: Promise @classmethod def safe(cls, fn): + # type: (Callable) -> Callable from functools import wraps if not cls._safe_resolved_promise: cls._safe_resolved_promise = Promise.resolve(None) @wraps(fn) def wrapper(*args, **kwargs): + # type: (*Any, **Any) -> Promise return cls._safe_resolved_promise.then(lambda v: fn(*args, **kwargs)) return wrapper @classmethod def all(cls, promises): + # type: (Any) -> Promise return PromiseList(promises, promise_class=cls).promise @classmethod def for_dict(cls, m): - # type: (Dict[Any, Promise]) -> Promise + # type: (Dict[Hashable, Promise]) -> Promise """ A special function that takes a dictionary of promises and turns them into a promise for a dictionary of values. @@ -691,6 +783,7 @@ def for_dict(cls, m): return cls.resolve(dict_type()) def handle_success(resolved_values): + # type: (List[Any]) -> Dict[Hashable, Any] return dict_type(zip(m.keys(), resolved_values)) return cls.all(m.values()).then(handle_success) @@ -706,15 +799,18 @@ def is_thenable(cls, obj): if obj is None or _type in BASE_TYPES: return False - return issubclass(_type, Promise) or \ - iscoroutine(obj) or \ + return ( + issubclass(_type, Promise) or + iscoroutine(obj) or # type: ignore is_future_like(_type) + ) _type_done_callbacks = {} # type: Dict[type, bool] def is_future_like(_type): + # type: (type) -> bool if _type not in _type_done_callbacks: _type_done_callbacks[_type] = callable( getattr(_type, 'add_done_callback', None)) @@ -727,7 +823,9 @@ def is_future_like(_type): def _process_future_result(resolve, reject): + # type: (Callable, Callable) -> Callable def handle_future_result(future): + # type: (Any) -> None exception = future.exception() if exception: reject(exception) diff --git a/promise/promise_list.py b/promise/promise_list.py index 7292914..9b85e86 100644 --- a/promise/promise_list.py +++ b/promise/promise_list.py @@ -1,42 +1,57 @@ from functools import partial from collections import Iterable +if False: + from .promise import Promise + from typing import (Any, Optional, Tuple, Union, List, + Type, Collection) # flake8: noqa class PromiseList(object): - __slots__ = ('_values', '_length', '_total_resolved', 'promise', '_promise_class') + __slots__ = ('_values', '_length', '_total_resolved', + 'promise', '_promise_class') def __init__(self, values, promise_class): + # type: (Union[Collection, Promise[Collection]], Type[Promise]) -> None self._promise_class = promise_class self.promise = self._promise_class() - self._values = values self._length = 0 self._total_resolved = 0 - self._init() + self._values = None # type: Optional[Collection] + Promise = self._promise_class + if Promise.is_thenable(values): + values_as_promise = Promise._try_convert_to_promise( + values)._target() # type: ignore + self._init_promise(values_as_promise) + else: + self._init(values) # type: ignore def __len__(self): + # type: () -> int return self._length - def _init(self): - Promise = self._promise_class - values = self._values - if Promise.is_thenable(values): - values = Promise._try_convert_to_promise(self._values)._target() - if values.is_fulfilled: - values = values._value() - elif values.is_rejected: - return self._reject(values._reason()) - # Is pending - else: - self.promise._is_async_guaranteed = True - return values._then( - self._init, - self._reject, - ) + def _init_promise(self, values): + # type: (Promise[Collection]) -> None + if values.is_fulfilled: + values = values._value() + elif values.is_rejected: + self._reject(values._reason()) + return + + self.promise._is_async_guaranteed = True + values._then( + self._init, + self._reject, + ) + return + def _init(self, values): + # type: (Collection) -> None + self._values = values if not isinstance(values, Iterable): - err = Exception("PromiseList requires an iterable. Received {}.".format(repr(values))) + err = Exception( + "PromiseList requires an iterable. Received {}.".format(repr(values))) self.promise._reject_callback(err, False) return @@ -45,10 +60,13 @@ def _init(self): return self._iterate(values) + return def _iterate(self, values): + # type: (Collection[Any]) -> None Promise = self._promise_class is_resolved = False + self._length = len(values) self._values = [None] * self._length @@ -68,9 +86,11 @@ def _iterate(self, values): ) self._values[i] = maybe_promise elif maybe_promise.is_fulfilled: - is_resolved = self._promise_fulfilled(maybe_promise._value(), i) + is_resolved = self._promise_fulfilled( + maybe_promise._value(), i) elif maybe_promise.is_rejected: - is_resolved = self._promise_rejected(maybe_promise._reason()) + is_resolved = self._promise_rejected( + maybe_promise._reason()) else: is_resolved = self._promise_fulfilled(val, i) @@ -82,21 +102,23 @@ def _iterate(self, values): result._is_async_guaranteed = True def _promise_fulfilled(self, value, i): + # type: (Any, int) -> bool if self.is_resolved: - return + return False # assert not self.is_resolved # assert isinstance(self._values, Iterable) # assert isinstance(i, int) - self._values[i] = value + self._values[i] = value # type: ignore self._total_resolved += 1 if self._total_resolved >= self._length: - self._resolve(self._values) + self._resolve(self._values) # type: ignore return True return False def _promise_rejected(self, reason): + # type: (Exception) -> bool if self.is_resolved: - return + return False # assert not self.is_resolved # assert isinstance(self._values, Iterable) self._total_resolved += 1 @@ -105,15 +127,18 @@ def _promise_rejected(self, reason): @property def is_resolved(self): + # type: () -> bool return self._values is None def _resolve(self, value): + # type: (Collection[Any]) -> None assert not self.is_resolved assert not isinstance(value, self._promise_class) self._values = None self.promise._fulfill(value) def _reject(self, reason): + # type: (Exception) -> None assert not self.is_resolved self._values = None self.promise._reject_callback(reason, False) diff --git a/promise/pyutils/version.py b/promise/pyutils/version.py index 5bcc33c..3d1d497 100644 --- a/promise/pyutils/version.py +++ b/promise/pyutils/version.py @@ -74,6 +74,6 @@ def get_git_changeset(): ) timestamp = git_log.communicate()[0] timestamp = datetime.datetime.utcfromtimestamp(int(timestamp)) - except: + except Exception: return None return timestamp.strftime('%Y%m%d%H%M%S') diff --git a/promise/schedulers/gevent.py b/promise/schedulers/gevent.py index efc2119..6b84c4c 100644 --- a/promise/schedulers/gevent.py +++ b/promise/schedulers/gevent.py @@ -1,7 +1,7 @@ from __future__ import absolute_import -from gevent.event import Event -import gevent +from gevent.event import Event # type: ignore +import gevent # type: ignore class GeventScheduler(object): diff --git a/promise/schedulers/immediate.py b/promise/schedulers/immediate.py index 79a8b04..1aed96b 100644 --- a/promise/schedulers/immediate.py +++ b/promise/schedulers/immediate.py @@ -1,17 +1,23 @@ from threading import Event +if False: + from ..promise import Promise + from typing import Callable, Any, Optional # flake8: noqa class ImmediateScheduler(object): def call(self, fn): + # type: (Callable) -> None try: fn() except: pass def wait(self, promise, timeout=None): + # type: (Promise, Optional[float]) -> None e = Event() def on_resolve_or_reject(_): + # type: (Any) -> None e.set() promise._then(on_resolve_or_reject, on_resolve_or_reject) diff --git a/promise/utils.py b/promise/utils.py index abc5bb5..edba6d4 100644 --- a/promise/utils.py +++ b/promise/utils.py @@ -6,6 +6,7 @@ def warn(msg): + # type: (str) -> None warnings.simplefilter('always', DeprecationWarning) # turn off filter warnings.warn(msg, category=DeprecationWarning, stacklevel=2) warnings.simplefilter('default', DeprecationWarning) # reset filter diff --git a/setup.py b/setup.py index 0536c28..f7f2d4a 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,6 @@ 'test': tests_require, }, install_requires=[ - 'typing', 'six' + 'typing>=3.6.4', 'six' ], tests_require=tests_require, )