Skip to content

Commit

Permalink
Merge pull request #978 from ZoomerAnalytics/xltrail-473-async
Browse files Browse the repository at this point in the history
added support for func(async='threading'), closes #473
  • Loading branch information
fzumstein committed Nov 4, 2018
2 parents b28e035 + c224ce0 commit a2ef426
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 21 deletions.
34 changes: 34 additions & 0 deletions docs/udfs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,37 @@ Imported functions can also be used from VBA. For example, for a function return
Next j

End Sub


.. _async_functions:

Asynchronous UDFs
-----------------

.. versionadded:: v0.14.0

xlwings offers an easy way to write asynchronous functions in Excel. Asynchronous functions return immediately with
``#N/A waiting...``. While the function is waiting for its return value, you can use Excel to do other stuff and whenever
the return value is available, the cell value will be updated.

The only available mode is currently ``async='threading'``, meaning that it's useful for I/O-bound tasks, for example when
you fetch data from an API over the web.

You make a function asynchronous simply by giving it the respective argument in the function decorator. In this example,
the time consuming I/O-bound task is simulated by using ``time.sleep``::

import xlwings as xw
import time

@xw.func(async='threading')
def myfunction(a):
time.sleep(5) # long running tasks
return a



You can use this function like any other xlwings function, simply by putting ``=myfunction("abcd")`` into a cell
(after you have imported the function, off course).

Note that xlwings doesn't use the native asynchronous functions that were introduced with Excel 2010, so xlwings
asynchronous functions are supported with any version of Excel.
19 changes: 19 additions & 0 deletions docs/whatsnew.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
What's New
==========

v0.14.0 (Nov 4, 2018)
---------------------

**Features**:

This release adds support for asynchronous functions (like all UDF related functionality, this is only available on Windows).
Making a function asynchronous is as easy as::

import xlwings as xw
import time

@xw.func(async='threading')
def myfunction(a):
time.sleep(5) # long running tasks
return a

See :ref:`async_functions` for the full docs.


v0.13.0 (Oct 22, 2018)
----------------------

Expand Down
20 changes: 20 additions & 0 deletions xlwings/tests/udfs/async_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import xlwings as xw
import time


@xw.func(async='threading')
def async1(a):
time.sleep(2)
return a


@xw.func(async='threading')
@xw.arg("n", numbers=int)
@xw.arg("m", numbers=int)
@xw.ret(expand='table')
def async2(n, m):
time.sleep(2)
return [
["%s x %s : %s, %s" % (n, m, i, j) for j in range(m)]
for i in range(n)
]
Binary file added xlwings/tests/udfs/async_tests.xlsm
Binary file not shown.
84 changes: 63 additions & 21 deletions xlwings/udfs.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
import os
import os.path
import sys
import re
import os.path
import tempfile
import inspect
from importlib import import_module
from threading import Thread

from win32com.client import Dispatch

from . import conversion
from . import conversion, xlplatform, Range, apps, PY3
from .utils import VBAWriter
from . import xlplatform
from . import Range

from . import PY3

cache = {}


class AsyncThread(Thread):
def __init__(self, pid, book_name, sheet_name, address, func, args, cache_key):
Thread.__init__(self)
self.pid = pid
self.book = book_name
self.sheet = sheet_name
self.address = address
self.func = func
self.args = args
self.cache_key = cache_key

def run(self):
cache[self.cache_key] = self.func(*self.args)

apps[self.pid].books[self.book].sheets[self.sheet][self.address].formula = \
apps[self.pid].books[self.book].sheets[self.sheet][self.address].formula


if PY3:
try:
Expand Down Expand Up @@ -70,21 +89,22 @@ def get_category(**func_kwargs):
return "xlwings" # Default category


def should_call_in_wizard(**func_kwargs):
if 'call_in_wizard' in func_kwargs:
call_in_wizard = func_kwargs.pop('call_in_wizard')
if isinstance(call_in_wizard, bool):
return call_in_wizard
raise Exception('call_in_wizard only takes boolean values ("{0}" provided).'.format(call_in_wizard))
return True
def get_async(**func_kwargs):
if 'async' in func_kwargs:
async = func_kwargs.pop('async')
if async in ['threading']:
return async
raise Exception('The only supported async mode is currently "threading".')
else:
return None


def check_volatile(**func_kwargs):
if 'volatile' in func_kwargs:
volatile = func_kwargs.pop('volatile')
if isinstance(volatile, bool):
return volatile
raise Exception('volatile only takes boolean values ("{0}" provided).'.format(volatile))
def check_bool(kw, **func_kwargs):
if kw in func_kwargs:
check = func_kwargs.pop(kw)
if isinstance(check, bool):
return check
raise Exception('{0} only takes boolean values. ("{1}" provided).'.format(kw, check))
return False


Expand Down Expand Up @@ -120,8 +140,9 @@ def inner(f):
"options": {}
}
f.__xlfunc__["category"] = get_category(**kwargs)
f.__xlfunc__['call_in_wizard'] = should_call_in_wizard(**kwargs)
f.__xlfunc__['volatile'] = check_volatile(**kwargs)
f.__xlfunc__['call_in_wizard'] = check_bool('call_in_wizard', **kwargs)
f.__xlfunc__['volatile'] = check_bool('volatile', **kwargs)
f.__xlfunc__['async'] = get_async(**kwargs)
return f
if f is None:
return inner
Expand Down Expand Up @@ -245,7 +266,28 @@ def call_udf(module_name, func_name, args, this_workbook=None, caller=None):
args[i] = conversion.read(None, arg, arg_info['options'])
if this_workbook:
xlplatform.BOOK_CALLER = Dispatch(this_workbook)
ret = func(*args)

if func_info['async'] and func_info['async'] == 'threading':
xw_caller = Range(impl=xlplatform.Range(xl=caller))
key = (func.__name__ + str(args) + str(xw_caller.sheet.book.app.pid) +
xw_caller.sheet.book.name + xw_caller.sheet.name + xw_caller.address)
cached_value = cache.get(key)
if cached_value:
del cache[key]
ret = cached_value
else:
# You can't pass pywin32 objects directly to threads
thread = AsyncThread(xw_caller.sheet.book.app.pid,
xw_caller.sheet.book.name,
xw_caller.sheet.name,
xw_caller.address,
func,
args,
key)
thread.start()
return '#N/A waiting...'
else:
ret = func(*args)

if ret_info['options'].get('expand', None):
from .server import add_idle_task
Expand Down

0 comments on commit a2ef426

Please sign in to comment.