Skip to content

Commit fe0c571

Browse files
committed
Typing fixes in file_handler: avoid cast()
1 parent cfe49fe commit fe0c571

3 files changed

Lines changed: 69 additions & 8 deletions

File tree

infrahub_sdk/file_handler.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
from dataclasses import dataclass
44
from io import BytesIO
55
from pathlib import Path
6-
from typing import TYPE_CHECKING, BinaryIO, cast, overload
6+
from typing import TYPE_CHECKING, BinaryIO, overload
77

88
import anyio
99
import httpx
10+
from anyio.to_thread import run_sync as run_sync_in_thread
1011

1112
from .exceptions import AuthenticationError, NodeNotFoundError, ServerNotReachableError
1213

@@ -21,6 +22,17 @@ class PreparedFile:
2122
should_close: bool
2223

2324

25+
def _open_binary(path: Path) -> BinaryIO:
26+
"""Open a file in binary read mode.
27+
28+
Wrapper exists to pin the return type to BinaryIO when called via
29+
``run_sync_in_thread``: the overload resolution on ``Path.open``'s mode arg
30+
is lost through the indirection and would otherwise widen to a union of all
31+
open() return types.
32+
"""
33+
return path.open("rb")
34+
35+
2436
class FileHandlerBase:
2537
"""Base class for file handling operations.
2638
@@ -57,11 +69,11 @@ async def prepare_upload(content: bytes | Path | BinaryIO | None, name: str | No
5769
if isinstance(content, Path):
5870
# Open file in thread pool to avoid blocking the event loop
5971
# Returns a sync file handle that httpx can stream from in chunks
60-
file_obj = await anyio.to_thread.run_sync(content.open, "rb")
61-
return PreparedFile(file_object=cast("BinaryIO", file_obj), filename=filename, should_close=True)
72+
file_obj = await run_sync_in_thread(_open_binary, content)
73+
return PreparedFile(file_object=file_obj, filename=filename, should_close=True)
6274

6375
# At this point, content must be a BinaryIO (file-like object)
64-
return PreparedFile(file_object=cast("BinaryIO", content), filename=filename, should_close=False)
76+
return PreparedFile(file_object=content, filename=filename, should_close=False)
6577

6678
@staticmethod
6779
def prepare_upload_sync(content: bytes | Path | BinaryIO | None, name: str | None = None) -> PreparedFile:
@@ -92,7 +104,7 @@ def prepare_upload_sync(content: bytes | Path | BinaryIO | None, name: str | Non
92104
return PreparedFile(file_object=content.open("rb"), filename=filename, should_close=True)
93105

94106
# At this point, content must be a BinaryIO (file-like object)
95-
return PreparedFile(file_object=cast("BinaryIO", content), filename=filename, should_close=False)
107+
return PreparedFile(file_object=content, filename=filename, should_close=False)
96108

97109
@staticmethod
98110
def handle_error_response(exc: httpx.HTTPStatusError) -> None:

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,10 @@ include = ["infrahub_sdk/checks.py"]
143143
invalid-await = "ignore" # 1 violation
144144

145145
[[tool.ty.overrides]]
146-
include = ["infrahub_sdk/file_handler.py", "infrahub_sdk/utils.py"]
146+
include = ["infrahub_sdk/utils.py"]
147147

148148
[tool.ty.overrides.rules]
149-
unresolved-attribute = "ignore" # 5 violations total (1 in file_handler.py, 4 in utils.py)
149+
unresolved-attribute = "ignore" # 4 violations in utils.py
150150

151151
[[tool.ty.overrides]]
152152
include = ["infrahub_sdk/transfer/**"]

tests/unit/sdk/test_file_handler.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import tempfile
44
from io import BytesIO
55
from pathlib import Path
6-
from typing import TYPE_CHECKING
6+
from typing import TYPE_CHECKING, BinaryIO
77

88
import anyio
99
import httpx
@@ -22,6 +22,15 @@
2222
NODE_ID = "test-node-123"
2323

2424

25+
def _open_binary_handle(path: str) -> BinaryIO:
26+
"""Sync helper to open a file in binary read mode.
27+
28+
Defined as a sync function so ruff's ASYNC* rules don't apply when called from async tests.
29+
The handle is the realistic non-BytesIO BinaryIO that production callers pass to ``prepare_upload``.
30+
"""
31+
return Path(path).open("rb")
32+
33+
2534
async def test_prepare_upload_with_bytes() -> None:
2635
"""Test preparing upload with bytes content (async)."""
2736
content = b"test file content"
@@ -85,6 +94,26 @@ async def test_prepare_upload_with_binary_io() -> None:
8594
assert prepared.should_close is False
8695

8796

97+
async def test_prepare_upload_with_open_file() -> None:
98+
"""Test preparing upload with a real file handle (not BytesIO) — async.
99+
100+
Locks in BinaryIO branch behaviour for callers passing the result of opening a real
101+
file, not just ``BytesIO``. Guards against narrowing the dispatch to a too-specific
102+
type.
103+
"""
104+
with tempfile.NamedTemporaryFile(suffix=".txt") as tmp:
105+
tmp.write(b"content from open file")
106+
tmp.flush()
107+
108+
with _open_binary_handle(tmp.name) as file_handle:
109+
prepared = await FileHandlerBase.prepare_upload(content=file_handle, name="from_open.bin")
110+
111+
assert prepared.file_object is file_handle
112+
assert prepared.filename == "from_open.bin"
113+
assert prepared.should_close is False
114+
assert prepared.file_object.read() == b"content from open file"
115+
116+
88117
async def test_prepare_upload_with_none() -> None:
89118
"""Test preparing upload with None content (async)."""
90119
prepared = await FileHandlerBase.prepare_upload(content=None)
@@ -157,6 +186,26 @@ def test_prepare_upload_sync_with_binary_io() -> None:
157186
assert prepared.should_close is False
158187

159188

189+
def test_prepare_upload_sync_with_open_file() -> None:
190+
"""Test preparing upload with a real file handle (not BytesIO) — sync.
191+
192+
Locks in BinaryIO branch behaviour for callers passing the result of opening a real
193+
file, not just ``BytesIO``. Guards against narrowing the dispatch to a too-specific
194+
type.
195+
"""
196+
with tempfile.NamedTemporaryFile(suffix=".txt") as tmp:
197+
tmp.write(b"content from open file")
198+
tmp.flush()
199+
200+
with _open_binary_handle(tmp.name) as file_handle:
201+
prepared = FileHandlerBase.prepare_upload_sync(content=file_handle, name="from_open.bin")
202+
203+
assert prepared.file_object is file_handle
204+
assert prepared.filename == "from_open.bin"
205+
assert prepared.should_close is False
206+
assert prepared.file_object.read() == b"content from open file"
207+
208+
160209
def test_prepare_upload_sync_with_none() -> None:
161210
"""Test preparing upload with None content (sync)."""
162211
prepared = FileHandlerBase.prepare_upload_sync(content=None)

0 commit comments

Comments
 (0)