Skip to content

Commit 19b1231

Browse files
stainless-app[bot]stainless-bot
authored andcommitted
chore: rebuild project due to codegen change (#7)
1 parent 370c08c commit 19b1231

File tree

4 files changed

+80
-50
lines changed

4 files changed

+80
-50
lines changed

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ dev-dependencies = [
5555
"dirty-equals>=0.6.0",
5656
"importlib-metadata>=6.7.0",
5757
"rich>=13.7.1",
58+
"nest_asyncio==1.6.0"
5859
]
5960

6061
[tool.rye.scripts]

requirements-dev.lock

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ mdurl==0.1.2
5151
mypy==1.13.0
5252
mypy-extensions==1.0.0
5353
# via mypy
54+
nest-asyncio==1.6.0
5455
nodeenv==1.8.0
5556
# via pyright
5657
nox==2023.4.22

src/sent/_utils/_sync.py

+40-50
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,62 @@
11
from __future__ import annotations
22

3+
import sys
4+
import asyncio
35
import functools
4-
from typing import TypeVar, Callable, Awaitable
6+
import contextvars
7+
from typing import Any, TypeVar, Callable, Awaitable
58
from typing_extensions import ParamSpec
69

7-
import anyio
8-
import anyio.to_thread
9-
10-
from ._reflection import function_has_argument
11-
1210
T_Retval = TypeVar("T_Retval")
1311
T_ParamSpec = ParamSpec("T_ParamSpec")
1412

1513

16-
# copied from `asyncer`, https://github.com/tiangolo/asyncer
17-
def asyncify(
18-
function: Callable[T_ParamSpec, T_Retval],
19-
*,
20-
cancellable: bool = False,
21-
limiter: anyio.CapacityLimiter | None = None,
22-
) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
14+
if sys.version_info >= (3, 9):
15+
to_thread = asyncio.to_thread
16+
else:
17+
# backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
18+
# for Python 3.8 support
19+
async def to_thread(
20+
func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
21+
) -> Any:
22+
"""Asynchronously run function *func* in a separate thread.
23+
24+
Any *args and **kwargs supplied for this function are directly passed
25+
to *func*. Also, the current :class:`contextvars.Context` is propagated,
26+
allowing context variables from the main thread to be accessed in the
27+
separate thread.
28+
29+
Returns a coroutine that can be awaited to get the eventual result of *func*.
30+
"""
31+
loop = asyncio.events.get_running_loop()
32+
ctx = contextvars.copy_context()
33+
func_call = functools.partial(ctx.run, func, *args, **kwargs)
34+
return await loop.run_in_executor(None, func_call)
35+
36+
37+
# inspired by `asyncer`, https://github.com/tiangolo/asyncer
38+
def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
2339
"""
2440
Take a blocking function and create an async one that receives the same
25-
positional and keyword arguments, and that when called, calls the original function
26-
in a worker thread using `anyio.to_thread.run_sync()`. Internally,
27-
`asyncer.asyncify()` uses the same `anyio.to_thread.run_sync()`, but it supports
28-
keyword arguments additional to positional arguments and it adds better support for
29-
autocompletion and inline errors for the arguments of the function called and the
30-
return value.
31-
32-
If the `cancellable` option is enabled and the task waiting for its completion is
33-
cancelled, the thread will still run its course but its return value (or any raised
34-
exception) will be ignored.
41+
positional and keyword arguments. For python version 3.9 and above, it uses
42+
asyncio.to_thread to run the function in a separate thread. For python version
43+
3.8, it uses locally defined copy of the asyncio.to_thread function which was
44+
introduced in python 3.9.
3545
36-
Use it like this:
46+
Usage:
3747
38-
```Python
39-
def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str:
40-
# Do work
41-
return "Some result"
48+
```python
49+
def blocking_func(arg1, arg2, kwarg1=None):
50+
# blocking code
51+
return result
4252
4353
44-
result = await to_thread.asyncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b")
45-
print(result)
54+
result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1)
4655
```
4756
4857
## Arguments
4958
5059
`function`: a blocking regular callable (e.g. a function)
51-
`cancellable`: `True` to allow cancellation of the operation
52-
`limiter`: capacity limiter to use to limit the total amount of threads running
53-
(if omitted, the default limiter is used)
5460
5561
## Return
5662
@@ -60,22 +66,6 @@ def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str:
6066
"""
6167

6268
async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval:
63-
partial_f = functools.partial(function, *args, **kwargs)
64-
65-
# In `v4.1.0` anyio added the `abandon_on_cancel` argument and deprecated the old
66-
# `cancellable` argument, so we need to use the new `abandon_on_cancel` to avoid
67-
# surfacing deprecation warnings.
68-
if function_has_argument(anyio.to_thread.run_sync, "abandon_on_cancel"):
69-
return await anyio.to_thread.run_sync(
70-
partial_f,
71-
abandon_on_cancel=cancellable,
72-
limiter=limiter,
73-
)
74-
75-
return await anyio.to_thread.run_sync(
76-
partial_f,
77-
cancellable=cancellable,
78-
limiter=limiter,
79-
)
69+
return await to_thread(function, *args, **kwargs)
8070

8171
return wrapper

tests/test_client.py

+38
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44

55
import gc
66
import os
7+
import sys
78
import json
89
import asyncio
910
import inspect
11+
import subprocess
1012
import tracemalloc
1113
from typing import Any, Union, cast
14+
from textwrap import dedent
1215
from unittest import mock
1316
from typing_extensions import Literal
1417

@@ -1545,3 +1548,38 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
15451548
response = await client.messages.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"})
15461549

15471550
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
1551+
1552+
def test_get_platform(self) -> None:
1553+
# A previous implementation of asyncify could leave threads unterminated when
1554+
# used with nest_asyncio.
1555+
#
1556+
# Since nest_asyncio.apply() is global and cannot be un-applied, this
1557+
# test is run in a separate process to avoid affecting other tests.
1558+
test_code = dedent("""
1559+
import asyncio
1560+
import nest_asyncio
1561+
import threading
1562+
1563+
from sent._utils import asyncify
1564+
from sent._base_client import get_platform
1565+
1566+
async def test_main() -> None:
1567+
result = await asyncify(get_platform)()
1568+
print(result)
1569+
for thread in threading.enumerate():
1570+
print(thread.name)
1571+
1572+
nest_asyncio.apply()
1573+
asyncio.run(test_main())
1574+
""")
1575+
with subprocess.Popen(
1576+
[sys.executable, "-c", test_code],
1577+
text=True,
1578+
) as process:
1579+
try:
1580+
process.wait(2)
1581+
if process.returncode:
1582+
raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
1583+
except subprocess.TimeoutExpired as e:
1584+
process.kill()
1585+
raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e

0 commit comments

Comments
 (0)