Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove references to gen.coroutine from docs and update without_document_lock decorator #12010

Merged
merged 5 commits into from
Feb 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions bokeh/document/locking.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#-----------------------------------------------------------------------------

# Standard library imports
import asyncio
from functools import wraps
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -81,10 +82,20 @@ def without_document_lock(func: F) -> NoLockCallback[F]:
Attempts to otherwise access or change the Document will result in an
exception being raised.

``func`` can be a synchronous function, an async function, or a function
decorated with ``asyncio.coroutine``. The returned function will be an
async function if ``func`` is any of the latter two.

'''
@wraps(func)
def _wrapper(*args: Any, **kw: Any) -> None:
func(*args, **kw)
if asyncio.iscoroutinefunction(func):
@wraps(func)
async def _wrapper(*args: Any, **kw: Any) -> None:
await func(*args, **kw)
else:
@wraps(func)
def _wrapper(*args: Any, **kw: Any) -> None:
func(*args, **kw)

wrapper = cast(NoLockCallback[F], _wrapper)
wrapper.nolock = True
return wrapper
Expand Down
26 changes: 10 additions & 16 deletions sphinx/source/docs/user_guide/server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -547,25 +547,22 @@ To allow all threads access to the same document, save a local copy of

.. code-block:: python

import time
from functools import partial
from random import random
from threading import Thread
import time

from bokeh.models import ColumnDataSource
from bokeh.plotting import curdoc, figure

from tornado import gen

# only modify from a Bokeh session callback
source = ColumnDataSource(data=dict(x=[0], y=[0]))

# This is important! Save curdoc() to make sure all threads
# see the same document.
doc = curdoc()

@gen.coroutine
def update(x, y):
async def update(x, y):
source.stream(dict(x=[x], y=[y]))

def blocking_task():
Expand Down Expand Up @@ -619,11 +616,10 @@ session callback with a different update rate.

.. code-block:: python

from functools import partial
import asyncio
import time

from concurrent.futures import ThreadPoolExecutor
from tornado import gen
from functools import partial

from bokeh.document import without_document_lock
from bokeh.models import ColumnDataSource
Expand All @@ -642,31 +638,29 @@ session callback with a different update rate.
return i

# the unlocked callback uses this locked callback to safely update
@gen.coroutine
def locked_update(i):
async def locked_update(i):
source.stream(dict(x=[source.data['x'][-1]+1], y=[i], color=["blue"]))

# this unlocked callback will not prevent other session callbacks from
# executing while it is running
@gen.coroutine
@without_document_lock
def unlocked_task():
async def unlocked_task():
global i
i += 1
res = yield executor.submit(blocking_task, i)
res = await asyncio.wrap_future(executor.submit(blocking_task, i), loop=None)
doc.add_next_tick_callback(partial(locked_update, i=res))

@gen.coroutine
def update():
async def update():
source.stream(dict(x=[source.data['x'][-1]+1], y=[i], color=["red"]))

p = figure(x_range=[0, 100], y_range=[0,20])
p = figure(x_range=[0, 100], y_range=[0, 20])
l = p.circle(x='x', y='y', color='color', source=source)

doc.add_periodic_callback(unlocked_task, 1000)
doc.add_periodic_callback(update, 200)
doc.add_root(p)


As before, you can run this example by saving to a Python file and running
``bokeh serve`` on it.

Expand Down
18 changes: 18 additions & 0 deletions tests/unit/bokeh/document/test_locking.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#-----------------------------------------------------------------------------

# Standard library imports
import asyncio
from typing import List

# Bokeh imports
Expand Down Expand Up @@ -70,6 +71,23 @@ def cb():
assert curdoc_from_cb[0]._doc is d
assert isinstance(curdoc_from_cb[0], locking.UnlockedDocumentProxy)

def test_without_document_lock_accepts_async_function() -> None:
i = 0
d = Document()

@locking.without_document_lock
async def cb():
nonlocal i
await asyncio.sleep(0.1)
i += 1
callback_obj = d.add_next_tick_callback(cb)

loop = asyncio.get_event_loop()
loop.run_until_complete(callback_obj.callback())

assert callback_obj.callback.nolock == True
assert i == 1

#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------
Expand Down