diff --git a/dominate/dom_tag.py b/dominate/dom_tag.py index b3becf5..18f62b7 100644 --- a/dominate/dom_tag.py +++ b/dominate/dom_tag.py @@ -23,7 +23,7 @@ from collections import defaultdict, namedtuple from functools import wraps import threading -from asyncio import get_running_loop +from asyncio import current_task, get_running_loop from uuid import uuid4 from contextvars import ContextVar @@ -73,7 +73,8 @@ def _get_thread_context(): try: if get_running_loop().is_running(): # Only add this extra information if we are actually in a running event loop - context.append(("async", _get_async_context_id())) + # A child task inherits ContextVars, so its task identity must also differ. + context.append(("async", _get_async_context_id(), current_task())) # A runtime error is raised if there is no async loop... except RuntimeError: pass diff --git a/tests/test_dom_tag_async.py b/tests/test_dom_tag_async.py index 92c2438..6d2dd74 100644 --- a/tests/test_dom_tag_async.py +++ b/tests/test_dom_tag_async.py @@ -1,10 +1,31 @@ -from asyncio import gather, run, Semaphore +from asyncio import create_task, gather, run, Semaphore from textwrap import dedent from dominate import tags from dominate.dom_tag import async_context_id +def test_child_task_does_not_inherit_parent_dom_context(): + async def build_child(): + with tags.div(id='child') as child: + tags.span('child content') + return str(child) + + async def build_parent(): + with tags.div(id='parent') as parent: + child = await create_task(build_child()) + return str(parent), child + + token = async_context_id.set(None) + try: + parent, child = run(build_parent()) + finally: + async_context_id.reset(token) + + assert parent == '
' + assert child == '
\n child content\n
' + + # To simulate sleep without making the tests take a hella long time to complete # lets use a pair of semaphores to explicitly control when our coroutines run. # The order of execution will be marked as comments below: @@ -33,20 +54,13 @@ async def merge(): tag_routine_2(sem_1, sem_2) ) - # Set this test up for failure - pre-set the context to a non-None value. - # As it is already set, _get_async_context_id will not set it to a new, unique value - # and thus we won't be able to differentiate between the two contexts. This essentially simulates - # the behavior before our async fix was implemented (the bleed): + # An ID initialized by a parent task is copied into each child's context. + # Task identity must still keep the two DOM contexts independent. async_context_id.set(0) tag_1, tag_2 = run(merge()) - # This looks wrong - but its what we would expect if we don't - # properly handle async... assert tag_1 == dedent("""\
-
-
-
""").strip() @@ -57,8 +71,7 @@ async def merge(): """).strip() - # Okay, now lets do it right - lets clear the context. Now when each async function - # calls _get_async_context_id, it will get a unique value and we can differentiate. + # Also verify tasks starting with an uninitialized context. async_context_id.set(None) tag_1, tag_2 = run(merge())