Skip to content

Commit 949b259

Browse files
authored
Merge pull request #23 from graingert/provide-thread-local
2 parents 5ef8e9d + a0f49cb commit 949b259

4 files changed

Lines changed: 60 additions & 30 deletions

File tree

docs/source/index.rst

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -52,53 +52,53 @@ If you'd like your library to be detected by ``sniffio``, it's pretty
5252
easy.
5353

5454
**Step 1:** Pick the magic string that will identify your library. To
55-
avoid collisions, this should match your library's name on PyPI.
55+
avoid collisions, this should match your library's PEP 503 normalized name on PyPI.
5656

57-
**Step 2:** There's a special :class:`contextvars.ContextVar` object:
57+
**Step 2:** There's a special :class:`threading.local` object:
5858

59-
.. data:: current_async_library_cvar
59+
.. data:: thread_local.name
6060

61-
Make sure that whenever your library is running, this is set to your
62-
identifier string. In most cases, this will be as simple as:
61+
Make sure that whenever your library is calling a coroutine ``throw()``, ``send()``, or ``close()``
62+
that this is set to your identifier string. In most cases, this will be as simple as:
6363

6464
.. code-block:: python3
6565
66-
from sniffio import current_async_library_cvar
66+
from sniffio import thread_local
6767
68-
# Your library's run function
69-
def run(...):
70-
token = current_async_library_cvar.set("my-library's-PyPI-name")
68+
# Your library's step function
69+
def step(...):
70+
old_name, thread_local.name = thread_local.name, "my-library's-PyPI-name"
7171
try:
72-
# The actual body of your run() function:
73-
...
72+
result = coro.send(None)
7473
finally:
75-
current_async_library_cvar.reset(token)
74+
thread_local.name = old_name
7675
7776
**Step 3:** Send us a PR to add your library to the list of supported
7877
libraries above.
7978

8079
That's it!
8180

82-
Notes:
81+
There are libraries that directly drive a sniffio-naive coroutine from another,
82+
outer sniffio-aware coroutine such as `trio_asyncio`.
83+
These libraries should make sure to set the correct value
84+
while calling a synchronous function that will go on to drive the
85+
sniffio-naive coroutine.
8386

84-
On older Pythons without native contextvars support, sniffio
85-
transparently uses `the official contextvars backport
86-
<https://pypi.org/project/contextvars/>`__, so you don't need to worry
87-
about that.
8887

89-
There are libraries that can switch back and forth between different
90-
async modes within a single call-task – like ``trio_asyncio`` or
91-
Twisted's asyncio operability. These libraries should make sure to set
92-
the value back and forth at appropriate points.
88+
.. code-block:: python3
89+
90+
from sniffio import thread_local
9391
94-
The general rule of thumb: :data:`current_async_library_cvar` should
95-
be set to X exactly at those moments when ``await X.sleep(...)`` will
96-
work.
92+
# Your library's compatibility loop
93+
async def main_loop(self, ...) -> None:
94+
...
95+
handle: asyncio.Handle = await self.get_next_handle()
96+
old_name, thread_local.name = thread_local.name, "asyncio"
97+
try:
98+
result = handle._callback(obj._args)
99+
finally:
100+
thread_local.name = old_name
97101
98-
.. warning:: You shouldn't attempt to read the value of
99-
``current_async_library_cvar`` directly –
100-
:func:`current_async_library` has a little bit more cleverness than
101-
that.
102102
103103
.. toctree::
104104
:maxdepth: 1

sniffio/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@
1111
current_async_library,
1212
AsyncLibraryNotFoundError,
1313
current_async_library_cvar,
14+
thread_local,
1415
)

sniffio/_impl.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
from contextvars import ContextVar
22
from typing import Optional
33
import sys
4+
import threading
45

56
current_async_library_cvar = ContextVar(
67
"current_async_library_cvar", default=None
78
) # type: ContextVar[Optional[str]]
89

910

11+
class _ThreadLocal(threading.local):
12+
# Since threading.local provides no explicit mechanism is for setting
13+
# a default for a value, a custom class with a class attribute is used
14+
# instead.
15+
name = None # type: Optional[str]
16+
17+
18+
thread_local = _ThreadLocal()
19+
20+
1021
class AsyncLibraryNotFoundError(RuntimeError):
1122
pass
1223

@@ -52,6 +63,10 @@ async def generic_sleep(seconds):
5263
raise RuntimeError(f"Unsupported library {library!r}")
5364
5465
"""
66+
value = thread_local.name
67+
if value is not None:
68+
return value
69+
5570
value = current_async_library_cvar.get()
5671
if value is not None:
5772
return value

sniffio/_tests/test_sniffio.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
from .. import (
66
current_async_library, AsyncLibraryNotFoundError,
7-
current_async_library_cvar
7+
current_async_library_cvar, thread_local
88
)
99

1010

11-
def test_basics():
11+
def test_basics_cvar():
1212
with pytest.raises(AsyncLibraryNotFoundError):
1313
current_async_library()
1414

@@ -22,6 +22,20 @@ def test_basics():
2222
current_async_library()
2323

2424

25+
def test_basics_tlocal():
26+
with pytest.raises(AsyncLibraryNotFoundError):
27+
current_async_library()
28+
29+
old_name, thread_local.name = thread_local.name, "generic-lib"
30+
try:
31+
assert current_async_library() == "generic-lib"
32+
finally:
33+
thread_local.name = old_name
34+
35+
with pytest.raises(AsyncLibraryNotFoundError):
36+
current_async_library()
37+
38+
2539
def test_asyncio():
2640
import asyncio
2741

0 commit comments

Comments
 (0)