Package: notcallback
Not callback.Promise-style interfaces for callback-based asynchronous libraries.yield from(Promise.all(open_db_connection(),Promise.race(fetch_data_region_US(),fetch_data_region_DE(),),).then(parse_data).then(update_rows).then(commit).catch(rollback).finally_(close_connection))Install withpip:python3-mpipinstallnotcallbackContentsExamplesasync/awaitand asyncioAPI ReferenceSee alsoThis library imitates the logic of thePromiseAPI found in ECMAScript.
It lets you restructure existing callback-style code into Promise workflows that are more readable.
It also provides a set of utility functions to let you better control the flow of your async programs.This library is written using thePromise/A+specification as the reference. I try to recreate most of
the behaviors outlined in Promise/A+. However, the standards-compliance of the library has not been evaluated.
Most importantly:Note on asynchronyThis is not an async framework.There is no event loop magic happening in this library. You must
already be working with an existing asynchronous/concurrency framework (preferably a callback-based one) for this
library to be any useful. Otherwise, your Promises will simply run sequentially and block.Here is an example of how one might turnScrapy'sRequest(which uses callbacks and is powered byTwisted) into Promise:importjsonimportloggingfromnotcallbackimportPromisefromscrapyimportRequestdeffetch(**kwargs):"""Create a Promise that will schedule a Request."""defexecutor(resolve,reject):# When the response is ready, the Promise gets resolved by Scrapy through the `resolve` function.yieldRequest(**kwargs,callback=resolve,errback=reject)returnPromise(executor)defstart_requests(self):# Promises are iterable, and Scrapy will receive the yielded Requestsreturn(# When the Promise is resolved, the handler in `.then()` gets executed.fetch('https://httpbin.org/ip')# A second Request is created from the response of the first one and is scheduled..then(lambdaresponse:fetch(json.loads(response.text)['origin']))# Print out the Response object from the second Request..then(print)# If an exception was raised at anytime during the Promise, log it..catch(lambdaexc:logging.getLogger().error(exc)))That being said, this library does provide a version of Promise that can work with the asyncio library.
Seeasync/awaitand asynciofor more info.ExamplesIf you are unfamiliar with how Promise works in JavaScript, a great starting point would be MDN'sPromise API referenceand the guide toUsing Promises.
Most of the usage choices here should be analogous to how Promise is used in JavaScript (except for the generator syntax).Creating a new Promisedefexecutor(resolve,reject):...ifshould_fulfill:yield fromresolve(value)# marks this Promise as resolved and begin the resolution process# eventually the Promise will become either fulfilled or rejectedelse:yield fromreject(reason)# rejects this Promise with the specified reasonpromise=Promise(executor)Evaluating a PromisePromises themselves are generators. You complete a Promise by exhausting it.# Within another generator:yield frompromise# In a loop:foriinpromise:...# As a coroutine (non async/await)promise.send(None)Accessing Promise propertiesThe main properties of a Promise are its state and value:>>>promise# resolved with True>>>promise.state<PromiseState.FULFILLED:'fulfilled'>>>>promise.get()# return the value of the PromiseTrue>>>promise.is_settledTrueProviding handlersThe most important feature of Promise is the.then(on_fulfill, on_reject)instance method, which allows you
to add handlers to a Promise.defextract_keys(file):data=json.load(file)returndata.keys()defprint_exception(exc):returnprint(repr(exc),file=sys.stderr)>>>Promise.settle(Promise(correct_file).then(extract_keys,print_exception))# Promise.settle runs a Promise until completion<Promiseat0x10b4baa10(fulfilled)=>dict_keys([...])>>>>Promise.settle(Promise(wrong_file).then(extract_keys).catch(print_exception)# Promise.catch is a convenient method for adding exception handlersFileNotFoundError:[Errno2]Nosuchfileordirectory:'wrong_file.json'<Promiseat0x10b4bad50(fulfilled)=>None># The state is fulfilled because the exception was successfully handled# The value is None because print() returns None.Error handlingThe following built-in exceptions will not be caught by Promise:GeneratorExit,KeyboardInterrupt,SystemExitAdditionally, the following exceptions represent unexpected behaviors from a Promise and will also be thrown:PromiseException# Base class for Exceptions indicating faulty Promise behaviorsPromiseWarning# Indicates behaviors that are correct but may be unintended, such as unhandled rejections.# Normally warned but may be thrown if a warnings filter is set.UnhandledPromiseRejectionWarning, a concrete subclass ofPromiseWarning, will be displayed when the final
Promise in a Promise chain is rejected (note that this is not raised):defno_recursion(resolve,reject):raiseRecursionError()>>>Promise.settle(Promise(no_recursion))Traceback(mostrecentcalllast):...UnhandledPromiseRejectionWarning:UnhandledPromiserejection:RecursionError:in<Promise'no_recursion'at0x104e3df50(rejected)=>RecursionError()>Promise branchingA Promise can register multiple handlers. Handlers are evaluated in the order they are registered.conn=Promise(open_connection())forrecipientinbcc:Promise.settle(conn.then(update_rows(recipient)))Note that Promises will always be evaluated only once: once they are settled, the executor will not be run again. However,
you can still attach handlers to an already settled Promise, and the resulting Promise, when evaluated, will run the handlers
immediately with the value of the settled Promise.Promise chainingPromise().then() returns a new Promise,which means you can chain multiple.then()handlers togetherdefadd(delta):"""Create a handler that adds `delta` to the incoming `val` and returns it."""defaccumulate(val=0):returnval+deltareturnaccumulate>>>Promise.settle(...Promise.resolve(-1)# Promise.resolve() returns a Promise that is already fulfilled with the given value.....then(add(3))....then(add(6))....then(add(10))....then(print)...)18If you call.then()without providing a rejection handler, the rejection is propagated down the Promise chain
like how an exception would bubble up the stack, until it encounters a Promise with a valid rejection handler.yield from(Promise.all(...)# Uncaught exceptions raised in.then(parse_data)# any.then(update_rows)# of.then(commit)# these.catch(rollback)# handlers will be caught here..finally_(close_connection))Dynamic chainingIf you return another Promise in your.then()handlers, it will get resolved, and then the remaining handlers will
be attached to it.defload_page(url):defexecutor(resolve,reject):yieldRequest(url,callback=resolve,errback=reject)# A Scrapy scenarioreturnPromise(executor)defnext_page(response):...page_id=response.get('pageId')ifpage_id:url=f'{response.url}&continueAt={page_id}'# Return a new Promisereturnload_page(url).then(next_page)yield fromload_page(start_url).then(next_page).then(finalize)#    Promises are inserted into the chain here ^# If there are multiple pages, the Promise chain will look like# load_page => next_page => load_page => next_page => load_page => next_page => ... => finalizePromise aggregation functionsThis library provides all 4 static Promise methods available in JavaScript:Promise.all(),Promise.race(),Promise.all_settled(), andPromise.any().For example:Promise.all(): Only resolve when all the Promises in the list are fulfilled, and reject as soon as one of them rejects:Promise.all(register_hardware,config_simulators,load_assets).then(render).catch(warn)Promise.race(): Resolve/reject as soon as one of the promises fulfills/rejects:Promise.race(*[access(file,region)forregionin['USNCalifornia','USOregon','USEOhio','USNewYork',]]).then(respond).catch(purge_cache)async/awaitand asyncioAlthough this library is only meant to work with async frameworks that predatesPEP 492, it does come with
experimental support for theasync/awaitsyntax and asyncio.Note: I designed the interface between Promise and asyncio mainly to learn how async functions in Python work. If you are already working with async functions and asyncio, you will probably find this library rarely useful.Import Promise fromnotcallback.async_instead ofnotcallback.fromnotcallback.async_importPromiseThe async Promise is API-compatible with the non-async version, meaning it still works as an iterator/generator.However, with async Promise:Promisecan beawaitedawaitwill return the Promise's value if it fulfills, or raise an exception if it rejects. This is very similar
to howasync/awaitworks in JavaScript.defkey(resolve,_):yield fromresolve(42)defauthenticate(key):defexecutor(resolve,reject):ifkey==42.1:yield fromresolve()else:yield fromreject('Access denied.')returnPromise(executor)# within an async function>>>awaitPromise(key)42>>>awaitauthenticate(awaitPromise(key))Traceback(mostrecentcalllast):...notcallback.exceptions.PromiseRejection:PromiseRejection:'Access denied.'If youyieldan awaitable from you executor or handler function,Promisewill schedule it for you:# sleep with extra stepsdefsleep(sec):defdo_sleep(resolve,reject):yieldasyncio.sleep(sec)# sleep hereyield fromresolve(repr(f'slept for{sec}s'))returnPromise(do_sleep)asyncdefmain():returnawaitsleep(5)>>>timeit.timeit(lambda:asyncio.run(main()),number=1)5.003376382999988If you need to yield values from your functions, Promises can be used as async iterators.defsleepn(n):defsleep(resolve,reject):forsecinrange(1,n+1):yieldsecyieldasyncio.sleep(sec)yield fromresolve()returnPromise(sleep)asyncdefmain():asyncforsecinsleepn(3):print(sec)>>>timeit.timeit(lambda:asyncio.run(main()),number=1)1236.007059991999995Promise.all(),Promise.race(),Promise.all_settled(), andPromise.any()now accept an additionalconcurrentlykeyword-only argument, which is default toFalse. Setting it toTrueallows Promises to run with asyncio
concurrently.asyncdefmain():returnawaitPromise.all(sleep(2),sleep(3),sleep(5),concurrently=True,# run Promises concurrently)>>>timeit.timeit(lambda:asyncio.run(main()),number=1)5.003311451000002Note that forPromise.race()andPromise.any(), the time for theawaitexpression to finish will always be
the same as that of thelongest-runningPromise. This is so that all asyncio tasks are properlyawaited. This means that these
methods will not save you execution time.The usefulness of these two methods is in that the aggregated Promise willsettleearly, and by that, all the handlers attached
to the aggregated Promise will also run early:defsleep(sec):defdo_sleep(resolve,reject):yieldasyncio.sleep(sec)yield fromresolve(f'finished sleeping for{sec}s at{time.perf_counter():.3f}')returnPromise(do_sleep)asyncdefmain():returnawait(Promise.race(sleep(2).then(print),sleep(3).then(print),sleep(5).then(print),concurrently=True,).then(lambda_:print(f'Promise.race fulfilled at{time.perf_counter():.3f}')))>>>total_time=timeit.timeit(lambda:asyncio.run(main()),number=1)finishedsleepingfor2sat14.542Promise.racefulfilledat14.542finishedsleepingfor3sat15.541finishedsleepingfor5sat17.542>>>print(f'main coroutine finished in{total_time:.3f}seconds')maincoroutinefinishedin5.003secondsUsing async functions as executors or handlers isnotsupported.Known issues:Using Promises with asyncio functions:With async support, thePromiseclass will be a subclass of bothcollections.abc.Generatorandcollections.abc.Coroutine.The problem is that some asyncio functions, such asasyncio.gather(), perform runtime type checking on
their arguments to see if they are awaitables or coroutines, and act differently. And, at least forasyncio.gather(), aRuntimeErrorwill be thrown if it encounters ayield fromexpression at any point.This means that you can no longer do things likeyield from resolve()when usingasyncio.gather().
This is hard-wired into asyncio.If you need to use Promises with asyncio functions, and you do not need toyieldany intermediate values
(except for awaitables), then a solution will be to use the.awaitable()instance method, which is guaranteed to
return a non-coroutine awaitable, which asyncio has no issue running.API ReferenceInitializerPromise(executor)Reference JavaScript function:Promise() constructorTurn a function into a Promise.executoris a function that takes exactly 2 arguments,resolveandreject.executorcan be a
regular function or a generator function. When it is ready to settle the Promise,executorshould call
eitherresolve()orreject()with the fulfilled value/rejection reason as the only argument. Bothresolve()andreject()return a new generator, andexecutormust exhaust it, either by usingyield fromor iterate over it.PropertiesPromise().stateReturn the state of the Promise. This is anotcallback.base.PromiseStateEnum which can be one of 3 values:PENDING,FULFILLED, orREJECTED.Promise().valueReturn the value of the Promise if it is fulfilled, or the reason of rejection if it is rejected.Attempting to retrieve the value of the Promise when it is still pending will result in aPromisePendingexception. This is so that theNonethat the Promise initially has as its "value" does not get mistaken
as a fulfillment or rejection whose value/reason isNone.See also:Promise().get(),Promise().fulfilled(exc_type),Promise().rejected(exc_type)Promise().is_pending,Promise().is_fulfilled,Promise().is_rejected,Promise().is_settledThese properties check whether a Promise is in a certain state.Instance methodsPromise().then(on_fulfill, on_reject)Reference JavaScript function:Promise.prototype.then()Return a new Promise that waits for this Promise to settle and then reacts accordingly.Accepts 1 to 2 arguments. Must be callable.If this Promise (the Promise whose.thenis called) is fulfilled,on_fulfill()will be called with the fulfilled value;
if this Promise is rejected,on_reject()will be called with the reason of rejection.on_rejectmay be omitted, and the rejection will be reraised (if the rejection value is not an exception, it will be wrapped in aPromiseRejectionexception).The new Promise will resolve with the return value of the handler. If the returned value is anotherPromise, thatPromisewill be settled first, and then the new Promise (the one returned by.then) will adopt
the state and value of thatPromise.If the handler raises an exception, the new Promise will be rejected with that exception.Promise().catch(on_reject)Reference JavaScript function:Promise.prototype.catch()Convenience method for registering an exception handler. Sugar forPromise().then(on_reject=on_reject).Promise().finally_(on_settle)Reference JavaScript function:Promise.prototype.finally()Return a Promise whose handler will run regardless of how the previous Promise was settled.on_settlecan be a function or a generator function. It will be called with no argument.The new Promise will adopt the state and value of the previous Promise. If an exception was raised
when runningon_settle, the new Promise will reject with that exception.Promise().get(default=None)Return the value of the Promise if it is FULFILLED, or the reason fpr rejection if its REJECTED.Unlike thePromise().valueproperty, which raises if the Promise is not settled, this method
will returndefaultif the Promise's value is None regardless of its state.Promise().fulfilled(default=None),Promise().rejected(default=None)Return the value of the Promise only if it is in the specified stateReturn the default value if it is not in that state, or if the value isNone.Promise().is_rejected_due_to(exc_type)Check whether the Promise was rejected due to a specific type of exception.ReturnTrueif the Promise is rejected and its value is an instance ofexc_class, andFalsein
all other cases.Promise().awaitable()Only available innotcallback.async_.PromiseReturn the Promise as an awaitable.Class methodsPromise.all(*promises)Reference JavaScript function:Promise.all()Return a new Promise that fulfills when all the provided Promises are fulfilled and rejects if any of them is rejected.If it fulfills, meaning all the provided Promises are fulfilled, its handlers will receive atuplethat contains
the values of all the Promises, with order preserved.If it rejects, it is rejected with the reason of the first rejection that occurred.Note:The Promises are evaluated sequentially.All of the Promises will be evaluated even if one of them rejects; only the execution order is different.For a call that looks like:>>>Promise.all(promise1,promise2,promise3).then(on_fulfill).catch(on_reject)where none of the Promises have async capabilities (meaning they run in order), if all Promises fulfill
successfully, the execution order will bepromise1 => promise2 => promise3 => on_fulfill()If e.g. promise2 rejects or raises an exception, it will bepromise1 => promise2 => on_reject() => promise3If there are multiple rejections, only the first one will have any effect.Only available innotcallback.async_.Promise: accepts an additionalconcurrentlykeyword-only argument.Promise.race(*promises)Reference JavaScript function:Promise.race()Return a new Promise that fulfills/rejects as soon as one of the Promises fulfills/rejects.It will adopt the state and value of the fulfilled/rejected Promise.Note:The Promises are evaluated sequentially. This means that if your function does not have actual async capabilities,
the first Promise in the list will always "win the race."All of the Promises will be evaluated in all cases; only the execution order is different: the Promise'son_fulfill/on_rejecthandlers are run immediately after the first Promise has settled.Only available innotcallback.async_.Promise: accepts an additionalconcurrentlykeyword-only argument.Promise.all_settled(*promises)Reference JavaScript function:Promise.allSettled()Return a new Promise that fulfills when all the Promises have settled i.e. either fulfilled or rejected.The returned Promise always fulfills with the list of Promises provided.Only available innotcallback.async_.Promise: accepts an additionalconcurrentlykeyword-only argument.Promise.any(*promises)Reference JavaScript function:Promise.any()Return a new Promise that ignores rejections among the provided Promises and fulfills upon the first fulfillment.If all Promises reject, it will reject with aPromiseAggregateError.Note:All Promises are evaluated regardless of their state; only the execution order is different: the Promise'son_fulfill/on_rejecthandlers are run immediately after the first Promise that was fulfilled.Only available innotcallback.async_.Promise: accepts an additionalconcurrentlykeyword-only argument.Promise.resolve(value)Reference JavaScript function:Promise.resolve()Return a new Promise that will resolve withvaluewhen it is evaluated. If the value is another Promise, this new Promise will
adopt the state and value of that Promise.Promise.reject(reason)Reference JavaScript function:Promise.reject()Return a new Promise that will reject withreasonwhen it is evaluated.Promise.settle(promise)A helper function that runs the Promise until it's settled and then return it. All intermediate values are discarded.See alsopromise, another Python implementation that is Promise/A+ compliant.