Files
2026-07-13 13:26:28 +08:00

164 lines
5.2 KiB
Python

"""Contract tests for data/exceptions.py.
Every loader in data/ raises DataNotFoundError with a specific combination
of keyword arguments (download_script vs instructions vs download_url, plus
readme). The tests below pin the message shape so a reformat of _build_message
does not silently drop the reader-facing download instructions.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from data.exceptions import DataNotFoundError, DownloadError, MissingDependencyError
def test_data_not_found_with_download_script_shows_command() -> None:
err = DataNotFoundError(
dataset_name="ETF Universe",
path=Path("/data/etfs/market/etf_universe.parquet"),
download_script="data/etfs/market/download.py",
)
msg = str(err)
assert "DATA NOT FOUND: ETF Universe" in msg
assert "/data/etfs/market/etf_universe.parquet" in msg
assert "uv run python data/etfs/market/download.py" in msg
assert "data/README.md" in msg # Default readme fallback
def test_data_not_found_with_custom_readme_overrides_default() -> None:
err = DataNotFoundError(
dataset_name="SEC Filings",
path=Path("/data/equities/fundamentals/"),
download_script="data/equities/fundamentals/filings_download.py",
readme="data/equities/fundamentals/README.md",
)
msg = str(err)
assert "data/equities/fundamentals/README.md" in msg
# Default readme should not appear when custom is set
assert msg.count("README.md") == 1
def test_data_not_found_with_download_url_prefers_url_over_script() -> None:
err = DataNotFoundError(
dataset_name="AlgoSeek Options",
path=Path("/data/options/sp500_options.parquet"),
download_url="https://example.com/algoseek.zip",
download_script="data/ignored/download.py", # Should be ignored
requires_api_key="ALGOSEEK_KEY",
)
msg = str(err)
assert "https://example.com/algoseek.zip" in msg
# download_script is shadowed by download_url in the elif chain
assert "uv run python data/ignored/download.py" not in msg
assert "Extract to:" in msg
assert "ALGOSEEK_KEY" in msg
def test_data_not_found_with_instructions_overrides_other_branches() -> None:
err = DataNotFoundError(
dataset_name="Derived Daily Bars",
path=Path("/data/futures/market/continuous/daily/continuous_daily.parquet"),
instructions="Run: python 02/05_futures_session_aggregation.py",
download_script="should_not_appear.py",
)
msg = str(err)
assert "02/05_futures_session_aggregation.py" in msg
assert "should_not_appear.py" not in msg
def test_data_not_found_with_derivation_notebook_adds_pointer() -> None:
err = DataNotFoundError(
dataset_name="Derived Label",
path=Path("/data/labels/fwd_ret_5d.parquet"),
derivation_notebook="07_defining_the_learning_task/03_label_methods.py",
)
msg = str(err)
assert "How this dataset is built" in msg
assert "07_defining_the_learning_task/03_label_methods.py" in msg
def test_data_not_found_is_filenotfounderror() -> None:
"""Callers catch FileNotFoundError generically; guard the inheritance."""
err = DataNotFoundError(
dataset_name="x",
path=Path("/tmp/x"),
download_script="x.py",
)
assert isinstance(err, FileNotFoundError)
with pytest.raises(FileNotFoundError):
raise err
def test_data_not_found_accepts_str_path() -> None:
"""Some callers pass str paths; the class must normalize to Path."""
err = DataNotFoundError(
dataset_name="x",
path="/tmp/string_path.parquet",
download_script="x.py",
)
assert isinstance(err.path, Path)
assert "/tmp/string_path.parquet" in str(err)
def test_download_error_includes_reason_and_suggestion() -> None:
err = DownloadError(
dataset_name="Crypto Perps",
reason="API returned 429",
suggestion="Retry after backoff or check rate limits",
)
msg = str(err)
assert "DOWNLOAD FAILED: Crypto Perps" in msg
assert "API returned 429" in msg
assert "Retry after backoff" in msg
def test_download_error_without_suggestion_still_valid() -> None:
err = DownloadError(dataset_name="x", reason="unknown")
msg = str(err)
assert "DOWNLOAD FAILED: x" in msg
assert "Suggestion:" not in msg
def test_download_error_is_runtime_error() -> None:
err = DownloadError(dataset_name="x", reason="y")
assert isinstance(err, RuntimeError)
def test_missing_dependency_error_defaults_install_command() -> None:
err = MissingDependencyError(package="edgartools")
msg = str(err)
assert "edgartools" in msg
assert "pip install edgartools" in msg
def test_missing_dependency_error_custom_install_and_purpose() -> None:
err = MissingDependencyError(
package="torch",
install_command="uv sync --extra gpu",
purpose="LSTM training in Ch13",
)
msg = str(err)
assert "torch" in msg
assert "uv sync --extra gpu" in msg
assert "LSTM training in Ch13" in msg
def test_missing_dependency_is_import_error() -> None:
"""Callers catch ImportError to gate optional backends."""
err = MissingDependencyError(package="x")
assert isinstance(err, ImportError)