Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/openai/_utils/_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
is_required_type,
is_sequence_type,
is_annotated_type,
is_not_required_type,
strip_annotated_type,
)

Expand Down Expand Up @@ -117,8 +118,8 @@ def _get_annotated_type(type_: type) -> type | None:

This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]`
"""
if is_required_type(type_):
# Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]`
if is_required_type(type_) or is_not_required_type(type_):
# Unwrap Required or NotRequired to expose the Annotated metadata
type_ = get_args(type_)[0]

if is_annotated_type(type_):
Expand Down
9 changes: 7 additions & 2 deletions src/openai/_utils/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
TypeIs,
Required,
Annotated,
NotRequired,
get_args,
get_origin,
)
Expand Down Expand Up @@ -45,6 +46,10 @@ def is_required_type(typ: type) -> bool:
return get_origin(typ) == Required


def is_not_required_type(typ: type) -> bool:
return get_origin(typ) == NotRequired


def is_typevar(typ: type) -> bool:
# type ignore is required because type checkers
# think this expression will always return False
Expand All @@ -71,10 +76,10 @@ def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]:
return isinstance(tp, _TYPE_ALIAS_TYPES)


# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]]
# Extracts T from Annotated[T, ...], including Required and NotRequired wrappers
@lru_cache(maxsize=8096)
def strip_annotated_type(typ: type) -> type:
if is_required_type(typ) or is_annotated_type(typ):
if is_required_type(typ) or is_not_required_type(typ) or is_annotated_type(typ):
return strip_annotated_type(cast(type, get_args(typ)[0]))

return typ
Expand Down
23 changes: 22 additions & 1 deletion tests/test_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pathlib
from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast
from datetime import date, datetime
from typing_extensions import Required, Annotated, TypedDict
from typing_extensions import Required, Annotated, TypedDict, NotRequired

import pytest

Expand Down Expand Up @@ -490,3 +490,24 @@ async def test_strips_notgiven(use_async: bool) -> None:
async def test_strips_omit(use_async: bool) -> None:
assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"}
assert await transform({"foo_bar": omit}, Foo1, use_async) == {}


class DateDictWithNotRequiredAlias(TypedDict):
optional_prop: NotRequired[Annotated[date, PropertyInfo(format="iso8601", alias="prop")]]
nested: NotRequired[Bar2]
items: NotRequired[List[Bar2]]


@parametrize
@pytest.mark.asyncio
async def test_not_required_transforms(use_async: bool) -> None:
assert await transform(cast(Dict[str, Any], {}), DateDictWithNotRequiredAlias, use_async) == {}
assert await transform({"optional_prop": date(2023, 2, 23)}, DateDictWithNotRequiredAlias, use_async) == {
"prop": "2023-02-23"
}
assert await transform({"nested": {"this_thing": 1}}, DateDictWithNotRequiredAlias, use_async) == {
"nested": {"this__thing": 1}
}
assert await transform({"items": [{"this_thing": 1}]}, DateDictWithNotRequiredAlias, use_async) == {
"items": [{"this__thing": 1}]
}