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
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ uv run excelbench benchmark --tests fixtures/excel --output results
2. Export it from `src/excelbench/harness/adapters/__init__.py`.
3. Add to `get_all_adapters()` if it should run by default.
4. Verify read/write capability flags.
5. For unsupported feature surfaces, raise `UnsupportedAdapterOperationError` via `self.unsupported_operation(...)` rather than silent no-ops so the harness can classify capability gaps separately from regressions.

## Tests
```bash
Expand Down
21 changes: 19 additions & 2 deletions src/excelbench/harness/adapters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@

JSONDict = dict[str, Any]

class UnsupportedAdapterOperationError(NotImplementedError):
"""Structured exception for adapter operations a library cannot support."""

def __init__(self, *, adapter: str, operation: str, reason: str) -> None:
self.adapter = adapter
self.operation = operation
self.reason = reason
super().__init__(f"{adapter} does not support {operation}: {reason}")


def _infer_diagnostic_category(exc: Exception) -> DiagnosticCategory:
name = type(exc).__name__.lower()
Expand All @@ -28,7 +37,7 @@ def _infer_diagnostic_category(exc: Exception) -> DiagnosticCategory:
return DiagnosticCategory.FILE_IO
if isinstance(exc, (ValueError, TypeError, KeyError)):
return DiagnosticCategory.INVALID_INPUT
if isinstance(exc, NotImplementedError):
if isinstance(exc, (NotImplementedError, UnsupportedAdapterOperationError)):
return DiagnosticCategory.UNSUPPORTED_FEATURE
if "not supported" in message or "unsupported" in message:
return DiagnosticCategory.UNSUPPORTED_FEATURE
Expand Down Expand Up @@ -83,6 +92,12 @@ def supports_read_path(self, path: Path) -> bool:
suffix = path.suffix.lower()
return suffix in self.supported_read_extensions

def unsupported_operation(self, operation: str, reason: str) -> None:
"""Raise a structured unsupported-feature exception for adapter methods."""
raise UnsupportedAdapterOperationError(
adapter=self.name, operation=operation, reason=reason
)
Comment thread
wolfiesch marked this conversation as resolved.


def map_error_to_diagnostic(
self,
Expand Down Expand Up @@ -113,7 +128,9 @@ def map_error_to_diagnostic(
cell=cell,
),
adapter_message=f"{type(exc).__name__}: {exc}",
probable_cause=probable_cause,
probable_cause=exc.reason
if isinstance(exc, UnsupportedAdapterOperationError)
else probable_cause,
)

def build_mismatch_diagnostic(
Expand Down
38 changes: 26 additions & 12 deletions src/excelbench/harness/adapters/pyexcel_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def write_cell_format(
cell: str,
format: CellFormat,
) -> None:
pass # pyexcel does not support formatting
self.unsupported_operation("write_cell_format", "pyexcel exposes value-only cells")

def write_cell_border(
self,
Expand All @@ -232,41 +232,55 @@ def write_cell_border(
cell: str,
border: BorderInfo,
) -> None:
pass # pyexcel does not support borders
self.unsupported_operation("write_cell_border", "pyexcel exposes value-only cells")

def set_row_height(self, workbook: Any, sheet: str, row: int, height: float) -> None:
pass
self.unsupported_operation(
"set_row_height", "pyexcel does not expose row dimension writing"
)

def set_column_width(self, workbook: Any, sheet: str, column: str, width: float) -> None:
pass
self.unsupported_operation(
"set_column_width", "pyexcel does not expose column dimension writing"
)

# =========================================================================
# Tier 2 Write Operations
# =========================================================================

def merge_cells(self, workbook: Any, sheet: str, cell_range: str) -> None:
pass
self.unsupported_operation("merge_cells", "pyexcel does not expose this worksheet feature")
Comment thread
wolfiesch marked this conversation as resolved.

def add_conditional_format(self, workbook: Any, sheet: str, rule: JSONDict) -> None:
pass
self.unsupported_operation(
"add_conditional_format", "pyexcel does not expose this worksheet feature"
)

def add_data_validation(self, workbook: Any, sheet: str, validation: JSONDict) -> None:
pass
self.unsupported_operation(
"add_data_validation", "pyexcel does not expose this worksheet feature"
)

def add_hyperlink(self, workbook: Any, sheet: str, link: JSONDict) -> None:
pass
self.unsupported_operation(
"add_hyperlink", "pyexcel does not expose this worksheet feature"
)

def add_image(self, workbook: Any, sheet: str, image: JSONDict) -> None:
pass
self.unsupported_operation("add_image", "pyexcel does not expose this worksheet feature")

def add_pivot_table(self, workbook: Any, sheet: str, pivot: JSONDict) -> None:
pass
self.unsupported_operation(
"add_pivot_table", "pyexcel does not expose this worksheet feature"
)

def add_comment(self, workbook: Any, sheet: str, comment: JSONDict) -> None:
pass
self.unsupported_operation("add_comment", "pyexcel does not expose this worksheet feature")

def set_freeze_panes(self, workbook: Any, sheet: str, settings: JSONDict) -> None:
pass
self.unsupported_operation(
"set_freeze_panes", "pyexcel does not expose this worksheet feature"
)

def save_workbook(self, workbook: WorkbookData, path: Path) -> None:
book_dict: dict[str, list[list[Any]]] = {}
Expand Down
28 changes: 16 additions & 12 deletions src/excelbench/harness/adapters/pylightxl_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ def write_cell_format(
cell: str,
format: CellFormat,
) -> None:
pass # Not supported
self.unsupported_operation("write_cell_format", "pylightxl does not implement this feature")
Comment thread
wolfiesch marked this conversation as resolved.

def write_cell_border(
self,
Expand All @@ -255,7 +255,7 @@ def write_cell_border(
cell: str,
border: BorderInfo,
) -> None:
pass # Not supported
self.unsupported_operation("write_cell_border", "pylightxl does not implement this feature")

def set_row_height(
self,
Expand All @@ -264,7 +264,7 @@ def set_row_height(
row: int,
height: float,
) -> None:
pass # Not supported
self.unsupported_operation("set_row_height", "pylightxl does not implement this feature")

def set_column_width(
self,
Expand All @@ -273,7 +273,7 @@ def set_column_width(
column: str,
width: float,
) -> None:
pass # Not supported
self.unsupported_operation("set_column_width", "pylightxl does not implement this feature")

def save_workbook(self, workbook: Any, path: Path) -> None:
# pylightxl tries to read an existing file as a ZIP for in-place update.
Expand All @@ -287,25 +287,29 @@ def save_workbook(self, workbook: Any, path: Path) -> None:
# =========================================================================

def merge_cells(self, workbook: Any, sheet: str, cell_range: str) -> None:
pass # Not supported
self.unsupported_operation("merge_cells", "pylightxl does not implement this feature")

def add_conditional_format(self, workbook: Any, sheet: str, rule: JSONDict) -> None:
pass # Not supported
self.unsupported_operation(
"add_conditional_format", "pylightxl does not implement this feature"
)

def add_data_validation(self, workbook: Any, sheet: str, validation: JSONDict) -> None:
pass # Not supported
self.unsupported_operation(
"add_data_validation", "pylightxl does not implement this feature"
)

def add_hyperlink(self, workbook: Any, sheet: str, link: JSONDict) -> None:
pass # Not supported
self.unsupported_operation("add_hyperlink", "pylightxl does not implement this feature")

def add_image(self, workbook: Any, sheet: str, image: JSONDict) -> None:
pass # Not supported
self.unsupported_operation("add_image", "pylightxl does not implement this feature")

def add_pivot_table(self, workbook: Any, sheet: str, pivot: JSONDict) -> None:
pass # Not supported
self.unsupported_operation("add_pivot_table", "pylightxl does not implement this feature")

def add_comment(self, workbook: Any, sheet: str, comment: JSONDict) -> None:
pass # Not supported
self.unsupported_operation("add_comment", "pylightxl does not implement this feature")

def set_freeze_panes(self, workbook: Any, sheet: str, settings: JSONDict) -> None:
pass # Not supported
self.unsupported_operation("set_freeze_panes", "pylightxl does not implement this feature")
17 changes: 11 additions & 6 deletions src/excelbench/harness/adapters/xlwt_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,22 +433,27 @@ def merge_cells(self, workbook: xlwt.Workbook, sheet: str, cell_range: str) -> N
ws.write_merge(r1, r2, c1, c2, "")

def add_conditional_format(self, workbook: Any, sheet: str, rule: JSONDict) -> None:
pass # xlwt does not support conditional formatting
self.unsupported_operation(
"add_conditional_format", "xlwt cannot author conditional formatting"
)

def add_data_validation(self, workbook: Any, sheet: str, validation: JSONDict) -> None:
pass # xlwt does not support data validation
self.unsupported_operation("add_data_validation", "xlwt cannot author data validations")

def add_hyperlink(self, workbook: Any, sheet: str, link: JSONDict) -> None:
pass # xlwt does not support hyperlinks via write_url
self.unsupported_operation(
"add_hyperlink",
"xlwt hyperlink metadata is not supported in this adapter",
)

def add_image(self, workbook: Any, sheet: str, image: JSONDict) -> None:
pass # xlwt does not support images
self.unsupported_operation("add_image", "xlwt cannot embed images in this adapter")

def add_pivot_table(self, workbook: Any, sheet: str, pivot: JSONDict) -> None:
pass # xlwt does not support pivot tables
self.unsupported_operation("add_pivot_table", "xlwt cannot author pivot tables")

def add_comment(self, workbook: Any, sheet: str, comment: JSONDict) -> None:
pass # xlwt does not support comments
self.unsupported_operation("add_comment", "xlwt cannot author comments")

def set_freeze_panes(self, workbook: xlwt.Workbook, sheet: str, settings: JSONDict) -> None:
ws = self._get_sheet(workbook, sheet)
Expand Down
11 changes: 10 additions & 1 deletion src/excelbench/results/failure_explainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from dataclasses import dataclass
from typing import Any

from excelbench.models import Diagnostic, TestResult
from excelbench.models import Diagnostic, DiagnosticCategory, TestResult

JSONDict = dict[str, Any]

Expand Down Expand Up @@ -45,6 +45,15 @@ def explain_diagnostic(
actual: JSONDict | None = None,
) -> FailureExplanation | None:
"""Classify a diagnostic plus optional expected/actual payloads."""
if diagnostic.category == DiagnosticCategory.UNSUPPORTED_FEATURE:
return FailureExplanation(
code="unsupported_feature",
summary="adapter reported this operation as unsupported",
probable_cause=diagnostic.probable_cause
or "library/adapter does not implement the requested feature surface",
next_step="treat as unsupported capability, not a semantic regression",
)

if diagnostic.root_cause_code and diagnostic.suggested_next_step:
return FailureExplanation(
code=diagnostic.root_cause_code,
Expand Down
18 changes: 17 additions & 1 deletion tests/test_adapter_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@

import pytest

from excelbench.harness.adapters.base import ReadOnlyAdapter, WriteOnlyAdapter
from excelbench.harness.adapters.base import (
ReadOnlyAdapter,
UnsupportedAdapterOperationError,
WriteOnlyAdapter,
)
from excelbench.models import (
BorderInfo,
CellFormat,
Expand Down Expand Up @@ -344,3 +348,15 @@ def test_tier3_defaults_raise_not_implemented() -> None:
adapter.read_tables(None, "S")
with pytest.raises(NotImplementedError, match="table writes"):
adapter.add_table(None, "S", {})


def test_unsupported_operation_maps_to_unsupported_diagnostic() -> None:
adapter = ConcreteReadOnly()
with pytest.raises(UnsupportedAdapterOperationError) as exc_info:
adapter.unsupported_operation("write_cell_format", "missing backend feature")
diagnostic = adapter.map_error_to_diagnostic(
exc=exc_info.value,
feature="cell_format",
operation=OperationType.WRITE,
)
assert diagnostic.category == DiagnosticCategory.UNSUPPORTED_FEATURE
Loading
Loading