Skip to content

Commit 32ed2f8

Browse files
authored
Merge branch 'main' into feature/default-parameter-values
2 parents 0eb7ddd + 1b28c0f commit 32ed2f8

File tree

3 files changed

+70
-60
lines changed

3 files changed

+70
-60
lines changed

src/lmstudio/history.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from typing_extensions import (
2626
# Native in 3.11+
2727
Self,
28+
# Native in Python 3.12+
29+
Buffer,
2830
# Native in 3.13+
2931
TypeIs,
3032
)
@@ -76,6 +78,7 @@
7678
"FileHandleDict",
7779
"FileHandleInput",
7880
"FileType",
81+
"LocalFileInput",
7982
"SystemPrompt",
8083
"SystemPromptContent",
8184
"ToolCallRequest",
@@ -529,17 +532,24 @@ def add_tool_result(self, result: ToolCallResultInput) -> ToolResultMessage:
529532
return message
530533

531534

532-
LocalFileInput = BinaryIO | bytes | str | os.PathLike[str]
535+
LocalFileInput = BinaryIO | Buffer | str | os.PathLike[str]
533536

534537

535538
# Private until file handle caching support is part of the published SDK API
536539

537540

538-
def _get_file_details(src: LocalFileInput) -> Tuple[str, bytes]:
541+
def _get_file_details(src: LocalFileInput) -> Tuple[str, Buffer]:
539542
"""Read file contents as binary data and generate a suitable default name."""
540-
if isinstance(src, bytes):
541-
# We process bytes as raw data, not a bytes filesystem path
542-
data = src
543+
data: Buffer
544+
if isinstance(src, Buffer):
545+
if isinstance(src, memoryview):
546+
# If already a memoryview, just use it directly
547+
data = src
548+
else:
549+
# Try to create a memoryview - this will work for any buffer protocol object
550+
# including bytes, bytearray, array.array, numpy arrays, etc.
551+
data = memoryview(src)
552+
# Received raw file data without any name information
543553
name = str(uuid.uuid4())
544554
elif hasattr(src, "read"):
545555
try:
@@ -550,10 +560,13 @@ def _get_file_details(src: LocalFileInput) -> Tuple[str, bytes]:
550560
raise LMStudioOSError(err_msg) from None
551561
name = getattr(src, "name", str(uuid.uuid4()))
552562
else:
563+
# At this point, src must be a path-like object
553564
try:
554565
src_path = Path(src)
555566
except Exception as exc:
556-
err_msg = f"Expected file-like object, filesystem path, or bytes ({exc!r})"
567+
err_msg = (
568+
f"Expected file-like object, filesystem path, or data buffer ({exc!r})"
569+
)
557570
raise LMStudioValueError(err_msg) from None
558571
try:
559572
data = src_path.read_bytes()
@@ -573,7 +586,7 @@ class _LocalFileData:
573586
"""Local file data to be added to a chat history."""
574587

575588
name: str
576-
raw_data: bytes
589+
raw_data: Buffer
577590

578591
def __init__(self, src: LocalFileInput, name: str | None = None) -> None:
579592
default_name, raw_data = _get_file_details(src)

tests/async/test_images_async.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from io import BytesIO
99

10-
from lmstudio import AsyncClient, Chat, FileHandle, LMStudioServerError
10+
from lmstudio import AsyncClient, Chat, FileHandle, LMStudioServerError, LocalFileInput
1111

1212
from ..support import (
1313
EXPECTED_VLM_ID,
@@ -17,59 +17,57 @@
1717
check_sdk_error,
1818
)
1919

20+
_IMAGE_DATA = IMAGE_FILEPATH.read_bytes()
21+
22+
_FILE_INPUT_CASES: list[tuple[str, LocalFileInput]] = [
23+
("filesystem path", IMAGE_FILEPATH),
24+
("bytes IO stream", BytesIO(_IMAGE_DATA)),
25+
("raw bytes", _IMAGE_DATA),
26+
("mutable buffer", bytearray(_IMAGE_DATA)),
27+
("memoryview", memoryview(_IMAGE_DATA)),
28+
]
29+
_FILE_INPUT_CASE_IDS = [case[0] for case in _FILE_INPUT_CASES]
30+
2031

2132
@pytest.mark.asyncio
2233
@pytest.mark.lmstudio
23-
async def test_upload_from_pathlike_async(caplog: LogCap) -> None:
34+
@pytest.mark.parametrize(
35+
"input_kind,file_input", _FILE_INPUT_CASES, ids=_FILE_INPUT_CASE_IDS
36+
)
37+
async def test_prepare_async(
38+
caplog: LogCap, input_kind: str, file_input: LocalFileInput
39+
) -> None:
2440
caplog.set_level(logging.DEBUG)
2541
async with AsyncClient() as client:
2642
session = client.files
27-
file = await session._prepare_file(IMAGE_FILEPATH)
43+
file = await session._prepare_file(file_input)
2844
assert file
2945
assert isinstance(file, FileHandle)
30-
logging.info(f"Uploaded file: {file}")
31-
image = await session.prepare_image(IMAGE_FILEPATH)
46+
logging.info(f"Uploaded file from {input_kind}: {file}")
47+
image = await session.prepare_image(file_input)
3248
assert image
3349
assert isinstance(image, FileHandle)
34-
logging.info(f"Uploaded image: {image}")
50+
logging.info(f"Uploaded image from {input_kind}: {image}")
3551
# Even with the same data uploaded, assigned identifiers should differ
3652
assert image != file
3753

3854

3955
@pytest.mark.asyncio
4056
@pytest.mark.lmstudio
41-
async def test_upload_from_file_obj_async(caplog: LogCap) -> None:
57+
async def test_prepare_from_file_obj_async(caplog: LogCap) -> None:
4258
caplog.set_level(logging.DEBUG)
4359
async with AsyncClient() as client:
4460
session = client.files
4561
with open(IMAGE_FILEPATH, "rb") as f:
4662
file = await session._prepare_file(f)
4763
assert file
4864
assert isinstance(file, FileHandle)
49-
logging.info(f"Uploaded file: {file}")
65+
logging.info(f"Uploaded file from file object: {file}")
5066
with open(IMAGE_FILEPATH, "rb") as f:
5167
image = await session.prepare_image(f)
5268
assert image
5369
assert isinstance(image, FileHandle)
54-
logging.info(f"Uploaded image: {image}")
55-
# Even with the same data uploaded, assigned identifiers should differ
56-
assert image != file
57-
58-
59-
@pytest.mark.asyncio
60-
@pytest.mark.lmstudio
61-
async def test_upload_from_bytesio_async(caplog: LogCap) -> None:
62-
caplog.set_level(logging.DEBUG)
63-
async with AsyncClient() as client:
64-
session = client.files
65-
file = await session._prepare_file(BytesIO(IMAGE_FILEPATH.read_bytes()))
66-
assert file
67-
assert isinstance(file, FileHandle)
68-
logging.info(f"Uploaded file: {file}")
69-
image = await session.prepare_image(BytesIO(IMAGE_FILEPATH.read_bytes()))
70-
assert image
71-
assert isinstance(image, FileHandle)
72-
logging.info(f"Uploaded image: {image}")
70+
logging.info(f"Uploaded image from file object: {image}")
7371
# Even with the same data uploaded, assigned identifiers should differ
7472
assert image != file
7573

tests/sync/test_images_sync.py

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from io import BytesIO
1616

17-
from lmstudio import Client, Chat, FileHandle, LMStudioServerError
17+
from lmstudio import Client, Chat, FileHandle, LMStudioServerError, LocalFileInput
1818

1919
from ..support import (
2020
EXPECTED_VLM_ID,
@@ -24,56 +24,55 @@
2424
check_sdk_error,
2525
)
2626

27+
_IMAGE_DATA = IMAGE_FILEPATH.read_bytes()
28+
29+
_FILE_INPUT_CASES: list[tuple[str, LocalFileInput]] = [
30+
("filesystem path", IMAGE_FILEPATH),
31+
("bytes IO stream", BytesIO(_IMAGE_DATA)),
32+
("raw bytes", _IMAGE_DATA),
33+
("mutable buffer", bytearray(_IMAGE_DATA)),
34+
("memoryview", memoryview(_IMAGE_DATA)),
35+
]
36+
_FILE_INPUT_CASE_IDS = [case[0] for case in _FILE_INPUT_CASES]
37+
2738

2839
@pytest.mark.lmstudio
29-
def test_upload_from_pathlike_sync(caplog: LogCap) -> None:
40+
@pytest.mark.parametrize(
41+
"input_kind,file_input", _FILE_INPUT_CASES, ids=_FILE_INPUT_CASE_IDS
42+
)
43+
def test_prepare_sync(
44+
caplog: LogCap, input_kind: str, file_input: LocalFileInput
45+
) -> None:
3046
caplog.set_level(logging.DEBUG)
3147
with Client() as client:
3248
session = client.files
33-
file = session._prepare_file(IMAGE_FILEPATH)
49+
file = session._prepare_file(file_input)
3450
assert file
3551
assert isinstance(file, FileHandle)
36-
logging.info(f"Uploaded file: {file}")
37-
image = session.prepare_image(IMAGE_FILEPATH)
52+
logging.info(f"Uploaded file from {input_kind}: {file}")
53+
image = session.prepare_image(file_input)
3854
assert image
3955
assert isinstance(image, FileHandle)
40-
logging.info(f"Uploaded image: {image}")
56+
logging.info(f"Uploaded image from {input_kind}: {image}")
4157
# Even with the same data uploaded, assigned identifiers should differ
4258
assert image != file
4359

4460

4561
@pytest.mark.lmstudio
46-
def test_upload_from_file_obj_sync(caplog: LogCap) -> None:
62+
def test_prepare_from_file_obj_sync(caplog: LogCap) -> None:
4763
caplog.set_level(logging.DEBUG)
4864
with Client() as client:
4965
session = client.files
5066
with open(IMAGE_FILEPATH, "rb") as f:
5167
file = session._prepare_file(f)
5268
assert file
5369
assert isinstance(file, FileHandle)
54-
logging.info(f"Uploaded file: {file}")
70+
logging.info(f"Uploaded file from file object: {file}")
5571
with open(IMAGE_FILEPATH, "rb") as f:
5672
image = session.prepare_image(f)
5773
assert image
5874
assert isinstance(image, FileHandle)
59-
logging.info(f"Uploaded image: {image}")
60-
# Even with the same data uploaded, assigned identifiers should differ
61-
assert image != file
62-
63-
64-
@pytest.mark.lmstudio
65-
def test_upload_from_bytesio_sync(caplog: LogCap) -> None:
66-
caplog.set_level(logging.DEBUG)
67-
with Client() as client:
68-
session = client.files
69-
file = session._prepare_file(BytesIO(IMAGE_FILEPATH.read_bytes()))
70-
assert file
71-
assert isinstance(file, FileHandle)
72-
logging.info(f"Uploaded file: {file}")
73-
image = session.prepare_image(BytesIO(IMAGE_FILEPATH.read_bytes()))
74-
assert image
75-
assert isinstance(image, FileHandle)
76-
logging.info(f"Uploaded image: {image}")
75+
logging.info(f"Uploaded image from file object: {image}")
7776
# Even with the same data uploaded, assigned identifiers should differ
7877
assert image != file
7978

0 commit comments

Comments
 (0)