diff --git a/src/openai/_utils/_transform.py b/src/openai/_utils/_transform.py index 304fd12ffe..f83045dcdc 100644 --- a/src/openai/_utils/_transform.py +++ b/src/openai/_utils/_transform.py @@ -27,6 +27,7 @@ is_required_type, is_sequence_type, is_annotated_type, + is_not_required_type, strip_annotated_type, ) @@ -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_): diff --git a/src/openai/_utils/_typing.py b/src/openai/_utils/_typing.py index 193109f3ad..ed43d9d64c 100644 --- a/src/openai/_utils/_typing.py +++ b/src/openai/_utils/_typing.py @@ -9,6 +9,7 @@ TypeIs, Required, Annotated, + NotRequired, get_args, get_origin, ) @@ -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 @@ -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 diff --git a/tests/test_transform.py b/tests/test_transform.py index 93f7ad8dd8..f1f06d1ae8 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -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 @@ -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}] + }