Commit 31bfc124
Changed files (17)
src
openai
_utils
resources
beta
assistants
threads
chat
fine_tuning
tests
src/openai/_utils/__init__.py
@@ -44,5 +44,7 @@ from ._streams import consume_sync_iterator as consume_sync_iterator, consume_as
from ._transform import (
PropertyInfo as PropertyInfo,
transform as transform,
+ async_transform as async_transform,
maybe_transform as maybe_transform,
+ async_maybe_transform as async_maybe_transform,
)
src/openai/_utils/_transform.py
@@ -180,11 +180,7 @@ def _transform_recursive(
if isinstance(data, pydantic.BaseModel):
return model_dump(data, exclude_unset=True)
- return _transform_value(data, annotation)
-
-
-def _transform_value(data: object, type_: type) -> object:
- annotated_type = _get_annotated_type(type_)
+ annotated_type = _get_annotated_type(annotation)
if annotated_type is None:
return data
@@ -222,3 +218,125 @@ def _transform_typeddict(
else:
result[_maybe_transform_key(key, type_)] = _transform_recursive(value, annotation=type_)
return result
+
+
+async def async_maybe_transform(
+ data: object,
+ expected_type: object,
+) -> Any | None:
+ """Wrapper over `async_transform()` that allows `None` to be passed.
+
+ See `async_transform()` for more details.
+ """
+ if data is None:
+ return None
+ return await async_transform(data, expected_type)
+
+
+async def async_transform(
+ data: _T,
+ expected_type: object,
+) -> _T:
+ """Transform dictionaries based off of type information from the given type, for example:
+
+ ```py
+ class Params(TypedDict, total=False):
+ card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]]
+
+
+ transformed = transform({"card_id": "<my card ID>"}, Params)
+ # {'cardID': '<my card ID>'}
+ ```
+
+ Any keys / data that does not have type information given will be included as is.
+
+ It should be noted that the transformations that this function does are not represented in the type system.
+ """
+ transformed = await _async_transform_recursive(data, annotation=cast(type, expected_type))
+ return cast(_T, transformed)
+
+
+async def _async_transform_recursive(
+ data: object,
+ *,
+ annotation: type,
+ inner_type: type | None = None,
+) -> object:
+ """Transform the given data against the expected type.
+
+ Args:
+ annotation: The direct type annotation given to the particular piece of data.
+ This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc
+
+ inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type
+ is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in
+ the list can be transformed using the metadata from the container type.
+
+ Defaults to the same value as the `annotation` argument.
+ """
+ if inner_type is None:
+ inner_type = annotation
+
+ stripped_type = strip_annotated_type(inner_type)
+ if is_typeddict(stripped_type) and is_mapping(data):
+ return await _async_transform_typeddict(data, stripped_type)
+
+ if (
+ # List[T]
+ (is_list_type(stripped_type) and is_list(data))
+ # Iterable[T]
+ or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
+ ):
+ inner_type = extract_type_arg(stripped_type, 0)
+ return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
+
+ if is_union_type(stripped_type):
+ # For union types we run the transformation against all subtypes to ensure that everything is transformed.
+ #
+ # TODO: there may be edge cases where the same normalized field name will transform to two different names
+ # in different subtypes.
+ for subtype in get_args(stripped_type):
+ data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype)
+ return data
+
+ if isinstance(data, pydantic.BaseModel):
+ return model_dump(data, exclude_unset=True)
+
+ annotated_type = _get_annotated_type(annotation)
+ if annotated_type is None:
+ return data
+
+ # ignore the first argument as it is the actual type
+ annotations = get_args(annotated_type)[1:]
+ for annotation in annotations:
+ if isinstance(annotation, PropertyInfo) and annotation.format is not None:
+ return await _async_format_data(data, annotation.format, annotation.format_template)
+
+ return data
+
+
+async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object:
+ if isinstance(data, (date, datetime)):
+ if format_ == "iso8601":
+ return data.isoformat()
+
+ if format_ == "custom" and format_template is not None:
+ return data.strftime(format_template)
+
+ return data
+
+
+async def _async_transform_typeddict(
+ data: Mapping[str, object],
+ expected_type: type,
+) -> Mapping[str, object]:
+ result: dict[str, object] = {}
+ annotations = get_type_hints(expected_type, include_extras=True)
+ for key, value in data.items():
+ type_ = annotations.get(key)
+ if type_ is None:
+ # we do not have a type annotation for this field, leave it as is
+ result[key] = value
+ else:
+ result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_)
+ return result
src/openai/resources/audio/speech.py
@@ -9,7 +9,10 @@ import httpx
from ... import _legacy_response
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ..._utils import maybe_transform
+from ..._utils import (
+ maybe_transform,
+ async_maybe_transform,
+)
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -161,7 +164,7 @@ class AsyncSpeech(AsyncAPIResource):
extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
return await self._post(
"/audio/speech",
- body=maybe_transform(
+ body=await async_maybe_transform(
{
"input": input,
"model": model,
src/openai/resources/audio/transcriptions.py
@@ -9,7 +9,12 @@ import httpx
from ... import _legacy_response
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from ..._utils import extract_files, maybe_transform, deepcopy_minimal
+from ..._utils import (
+ extract_files,
+ maybe_transform,
+ deepcopy_minimal,
+ async_maybe_transform,
+)
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -200,7 +205,7 @@ class AsyncTranscriptions(AsyncAPIResource):
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/audio/transcriptions",
- body=maybe_transform(body, transcription_create_params.TranscriptionCreateParams),
+ body=await async_maybe_transform(body, transcription_create_params.TranscriptionCreateParams),
files=files,
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
src/openai/resources/audio/translations.py
@@ -9,7 +9,12 @@ import httpx
from ... import _legacy_response
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from ..._utils import extract_files, maybe_transform, deepcopy_minimal
+from ..._utils import (
+ extract_files,
+ maybe_transform,
+ deepcopy_minimal,
+ async_maybe_transform,
+)
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -174,7 +179,7 @@ class AsyncTranslations(AsyncAPIResource):
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/audio/translations",
- body=maybe_transform(body, translation_create_params.TranslationCreateParams),
+ body=await async_maybe_transform(body, translation_create_params.TranslationCreateParams),
files=files,
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
src/openai/resources/beta/assistants/assistants.py
@@ -17,7 +17,10 @@ from .files import (
AsyncFilesWithStreamingResponse,
)
from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ...._utils import maybe_transform
+from ...._utils import (
+ maybe_transform,
+ async_maybe_transform,
+)
from ...._compat import cached_property
from ...._resource import SyncAPIResource, AsyncAPIResource
from ...._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -410,7 +413,7 @@ class AsyncAssistants(AsyncAPIResource):
extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})}
return await self._post(
"/assistants",
- body=maybe_transform(
+ body=await async_maybe_transform(
{
"model": model,
"description": description,
@@ -525,7 +528,7 @@ class AsyncAssistants(AsyncAPIResource):
extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})}
return await self._post(
f"/assistants/{assistant_id}",
- body=maybe_transform(
+ body=await async_maybe_transform(
{
"description": description,
"file_ids": file_ids,
src/openai/resources/beta/assistants/files.py
@@ -8,7 +8,10 @@ import httpx
from .... import _legacy_response
from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ...._utils import maybe_transform
+from ...._utils import (
+ maybe_transform,
+ async_maybe_transform,
+)
from ...._compat import cached_property
from ...._resource import SyncAPIResource, AsyncAPIResource
from ...._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -259,7 +262,7 @@ class AsyncFiles(AsyncAPIResource):
extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})}
return await self._post(
f"/assistants/{assistant_id}/files",
- body=maybe_transform({"file_id": file_id}, file_create_params.FileCreateParams),
+ body=await async_maybe_transform({"file_id": file_id}, file_create_params.FileCreateParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
src/openai/resources/beta/threads/messages/messages.py
@@ -17,7 +17,10 @@ from .files import (
AsyncFilesWithStreamingResponse,
)
from ....._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ....._utils import maybe_transform
+from ....._utils import (
+ maybe_transform,
+ async_maybe_transform,
+)
from ....._compat import cached_property
from ....._resource import SyncAPIResource, AsyncAPIResource
from ....._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -315,7 +318,7 @@ class AsyncMessages(AsyncAPIResource):
extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})}
return await self._post(
f"/threads/{thread_id}/messages",
- body=maybe_transform(
+ body=await async_maybe_transform(
{
"content": content,
"role": role,
@@ -404,7 +407,7 @@ class AsyncMessages(AsyncAPIResource):
extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})}
return await self._post(
f"/threads/{thread_id}/messages/{message_id}",
- body=maybe_transform({"metadata": metadata}, message_update_params.MessageUpdateParams),
+ body=await async_maybe_transform({"metadata": metadata}, message_update_params.MessageUpdateParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
src/openai/resources/beta/threads/runs/runs.py
@@ -17,7 +17,10 @@ from .steps import (
AsyncStepsWithStreamingResponse,
)
from ....._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ....._utils import maybe_transform
+from ....._utils import (
+ maybe_transform,
+ async_maybe_transform,
+)
from ....._compat import cached_property
from ....._resource import SyncAPIResource, AsyncAPIResource
from ....._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -430,7 +433,7 @@ class AsyncRuns(AsyncAPIResource):
extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})}
return await self._post(
f"/threads/{thread_id}/runs",
- body=maybe_transform(
+ body=await async_maybe_transform(
{
"assistant_id": assistant_id,
"additional_instructions": additional_instructions,
@@ -521,7 +524,7 @@ class AsyncRuns(AsyncAPIResource):
extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})}
return await self._post(
f"/threads/{thread_id}/runs/{run_id}",
- body=maybe_transform({"metadata": metadata}, run_update_params.RunUpdateParams),
+ body=await async_maybe_transform({"metadata": metadata}, run_update_params.RunUpdateParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -669,7 +672,7 @@ class AsyncRuns(AsyncAPIResource):
extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})}
return await self._post(
f"/threads/{thread_id}/runs/{run_id}/submit_tool_outputs",
- body=maybe_transform(
+ body=await async_maybe_transform(
{"tool_outputs": tool_outputs}, run_submit_tool_outputs_params.RunSubmitToolOutputsParams
),
options=make_request_options(
src/openai/resources/beta/threads/threads.py
@@ -24,7 +24,10 @@ from .messages import (
AsyncMessagesWithStreamingResponse,
)
from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ...._utils import maybe_transform
+from ...._utils import (
+ maybe_transform,
+ async_maybe_transform,
+)
from .runs.runs import Runs, AsyncRuns
from ...._compat import cached_property
from ...._resource import SyncAPIResource, AsyncAPIResource
@@ -342,7 +345,7 @@ class AsyncThreads(AsyncAPIResource):
extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})}
return await self._post(
"/threads",
- body=maybe_transform(
+ body=await async_maybe_transform(
{
"messages": messages,
"metadata": metadata,
@@ -423,7 +426,7 @@ class AsyncThreads(AsyncAPIResource):
extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})}
return await self._post(
f"/threads/{thread_id}",
- body=maybe_transform({"metadata": metadata}, thread_update_params.ThreadUpdateParams),
+ body=await async_maybe_transform({"metadata": metadata}, thread_update_params.ThreadUpdateParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -517,7 +520,7 @@ class AsyncThreads(AsyncAPIResource):
extra_headers = {"OpenAI-Beta": "assistants=v1", **(extra_headers or {})}
return await self._post(
"/threads/runs",
- body=maybe_transform(
+ body=await async_maybe_transform(
{
"assistant_id": assistant_id,
"instructions": instructions,
src/openai/resources/chat/completions.py
@@ -9,7 +9,11 @@ import httpx
from ... import _legacy_response
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ..._utils import required_args, maybe_transform
+from ..._utils import (
+ required_args,
+ maybe_transform,
+ async_maybe_transform,
+)
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -1329,7 +1333,7 @@ class AsyncCompletions(AsyncAPIResource):
) -> ChatCompletion | AsyncStream[ChatCompletionChunk]:
return await self._post(
"/chat/completions",
- body=maybe_transform(
+ body=await async_maybe_transform(
{
"messages": messages,
"model": model,
src/openai/resources/fine_tuning/jobs.py
@@ -9,7 +9,10 @@ import httpx
from ... import _legacy_response
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ..._utils import maybe_transform
+from ..._utils import (
+ maybe_transform,
+ async_maybe_transform,
+)
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -369,7 +372,7 @@ class AsyncJobs(AsyncAPIResource):
"""
return await self._post(
"/fine_tuning/jobs",
- body=maybe_transform(
+ body=await async_maybe_transform(
{
"model": model,
"training_file": training_file,
src/openai/resources/completions.py
@@ -10,7 +10,11 @@ import httpx
from .. import _legacy_response
from ..types import Completion, completion_create_params
from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from .._utils import required_args, maybe_transform
+from .._utils import (
+ required_args,
+ maybe_transform,
+ async_maybe_transform,
+)
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -1019,7 +1023,7 @@ class AsyncCompletions(AsyncAPIResource):
) -> Completion | AsyncStream[Completion]:
return await self._post(
"/completions",
- body=maybe_transform(
+ body=await async_maybe_transform(
{
"model": model,
"prompt": prompt,
src/openai/resources/files.py
@@ -12,7 +12,12 @@ import httpx
from .. import _legacy_response
from ..types import FileObject, FileDeleted, file_list_params, file_create_params
from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from .._utils import extract_files, maybe_transform, deepcopy_minimal
+from .._utils import (
+ extract_files,
+ maybe_transform,
+ deepcopy_minimal,
+ async_maybe_transform,
+)
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
@@ -374,7 +379,7 @@ class AsyncFiles(AsyncAPIResource):
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/files",
- body=maybe_transform(body, file_create_params.FileCreateParams),
+ body=await async_maybe_transform(body, file_create_params.FileCreateParams),
files=files,
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
src/openai/resources/images.py
@@ -15,7 +15,12 @@ from ..types import (
image_create_variation_params,
)
from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from .._utils import extract_files, maybe_transform, deepcopy_minimal
+from .._utils import (
+ extract_files,
+ maybe_transform,
+ deepcopy_minimal,
+ async_maybe_transform,
+)
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -345,7 +350,7 @@ class AsyncImages(AsyncAPIResource):
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/images/variations",
- body=maybe_transform(body, image_create_variation_params.ImageCreateVariationParams),
+ body=await async_maybe_transform(body, image_create_variation_params.ImageCreateVariationParams),
files=files,
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -428,7 +433,7 @@ class AsyncImages(AsyncAPIResource):
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/images/edits",
- body=maybe_transform(body, image_edit_params.ImageEditParams),
+ body=await async_maybe_transform(body, image_edit_params.ImageEditParams),
files=files,
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -496,7 +501,7 @@ class AsyncImages(AsyncAPIResource):
"""
return await self._post(
"/images/generations",
- body=maybe_transform(
+ body=await async_maybe_transform(
{
"prompt": prompt,
"model": model,
src/openai/resources/moderations.py
@@ -10,7 +10,10 @@ import httpx
from .. import _legacy_response
from ..types import ModerationCreateResponse, moderation_create_params
from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from .._utils import maybe_transform
+from .._utils import (
+ maybe_transform,
+ async_maybe_transform,
+)
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -127,7 +130,7 @@ class AsyncModerations(AsyncAPIResource):
"""
return await self._post(
"/moderations",
- body=maybe_transform(
+ body=await async_maybe_transform(
{
"input": input,
"model": model,
tests/test_transform.py
@@ -1,22 +1,45 @@
from __future__ import annotations
-from typing import Any, List, Union, Iterable, Optional, cast
+from typing import Any, List, Union, TypeVar, Iterable, Optional, cast
from datetime import date, datetime
from typing_extensions import Required, Annotated, TypedDict
import pytest
-from openai._utils import PropertyInfo, transform, parse_datetime
+from openai._utils import (
+ PropertyInfo,
+ transform as _transform,
+ parse_datetime,
+ async_transform as _async_transform,
+)
from openai._compat import PYDANTIC_V2
from openai._models import BaseModel
+_T = TypeVar("_T")
+
+
+async def transform(
+ data: _T,
+ expected_type: object,
+ use_async: bool,
+) -> _T:
+ if use_async:
+ return await _async_transform(data, expected_type=expected_type)
+
+ return _transform(data, expected_type=expected_type)
+
+
+parametrize = pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"])
+
class Foo1(TypedDict):
foo_bar: Annotated[str, PropertyInfo(alias="fooBar")]
-def test_top_level_alias() -> None:
- assert transform({"foo_bar": "hello"}, expected_type=Foo1) == {"fooBar": "hello"}
+@parametrize
+@pytest.mark.asyncio
+async def test_top_level_alias(use_async: bool) -> None:
+ assert await transform({"foo_bar": "hello"}, expected_type=Foo1, use_async=use_async) == {"fooBar": "hello"}
class Foo2(TypedDict):
@@ -32,9 +55,11 @@ class Baz2(TypedDict):
my_baz: Annotated[str, PropertyInfo(alias="myBaz")]
-def test_recursive_typeddict() -> None:
- assert transform({"bar": {"this_thing": 1}}, Foo2) == {"bar": {"this__thing": 1}}
- assert transform({"bar": {"baz": {"my_baz": "foo"}}}, Foo2) == {"bar": {"Baz": {"myBaz": "foo"}}}
+@parametrize
+@pytest.mark.asyncio
+async def test_recursive_typeddict(use_async: bool) -> None:
+ assert await transform({"bar": {"this_thing": 1}}, Foo2, use_async) == {"bar": {"this__thing": 1}}
+ assert await transform({"bar": {"baz": {"my_baz": "foo"}}}, Foo2, use_async) == {"bar": {"Baz": {"myBaz": "foo"}}}
class Foo3(TypedDict):
@@ -45,8 +70,10 @@ class Bar3(TypedDict):
my_field: Annotated[str, PropertyInfo(alias="myField")]
-def test_list_of_typeddict() -> None:
- result = transform({"things": [{"my_field": "foo"}, {"my_field": "foo2"}]}, expected_type=Foo3)
+@parametrize
+@pytest.mark.asyncio
+async def test_list_of_typeddict(use_async: bool) -> None:
+ result = await transform({"things": [{"my_field": "foo"}, {"my_field": "foo2"}]}, Foo3, use_async)
assert result == {"things": [{"myField": "foo"}, {"myField": "foo2"}]}
@@ -62,10 +89,14 @@ class Baz4(TypedDict):
foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")]
-def test_union_of_typeddict() -> None:
- assert transform({"foo": {"foo_bar": "bar"}}, Foo4) == {"foo": {"fooBar": "bar"}}
- assert transform({"foo": {"foo_baz": "baz"}}, Foo4) == {"foo": {"fooBaz": "baz"}}
- assert transform({"foo": {"foo_baz": "baz", "foo_bar": "bar"}}, Foo4) == {"foo": {"fooBaz": "baz", "fooBar": "bar"}}
+@parametrize
+@pytest.mark.asyncio
+async def test_union_of_typeddict(use_async: bool) -> None:
+ assert await transform({"foo": {"foo_bar": "bar"}}, Foo4, use_async) == {"foo": {"fooBar": "bar"}}
+ assert await transform({"foo": {"foo_baz": "baz"}}, Foo4, use_async) == {"foo": {"fooBaz": "baz"}}
+ assert await transform({"foo": {"foo_baz": "baz", "foo_bar": "bar"}}, Foo4, use_async) == {
+ "foo": {"fooBaz": "baz", "fooBar": "bar"}
+ }
class Foo5(TypedDict):
@@ -80,9 +111,11 @@ class Baz5(TypedDict):
foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")]
-def test_union_of_list() -> None:
- assert transform({"foo": {"foo_bar": "bar"}}, Foo5) == {"FOO": {"fooBar": "bar"}}
- assert transform(
+@parametrize
+@pytest.mark.asyncio
+async def test_union_of_list(use_async: bool) -> None:
+ assert await transform({"foo": {"foo_bar": "bar"}}, Foo5, use_async) == {"FOO": {"fooBar": "bar"}}
+ assert await transform(
{
"foo": [
{"foo_baz": "baz"},
@@ -90,6 +123,7 @@ def test_union_of_list() -> None:
]
},
Foo5,
+ use_async,
) == {"FOO": [{"fooBaz": "baz"}, {"fooBaz": "baz"}]}
@@ -97,8 +131,10 @@ class Foo6(TypedDict):
bar: Annotated[str, PropertyInfo(alias="Bar")]
-def test_includes_unknown_keys() -> None:
- assert transform({"bar": "bar", "baz_": {"FOO": 1}}, Foo6) == {
+@parametrize
+@pytest.mark.asyncio
+async def test_includes_unknown_keys(use_async: bool) -> None:
+ assert await transform({"bar": "bar", "baz_": {"FOO": 1}}, Foo6, use_async) == {
"Bar": "bar",
"baz_": {"FOO": 1},
}
@@ -113,9 +149,11 @@ class Bar7(TypedDict):
foo: str
-def test_ignores_invalid_input() -> None:
- assert transform({"bar": "<foo>"}, Foo7) == {"bAr": "<foo>"}
- assert transform({"foo": "<foo>"}, Foo7) == {"foo": "<foo>"}
+@parametrize
+@pytest.mark.asyncio
+async def test_ignores_invalid_input(use_async: bool) -> None:
+ assert await transform({"bar": "<foo>"}, Foo7, use_async) == {"bAr": "<foo>"}
+ assert await transform({"foo": "<foo>"}, Foo7, use_async) == {"foo": "<foo>"}
class DatetimeDict(TypedDict, total=False):
@@ -134,52 +172,66 @@ class DateDict(TypedDict, total=False):
foo: Annotated[date, PropertyInfo(format="iso8601")]
-def test_iso8601_format() -> None:
+@parametrize
+@pytest.mark.asyncio
+async def test_iso8601_format(use_async: bool) -> None:
dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")
- assert transform({"foo": dt}, DatetimeDict) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap]
+ assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap]
dt = dt.replace(tzinfo=None)
- assert transform({"foo": dt}, DatetimeDict) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap]
+ assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap]
- assert transform({"foo": None}, DateDict) == {"foo": None} # type: ignore[comparison-overlap]
- assert transform({"foo": date.fromisoformat("2023-02-23")}, DateDict) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap]
+ assert await transform({"foo": None}, DateDict, use_async) == {"foo": None} # type: ignore[comparison-overlap]
+ assert await transform({"foo": date.fromisoformat("2023-02-23")}, DateDict, use_async) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap]
-def test_optional_iso8601_format() -> None:
+@parametrize
+@pytest.mark.asyncio
+async def test_optional_iso8601_format(use_async: bool) -> None:
dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")
- assert transform({"bar": dt}, DatetimeDict) == {"bar": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap]
+ assert await transform({"bar": dt}, DatetimeDict, use_async) == {"bar": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap]
- assert transform({"bar": None}, DatetimeDict) == {"bar": None}
+ assert await transform({"bar": None}, DatetimeDict, use_async) == {"bar": None}
-def test_required_iso8601_format() -> None:
+@parametrize
+@pytest.mark.asyncio
+async def test_required_iso8601_format(use_async: bool) -> None:
dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")
- assert transform({"required": dt}, DatetimeDict) == {"required": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap]
+ assert await transform({"required": dt}, DatetimeDict, use_async) == {
+ "required": "2023-02-23T14:16:36.337692+00:00"
+ } # type: ignore[comparison-overlap]
- assert transform({"required": None}, DatetimeDict) == {"required": None}
+ assert await transform({"required": None}, DatetimeDict, use_async) == {"required": None}
-def test_union_datetime() -> None:
+@parametrize
+@pytest.mark.asyncio
+async def test_union_datetime(use_async: bool) -> None:
dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")
- assert transform({"union": dt}, DatetimeDict) == { # type: ignore[comparison-overlap]
+ assert await transform({"union": dt}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap]
"union": "2023-02-23T14:16:36.337692+00:00"
}
- assert transform({"union": "foo"}, DatetimeDict) == {"union": "foo"}
+ assert await transform({"union": "foo"}, DatetimeDict, use_async) == {"union": "foo"}
-def test_nested_list_iso6801_format() -> None:
+@parametrize
+@pytest.mark.asyncio
+async def test_nested_list_iso6801_format(use_async: bool) -> None:
dt1 = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")
dt2 = parse_datetime("2022-01-15T06:34:23Z")
- assert transform({"list_": [dt1, dt2]}, DatetimeDict) == { # type: ignore[comparison-overlap]
+ assert await transform({"list_": [dt1, dt2]}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap]
"list_": ["2023-02-23T14:16:36.337692+00:00", "2022-01-15T06:34:23+00:00"]
}
-def test_datetime_custom_format() -> None:
+@parametrize
+@pytest.mark.asyncio
+async def test_datetime_custom_format(use_async: bool) -> None:
dt = parse_datetime("2022-01-15T06:34:23Z")
- result = transform(dt, Annotated[datetime, PropertyInfo(format="custom", format_template="%H")])
+ result = await transform(dt, Annotated[datetime, PropertyInfo(format="custom", format_template="%H")], use_async)
assert result == "06" # type: ignore[comparison-overlap]
@@ -187,47 +239,59 @@ class DateDictWithRequiredAlias(TypedDict, total=False):
required_prop: Required[Annotated[date, PropertyInfo(format="iso8601", alias="prop")]]
-def test_datetime_with_alias() -> None:
- assert transform({"required_prop": None}, DateDictWithRequiredAlias) == {"prop": None} # type: ignore[comparison-overlap]
- assert transform({"required_prop": date.fromisoformat("2023-02-23")}, DateDictWithRequiredAlias) == {
- "prop": "2023-02-23"
- } # type: ignore[comparison-overlap]
+@parametrize
+@pytest.mark.asyncio
+async def test_datetime_with_alias(use_async: bool) -> None:
+ assert await transform({"required_prop": None}, DateDictWithRequiredAlias, use_async) == {"prop": None} # type: ignore[comparison-overlap]
+ assert await transform(
+ {"required_prop": date.fromisoformat("2023-02-23")}, DateDictWithRequiredAlias, use_async
+ ) == {"prop": "2023-02-23"} # type: ignore[comparison-overlap]
class MyModel(BaseModel):
foo: str
-def test_pydantic_model_to_dictionary() -> None:
- assert transform(MyModel(foo="hi!"), Any) == {"foo": "hi!"}
- assert transform(MyModel.construct(foo="hi!"), Any) == {"foo": "hi!"}
+@parametrize
+@pytest.mark.asyncio
+async def test_pydantic_model_to_dictionary(use_async: bool) -> None:
+ assert await transform(MyModel(foo="hi!"), Any, use_async) == {"foo": "hi!"}
+ assert await transform(MyModel.construct(foo="hi!"), Any, use_async) == {"foo": "hi!"}
-def test_pydantic_empty_model() -> None:
- assert transform(MyModel.construct(), Any) == {}
+@parametrize
+@pytest.mark.asyncio
+async def test_pydantic_empty_model(use_async: bool) -> None:
+ assert await transform(MyModel.construct(), Any, use_async) == {}
-def test_pydantic_unknown_field() -> None:
- assert transform(MyModel.construct(my_untyped_field=True), Any) == {"my_untyped_field": True}
+@parametrize
+@pytest.mark.asyncio
+async def test_pydantic_unknown_field(use_async: bool) -> None:
+ assert await transform(MyModel.construct(my_untyped_field=True), Any, use_async) == {"my_untyped_field": True}
-def test_pydantic_mismatched_types() -> None:
+@parametrize
+@pytest.mark.asyncio
+async def test_pydantic_mismatched_types(use_async: bool) -> None:
model = MyModel.construct(foo=True)
if PYDANTIC_V2:
with pytest.warns(UserWarning):
- params = transform(model, Any)
+ params = await transform(model, Any, use_async)
else:
- params = transform(model, Any)
+ params = await transform(model, Any, use_async)
assert params == {"foo": True}
-def test_pydantic_mismatched_object_type() -> None:
+@parametrize
+@pytest.mark.asyncio
+async def test_pydantic_mismatched_object_type(use_async: bool) -> None:
model = MyModel.construct(foo=MyModel.construct(hello="world"))
if PYDANTIC_V2:
with pytest.warns(UserWarning):
- params = transform(model, Any)
+ params = await transform(model, Any, use_async)
else:
- params = transform(model, Any)
+ params = await transform(model, Any, use_async)
assert params == {"foo": {"hello": "world"}}
@@ -235,10 +299,12 @@ class ModelNestedObjects(BaseModel):
nested: MyModel
-def test_pydantic_nested_objects() -> None:
+@parametrize
+@pytest.mark.asyncio
+async def test_pydantic_nested_objects(use_async: bool) -> None:
model = ModelNestedObjects.construct(nested={"foo": "stainless"})
assert isinstance(model.nested, MyModel)
- assert transform(model, Any) == {"nested": {"foo": "stainless"}}
+ assert await transform(model, Any, use_async) == {"nested": {"foo": "stainless"}}
class ModelWithDefaultField(BaseModel):
@@ -247,24 +313,26 @@ class ModelWithDefaultField(BaseModel):
with_str_default: str = "foo"
-def test_pydantic_default_field() -> None:
+@parametrize
+@pytest.mark.asyncio
+async def test_pydantic_default_field(use_async: bool) -> None:
# should be excluded when defaults are used
model = ModelWithDefaultField.construct()
assert model.with_none_default is None
assert model.with_str_default == "foo"
- assert transform(model, Any) == {}
+ assert await transform(model, Any, use_async) == {}
# should be included when the default value is explicitly given
model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo")
assert model.with_none_default is None
assert model.with_str_default == "foo"
- assert transform(model, Any) == {"with_none_default": None, "with_str_default": "foo"}
+ assert await transform(model, Any, use_async) == {"with_none_default": None, "with_str_default": "foo"}
# should be included when a non-default value is explicitly given
model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz")
assert model.with_none_default == "bar"
assert model.with_str_default == "baz"
- assert transform(model, Any) == {"with_none_default": "bar", "with_str_default": "baz"}
+ assert await transform(model, Any, use_async) == {"with_none_default": "bar", "with_str_default": "baz"}
class TypedDictIterableUnion(TypedDict):
@@ -279,21 +347,33 @@ class Baz8(TypedDict):
foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")]
-def test_iterable_of_dictionaries() -> None:
- assert transform({"foo": [{"foo_baz": "bar"}]}, TypedDictIterableUnion) == {"FOO": [{"fooBaz": "bar"}]}
- assert cast(Any, transform({"foo": ({"foo_baz": "bar"},)}, TypedDictIterableUnion)) == {"FOO": [{"fooBaz": "bar"}]}
+@parametrize
+@pytest.mark.asyncio
+async def test_iterable_of_dictionaries(use_async: bool) -> None:
+ assert await transform({"foo": [{"foo_baz": "bar"}]}, TypedDictIterableUnion, use_async) == {
+ "FOO": [{"fooBaz": "bar"}]
+ }
+ assert cast(Any, await transform({"foo": ({"foo_baz": "bar"},)}, TypedDictIterableUnion, use_async)) == {
+ "FOO": [{"fooBaz": "bar"}]
+ }
def my_iter() -> Iterable[Baz8]:
yield {"foo_baz": "hello"}
yield {"foo_baz": "world"}
- assert transform({"foo": my_iter()}, TypedDictIterableUnion) == {"FOO": [{"fooBaz": "hello"}, {"fooBaz": "world"}]}
+ assert await transform({"foo": my_iter()}, TypedDictIterableUnion, use_async) == {
+ "FOO": [{"fooBaz": "hello"}, {"fooBaz": "world"}]
+ }
class TypedDictIterableUnionStr(TypedDict):
foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")]
-def test_iterable_union_str() -> None:
- assert transform({"foo": "bar"}, TypedDictIterableUnionStr) == {"FOO": "bar"}
- assert cast(Any, transform(iter([{"foo_baz": "bar"}]), Union[str, Iterable[Baz8]])) == [{"fooBaz": "bar"}]
+@parametrize
+@pytest.mark.asyncio
+async def test_iterable_union_str(use_async: bool) -> None:
+ assert await transform({"foo": "bar"}, TypedDictIterableUnionStr, use_async) == {"FOO": "bar"}
+ assert cast(Any, await transform(iter([{"foo_baz": "bar"}]), Union[str, Iterable[Baz8]], use_async)) == [
+ {"fooBaz": "bar"}
+ ]