Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions src/cachekit/serializers/arrow_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
- Requires: pip install 'cachekit[data]' (includes pyarrow, pandas)

Type Checking Note:
Optional imports (pyarrow, pandas) are guarded at runtime by HAS_PYARROW, HAS_PANDAS flags.
Type checker cannot statically verify these; suppressed via pyright config comments above.
pandas is guarded at runtime by HAS_PANDAS flag. pyarrow is required at import time
(module fails to load without it, enabling proper import guards in auto_serializer).
Type checker cannot statically verify optional imports; suppressed via pyright config comments above.
"""

from __future__ import annotations
Expand All @@ -38,15 +39,12 @@
HAS_PANDAS = False
pd = None # type: ignore[assignment]

# Optional dependency: pyarrow
# Required dependency: pyarrow (fail-fast at module level for import guard in auto_serializer)
try:
import pyarrow as pa
import pyarrow.ipc # noqa: F401 (used via pa.ipc.new_file and pa.ipc.open_file)

HAS_PYARROW = True
except ImportError:
HAS_PYARROW = False
pa = None # type: ignore[assignment]
except ImportError as e:
raise ImportError("pyarrow is not installed. ArrowSerializer requires the [data] extra: pip install 'cachekit[data]'") from e

# Standard dependency: xxhash (always available)
import xxhash
Expand Down Expand Up @@ -124,13 +122,8 @@ def __init__(self, return_format: str = "pandas", enable_integrity_checking: boo
When False: No checksum (faster, use for @cache.minimal speed-first scenarios)

Raises:
ImportError: If required optional dependencies are not installed
ValueError: If return_format is not one of the valid options
"""
if not HAS_PYARROW:
raise ImportError(
"pyarrow is not installed. ArrowSerializer requires the [data] extra: pip install 'cachekit[data]'"
)
if return_format not in ("pandas", "polars", "arrow"):
raise ValueError(f"Invalid return_format: '{return_format}'. Valid options: 'pandas', 'polars', 'arrow'")
self.return_format = return_format
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/test_arrow_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,51 @@ def test_polars_serialization_requires_arrow_c_stream(self):
# are detected via __arrow_c_stream__ interface (zero-copy).
# We can't test this without polars installed, but the code path is covered.
pass


class TestImportGuard:
"""Test module-level import guard for pyarrow dependency."""

def test_import_fails_without_pyarrow(self):
"""ArrowSerializer module raises ImportError when pyarrow is not installed.

This tests the fail-fast import guard that enables auto_serializer.py
to correctly detect when ArrowSerializer is unavailable.
"""
import builtins
import importlib
import sys

# Save original modules and import function
original_import = builtins.__import__
original_modules = {}

# Collect all modules we need to temporarily remove
modules_to_remove = [
key
for key in list(sys.modules.keys())
if key == "pyarrow" or key.startswith("pyarrow.") or key == "cachekit.serializers.arrow_serializer"
]

for mod in modules_to_remove:
original_modules[mod] = sys.modules.pop(mod)

def blocking_import(name, globals=None, locals=None, fromlist=(), level=0):
if name == "pyarrow" or name.startswith("pyarrow."):
raise ImportError(f"No module named '{name}'")
return original_import(name, globals, locals, fromlist, level)

builtins.__import__ = blocking_import

try:
# Now importing arrow_serializer should fail with our custom message
with pytest.raises(ImportError) as exc_info:
importlib.import_module("cachekit.serializers.arrow_serializer")

assert "pyarrow is not installed" in str(exc_info.value)
assert "pip install 'cachekit[data]'" in str(exc_info.value)
finally:
# Restore everything
builtins.__import__ = original_import
for mod, module in original_modules.items():
sys.modules[mod] = module
Loading