From e851675b1655ed59be9a439eb7840724a7719f51 Mon Sep 17 00:00:00 2001 From: Daniel Gaskins Date: Mon, 21 Sep 2026 10:54:09 -0700 Subject: [PATCH] fix(files): read PathLike content in upload tuples --- src/browserbase/_files.py | 6 ++---- tests/test_files.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/browserbase/_files.py b/src/browserbase/_files.py index 8042111f..e9e272b8 100644 --- a/src/browserbase/_files.py +++ b/src/browserbase/_files.py @@ -27,13 +27,11 @@ 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: - 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/browserbase/sdk-python/tree/main#file-uploads" diff --git a/tests/test_files.py b/tests/test_files.py index 713c5994..cb2fbc42 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -5,6 +5,7 @@ from dirty_equals import IsDict, IsList, IsBytes, IsTuple from browserbase._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files +from browserbase._types import FileTypes from browserbase._utils import extract_files readme_path = Path(__file__).parent.parent.joinpath("README.md") @@ -22,6 +23,21 @@ def test_tuple_input() -> None: assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) +@pytest.mark.parametrize( + "file", + [ + ("custom.md", readme_path), + ("custom.md", readme_path, "text/markdown"), + ("custom.md", readme_path, "text/markdown", {"X-Test": "true"}), + ], +) +def test_pathlib_inside_file_tuple(file: FileTypes) -> None: + result = to_httpx_files({"file": file}) + + assert isinstance(file, tuple) + assert result == {"file": (file[0], readme_path.read_bytes(), *file[2:])} + + @pytest.mark.asyncio async def test_async_pathlib_includes_file_name() -> None: result = await async_to_httpx_files({"file": readme_path}) @@ -43,6 +59,22 @@ async def test_async_tuple_input() -> None: assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) +@pytest.mark.parametrize( + "file", + [ + ("custom.md", readme_path), + ("custom.md", readme_path, "text/markdown"), + ("custom.md", readme_path, "text/markdown", {"X-Test": "true"}), + ], +) +@pytest.mark.asyncio +async def test_async_pathlib_inside_file_tuple(file: FileTypes) -> None: + result = await async_to_httpx_files({"file": file}) + + assert isinstance(file, tuple) + assert result == {"file": (file[0], readme_path.read_bytes(), *file[2:])} + + 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(