Skip to content

Commit 629bde5

Browse files
chore(internal): add internal helpers (#1092)
1 parent 15488ce commit 629bde5

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

src/openai/_compat.py

+38-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any, Union, TypeVar, cast
3+
from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload
44
from datetime import date, datetime
5+
from typing_extensions import Self
56

67
import pydantic
78
from pydantic.fields import FieldInfo
89

910
from ._types import StrBytesIntFloat
1011

12+
_T = TypeVar("_T")
1113
_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel)
1214

1315
# --------------- Pydantic v2 compatibility ---------------
@@ -178,8 +180,43 @@ class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel):
178180
# cached properties
179181
if TYPE_CHECKING:
180182
cached_property = property
183+
184+
# we define a separate type (copied from typeshed)
185+
# that represents that `cached_property` is `set`able
186+
# at runtime, which differs from `@property`.
187+
#
188+
# this is a separate type as editors likely special case
189+
# `@property` and we don't want to cause issues just to have
190+
# more helpful internal types.
191+
192+
class typed_cached_property(Generic[_T]):
193+
func: Callable[[Any], _T]
194+
attrname: str | None
195+
196+
def __init__(self, func: Callable[[Any], _T]) -> None:
197+
...
198+
199+
@overload
200+
def __get__(self, instance: None, owner: type[Any] | None = None) -> Self:
201+
...
202+
203+
@overload
204+
def __get__(self, instance: object, owner: type[Any] | None = None) -> _T:
205+
...
206+
207+
def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self:
208+
raise NotImplementedError()
209+
210+
def __set_name__(self, owner: type[Any], name: str) -> None:
211+
...
212+
213+
# __set__ is not defined at runtime, but @cached_property is designed to be settable
214+
def __set__(self, instance: object, value: _T) -> None:
215+
...
181216
else:
182217
try:
183218
from functools import cached_property as cached_property
184219
except ImportError:
185220
from cached_property import cached_property as cached_property
221+
222+
typed_cached_property = cached_property

src/openai/_utils/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from ._sync import asyncify as asyncify
12
from ._proxy import LazyProxy as LazyProxy
23
from ._utils import (
34
flatten as flatten,

src/openai/_utils/_sync.py

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
import functools
4+
from typing import TypeVar, Callable, Awaitable
5+
from typing_extensions import ParamSpec
6+
7+
import anyio
8+
import anyio.to_thread
9+
10+
T_Retval = TypeVar("T_Retval")
11+
T_ParamSpec = ParamSpec("T_ParamSpec")
12+
13+
14+
# copied from `asyncer`, https://github.com/tiangolo/asyncer
15+
def asyncify(
16+
function: Callable[T_ParamSpec, T_Retval],
17+
*,
18+
cancellable: bool = False,
19+
limiter: anyio.CapacityLimiter | None = None,
20+
) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
21+
"""
22+
Take a blocking function and create an async one that receives the same
23+
positional and keyword arguments, and that when called, calls the original function
24+
in a worker thread using `anyio.to_thread.run_sync()`. Internally,
25+
`asyncer.asyncify()` uses the same `anyio.to_thread.run_sync()`, but it supports
26+
keyword arguments additional to positional arguments and it adds better support for
27+
autocompletion and inline errors for the arguments of the function called and the
28+
return value.
29+
30+
If the `cancellable` option is enabled and the task waiting for its completion is
31+
cancelled, the thread will still run its course but its return value (or any raised
32+
exception) will be ignored.
33+
34+
Use it like this:
35+
36+
```Python
37+
def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str:
38+
# Do work
39+
return "Some result"
40+
41+
42+
result = await to_thread.asyncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b")
43+
print(result)
44+
```
45+
46+
## Arguments
47+
48+
`function`: a blocking regular callable (e.g. a function)
49+
`cancellable`: `True` to allow cancellation of the operation
50+
`limiter`: capacity limiter to use to limit the total amount of threads running
51+
(if omitted, the default limiter is used)
52+
53+
## Return
54+
55+
An async function that takes the same positional and keyword arguments as the
56+
original one, that when called runs the same original function in a thread worker
57+
and returns the result.
58+
"""
59+
60+
async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval:
61+
partial_f = functools.partial(function, *args, **kwargs)
62+
return await anyio.to_thread.run_sync(partial_f, cancellable=cancellable, limiter=limiter)
63+
64+
return wrapper

0 commit comments

Comments
 (0)