Skip to content
Open
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
3 changes: 2 additions & 1 deletion data_parser.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
Data Parser Sub-Agent
Reads and parses all TSV files dynamically from GrindPulse/raw/
Reads and parses all TSV files dynamically from the raw/ directory
"""

import csv
Expand Down Expand Up @@ -164,6 +164,7 @@ def parse_tsv_files(raw_folder):
if total_problems == 0:
raise ValidationError(
"No problems parsed from any TSV file",
file_path=str(raw_path),
suggestion="Check that TSV files contain data rows after the header",
)

Expand Down
19 changes: 13 additions & 6 deletions exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,27 @@ def __init__(
super().__init__(full_message)


class FileIOError(GrindPulseError):
class GrindPulseIOError(GrindPulseError):
"""Raised when file read/write operations fail."""

pass


class DataFileNotFoundError(FileIOError):
FileIOError = GrindPulseIOError


class DataFileNotFoundError(GrindPulseIOError):
"""Raised when a required data file is missing."""

pass
def __init__(self, message: str, file_path: str, suggestion: str | None = None) -> None:
super().__init__(message, file_path, suggestion)


class DataFileEmptyError(FileIOError):
class DataFileEmptyError(GrindPulseIOError):
"""Raised when a data file exists but is empty."""

pass
def __init__(self, message: str, file_path: str, suggestion: str | None = None) -> None:
super().__init__(message, file_path, suggestion)


class ParseError(GrindPulseError):
Expand All @@ -63,7 +68,9 @@ def __init__(
suggestion: str | None = None,
) -> None:
self.line_number = line_number
if line_number:
if line_number is not None:
if line_number < 1:
raise ValueError(f"line_number must be >= 1, got {line_number}")
message = f"{message} (line {line_number})"
super().__init__(message, file_path, suggestion)

Expand Down
98 changes: 91 additions & 7 deletions tests/python/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""Tests for the custom exception hierarchy."""

import pytest

from exceptions import (
DataFileEmptyError,
DataFileNotFoundError,
FileIOError,
GeneratorError,
GrindPulseError,
GrindPulseIOError,
JSONParseError,
ParseError,
TSVParseError,
Expand Down Expand Up @@ -66,8 +69,30 @@ def test_none_attributes(self):
assert err.suggestion is None


class TestGrindPulseIOError:
"""Tests for GrindPulseIOError (renamed from FileIOError)."""

def test_inherits_from_grindpulse_error(self):
err = GrindPulseIOError("IO failed")
assert isinstance(err, GrindPulseError)

def test_with_file_path(self):
err = GrindPulseIOError("Cannot read", file_path="/test.txt")
assert "/test.txt" in str(err)

def test_file_io_error_alias_works(self):
"""FileIOError backward-compat alias should still work."""
err = FileIOError("IO failed")
assert isinstance(err, GrindPulseIOError)
assert isinstance(err, GrindPulseError)

def test_file_io_error_alias_is_same_class(self):
"""FileIOError alias should be the same class as GrindPulseIOError."""
assert FileIOError is GrindPulseIOError


class TestFileIOError:
"""Tests for FileIOError."""
"""Tests for FileIOError (backward-compat alias)."""

def test_inherits_from_grindpulse_error(self):
"""Should inherit from GrindPulseError."""
Expand All @@ -84,11 +109,22 @@ class TestDataFileNotFoundError:
"""Tests for DataFileNotFoundError."""

def test_inherits_from_file_io_error(self):
"""Should inherit from FileIOError."""
err = DataFileNotFoundError("Not found")
"""Should inherit from GrindPulseIOError (via FileIOError alias)."""
err = DataFileNotFoundError("Not found", "path.tsv")
assert isinstance(err, FileIOError)
assert isinstance(err, GrindPulseError)

def test_requires_file_path(self):
"""Should raise TypeError when file_path is omitted."""
with pytest.raises(TypeError):
DataFileNotFoundError("Not found")

def test_with_file_path(self):
"""Should work correctly when file_path is provided."""
err = DataFileNotFoundError("Not found", "path.tsv")
assert "path.tsv" in str(err)
assert "Not found" in str(err)

def test_typical_usage(self):
"""Should work with typical missing file scenario."""
err = DataFileNotFoundError(
Expand All @@ -107,14 +143,25 @@ class TestDataFileEmptyError:

def test_inherits_from_file_io_error(self):
"""Should inherit from FileIOError."""
err = DataFileEmptyError("File is empty")
err = DataFileEmptyError("File is empty", "path.tsv")
assert isinstance(err, FileIOError)

def test_requires_file_path(self):
"""Should raise TypeError when file_path is omitted."""
with pytest.raises(TypeError):
DataFileEmptyError("File is empty")

def test_with_file_path(self):
"""Should work correctly when file_path is provided."""
err = DataFileEmptyError("File is empty", "path.tsv")
assert "path.tsv" in str(err)
assert "File is empty" in str(err)

def test_typical_usage(self):
"""Should work with typical empty file scenario."""
err = DataFileEmptyError(
"TSV file is empty",
file_path="/raw/blind75.tsv",
"/raw/blind75.tsv",
suggestion="Add problem data",
)
assert "TSV file is empty" in str(err)
Expand Down Expand Up @@ -193,6 +240,26 @@ def test_with_all_parameters(self):
assert "/raw/problems.tsv" in message
assert "Add missing columns" in message

def test_line_number_zero_raises_value_error(self):
"""line_number=0 should raise ValueError."""
with pytest.raises(ValueError, match="line_number must be >= 1"):
TSVParseError("msg", "file.tsv", line_number=0)

def test_line_number_negative_raises_value_error(self):
"""Negative line_number should raise ValueError."""
with pytest.raises(ValueError, match="line_number must be >= 1"):
TSVParseError("msg", "file.tsv", line_number=-1)

def test_line_number_one_is_valid(self):
"""line_number=1 should work and include '(line 1)' in message."""
err = TSVParseError("msg", "file.tsv", line_number=1)
assert "line 1" in str(err)

def test_line_number_none_works_without_line_info(self):
"""line_number=None should work and not include line info."""
err = TSVParseError("msg", "file.tsv", line_number=None)
assert "line" not in str(err)


class TestGeneratorError:
"""Tests for GeneratorError."""
Expand Down Expand Up @@ -241,6 +308,18 @@ def test_typical_usage(self):
assert "No problems parsed" in message
assert "Check TSV files" in message

def test_with_file_path_includes_path_in_message(self):
"""ValidationError with file_path should include path in error context."""
err = ValidationError(
"No problems parsed from any TSV file",
file_path="/path/to/raw",
suggestion="Check that TSV files contain data rows after the header",
)
message = str(err)
assert "No problems parsed" in message
assert "/path/to/raw" in message
assert err.file_path == "/path/to/raw"


class TestExceptionHierarchy:
"""Tests for exception class inheritance."""
Expand Down Expand Up @@ -277,12 +356,17 @@ def test_validation_error_inherits_from_base(self):
"""ValidationError should inherit from GrindPulseError."""
assert issubclass(ValidationError, GrindPulseError)

def test_grindpulse_io_error_in_hierarchy(self):
"""GrindPulseIOError should be in hierarchy and catchable as GrindPulseError."""
assert issubclass(GrindPulseIOError, GrindPulseError)

def test_can_catch_all_with_base(self):
"""Should be able to catch all errors with GrindPulseError."""
errors = [
FileIOError("test"),
DataFileNotFoundError("test"),
DataFileEmptyError("test"),
GrindPulseIOError("test"),
DataFileNotFoundError("test", "/test"),
DataFileEmptyError("test", "/test"),
ParseError("test"),
JSONParseError("test"),
TSVParseError("test", file_path="/test"),
Expand Down
Loading