From 3c800d5ae6af956f94c5aae90767511a96c5d42b Mon Sep 17 00:00:00 2001 From: Pranjal Bhatia <233476158+pranjalbhatia710@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:30:23 +0400 Subject: [PATCH 1/2] fix(files): normalize PathLike upload tuples --- src/openai/_files.py | 4 +--- tests/test_files.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/openai/_files.py b/src/openai/_files.py index 1a2cc77478..cb8fcbebf8 100644 --- a/src/openai/_files.py +++ b/src/openai/_files.py @@ -27,9 +27,7 @@ def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: def is_file_content(obj: object) -> TypeGuard[FileContent]: - return ( - isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) - ) + return isinstance(obj, bytes) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) def assert_is_file_content(obj: object, *, key: str | None = None) -> None: diff --git a/tests/test_files.py b/tests/test_files.py index 56445fb550..60ad480783 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -22,6 +22,18 @@ def test_tuple_input() -> None: assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) +def test_file_tuple_with_pathlike_content() -> None: + result = to_httpx_files({"file": ("custom-name.md", readme_path)}) + print(result) + assert result == IsDict({"file": IsTuple("custom-name.md", IsBytes())}) + + +def test_file_tuple_with_pathlike_content_and_metadata() -> None: + result = to_httpx_files({"file": ("custom-name.md", readme_path, "text/markdown")}) + print(result) + assert result == IsDict({"file": IsTuple("custom-name.md", IsBytes(), "text/markdown")}) + + @pytest.mark.asyncio async def test_async_pathlib_includes_file_name() -> None: result = await async_to_httpx_files({"file": readme_path}) @@ -43,6 +55,20 @@ async def test_async_tuple_input() -> None: assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) +@pytest.mark.asyncio +async def test_async_file_tuple_with_pathlike_content() -> None: + result = await async_to_httpx_files({"file": ("custom-name.md", readme_path)}) + print(result) + assert result == IsDict({"file": IsTuple("custom-name.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_file_tuple_with_pathlike_content_and_metadata() -> None: + result = await async_to_httpx_files({"file": ("custom-name.md", readme_path, "text/markdown")}) + print(result) + assert result == IsDict({"file": IsTuple("custom-name.md", IsBytes(), "text/markdown")}) + + def test_string_not_allowed() -> None: with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): to_httpx_files( From 2f0860ff319e8b8119d0e32566831f24a4a25cfc Mon Sep 17 00:00:00 2001 From: Pranjal Bhatia Date: Wed, 8 Jul 2026 10:03:49 +0400 Subject: [PATCH 2/2] fix(files): preserve tuple uploads during extraction --- src/openai/_files.py | 2 +- tests/test_files.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/openai/_files.py b/src/openai/_files.py index cb8fcbebf8..76f71238ff 100644 --- a/src/openai/_files.py +++ b/src/openai/_files.py @@ -31,7 +31,7 @@ def is_file_content(obj: object) -> TypeGuard[FileContent]: def assert_is_file_content(obj: object, *, key: str | None = None) -> None: - if not is_file_content(obj): + if not is_file_content(obj) and not is_tuple_t(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/openai/openai-python/tree/main#file-uploads" diff --git a/tests/test_files.py b/tests/test_files.py index 60ad480783..180643e312 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -143,6 +143,17 @@ def test_extract_files_does_not_mutate_original_top_level(self) -> None: assert original == {"file": file_bytes, "other": "value"} assert copied == {"other": "value"} + def test_extract_files_accepts_file_tuple(self) -> None: + file_tuple = ("custom-name.jsonl", b"contents", "application/jsonl") + original = {"file": file_tuple, "purpose": "batch"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_tuple)] + assert original == {"file": file_tuple, "purpose": "batch"} + assert copied == {"purpose": "batch"} + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: file1 = b"f1" file2 = b"f2"