From 571700fbcb7a227cf1e9a6f3a9ce0fc947ae201b Mon Sep 17 00:00:00 2001 From: Wolfie Date: Fri, 8 May 2026 01:30:48 -0700 Subject: [PATCH 1/4] fix(adapters): enforce structured unsupported-operation behavior --- CONTRIBUTING.md | 1 + src/excelbench/harness/adapters/base.py | 17 ++++++++- .../harness/adapters/pyexcel_adapter.py | 38 +++++++++++++------ .../harness/adapters/pylightxl_adapter.py | 28 ++++++++------ .../harness/adapters/xlwt_adapter.py | 17 ++++++--- src/excelbench/results/failure_explainer.py | 11 +++++- tests/test_adapter_base.py | 18 ++++++++- tests/test_adapters.py | 17 +++++++++ tests/test_lightweight_adapters.py | 31 ++++++++------- 9 files changed, 132 insertions(+), 46 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cc95e7..05e1dee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/src/excelbench/harness/adapters/base.py b/src/excelbench/harness/adapters/base.py index c461af2..14707ef 100644 --- a/src/excelbench/harness/adapters/base.py +++ b/src/excelbench/harness/adapters/base.py @@ -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() @@ -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 @@ -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 + ) + def map_error_to_diagnostic( self, diff --git a/src/excelbench/harness/adapters/pyexcel_adapter.py b/src/excelbench/harness/adapters/pyexcel_adapter.py index e38f53b..524dd2f 100644 --- a/src/excelbench/harness/adapters/pyexcel_adapter.py +++ b/src/excelbench/harness/adapters/pyexcel_adapter.py @@ -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, @@ -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") 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]]] = {} diff --git a/src/excelbench/harness/adapters/pylightxl_adapter.py b/src/excelbench/harness/adapters/pylightxl_adapter.py index 11340d2..d994cea 100644 --- a/src/excelbench/harness/adapters/pylightxl_adapter.py +++ b/src/excelbench/harness/adapters/pylightxl_adapter.py @@ -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") def write_cell_border( self, @@ -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, @@ -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, @@ -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. @@ -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") diff --git a/src/excelbench/harness/adapters/xlwt_adapter.py b/src/excelbench/harness/adapters/xlwt_adapter.py index 1cf19f8..c6200de 100644 --- a/src/excelbench/harness/adapters/xlwt_adapter.py +++ b/src/excelbench/harness/adapters/xlwt_adapter.py @@ -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) diff --git a/src/excelbench/results/failure_explainer.py b/src/excelbench/results/failure_explainer.py index 2c78db9..234306a 100644 --- a/src/excelbench/results/failure_explainer.py +++ b/src/excelbench/results/failure_explainer.py @@ -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] @@ -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, diff --git a/tests/test_adapter_base.py b/tests/test_adapter_base.py index 3602d83..91a36e5 100644 --- a/tests/test_adapter_base.py +++ b/tests/test_adapter_base.py @@ -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, @@ -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 diff --git a/tests/test_adapters.py b/tests/test_adapters.py index d291eab..5b466d4 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -7,6 +7,7 @@ import pytest +from excelbench.harness.adapters.base import UnsupportedAdapterOperationError from excelbench.harness.adapters.calamine_adapter import CalamineAdapter from excelbench.harness.adapters.openpyxl_adapter import OpenpyxlAdapter from excelbench.harness.adapters.pylightxl_adapter import PylightxlAdapter @@ -640,3 +641,19 @@ def test_tier2_returns_empty(self, xlrd_adapter: XlrdAdapter, sample_xls: Path) assert xlrd_adapter.read_images(wb, "Data") == [] assert xlrd_adapter.read_pivot_tables(wb, "Data") == [] xlrd_adapter.close_workbook(wb) + + +class TestXlwtUnsupportedOperations: + def test_unsupported_methods_raise(self, xlwt_adapter) -> None: + wb = xlwt_adapter.create_workbook() + xlwt_adapter.add_sheet(wb, "S1") + with pytest.raises(UnsupportedAdapterOperationError): + xlwt_adapter.add_conditional_format(wb, "S1", {}) + + +class TestCalamineCloseWorkbook: + def test_close_workbook_no_exception( + self, calamine_adapter: CalamineAdapter, sample_xlsx: Path + ) -> None: + wb = calamine_adapter.open_workbook(sample_xlsx) + calamine_adapter.close_workbook(wb) diff --git a/tests/test_lightweight_adapters.py b/tests/test_lightweight_adapters.py index 60df7c1..2fb9e1b 100644 --- a/tests/test_lightweight_adapters.py +++ b/tests/test_lightweight_adapters.py @@ -11,6 +11,7 @@ import pytest +from excelbench.harness.adapters.base import UnsupportedAdapterOperationError from excelbench.harness.adapters.openpyxl_adapter import OpenpyxlAdapter from excelbench.harness.adapters.pyexcel_adapter import PyexcelAdapter from excelbench.harness.adapters.pylightxl_adapter import PylightxlAdapter @@ -242,21 +243,15 @@ def test_write_auto_creates_sheet(self, pyxl: PyexcelAdapter, tmp_path: Path) -> pyxl.write_cell_value(wb, "Auto", "A1", CellValue(type=CellType.STRING, value="x")) assert "Auto" in wb["sheets"] - def test_write_noop_methods(self, pyxl: PyexcelAdapter) -> None: + def test_write_unsupported_methods_raise(self, pyxl: PyexcelAdapter) -> None: wb = pyxl.create_workbook() pyxl.add_sheet(wb, "S1") - # All no-ops — should not raise - pyxl.write_cell_format(wb, "S1", "A1", CellFormat()) - pyxl.set_row_height(wb, "S1", 1, 30.0) - pyxl.set_column_width(wb, "S1", "A", 20.0) - pyxl.merge_cells(wb, "S1", "A1:B2") - pyxl.add_conditional_format(wb, "S1", {}) - pyxl.add_data_validation(wb, "S1", {}) - pyxl.add_hyperlink(wb, "S1", {}) - pyxl.add_image(wb, "S1", {}) - pyxl.add_pivot_table(wb, "S1", {}) - pyxl.add_comment(wb, "S1", {}) - pyxl.set_freeze_panes(wb, "S1", {}) + with pytest.raises(UnsupportedAdapterOperationError): + pyxl.write_cell_format(wb, "S1", "A1", CellFormat()) + with pytest.raises(UnsupportedAdapterOperationError): + pyxl.set_row_height(wb, "S1", 1, 30.0) + with pytest.raises(UnsupportedAdapterOperationError): + pyxl.set_column_width(wb, "S1", "A", 20.0) def test_save_empty_sheet( self, pyxl: PyexcelAdapter, opxl: OpenpyxlAdapter, tmp_path: Path @@ -960,3 +955,13 @@ def test_tier2_stubs(self, opxl: OpenpyxlAdapter, tmp_path: Path) -> None: assert adapter.read_row_height(wb, "S1", 1) is None assert adapter.read_column_width(wb, "S1", "A") is None adapter.close_workbook(wb) + + +class TestPylightxlUnsupportedWrite: + def test_unsupported_methods_raise(self, plxl: PylightxlAdapter) -> None: + wb = plxl.create_workbook() + plxl.add_sheet(wb, "S1") + with pytest.raises(UnsupportedAdapterOperationError): + plxl.write_cell_format(wb, "S1", "A1", CellFormat()) + with pytest.raises(UnsupportedAdapterOperationError): + plxl.merge_cells(wb, "S1", "A1:B2") From 487cb25dac153e37d7887d94397c929444ea802f Mon Sep 17 00:00:00 2001 From: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com> Date: Fri, 8 May 2026 01:37:20 -0700 Subject: [PATCH 2/4] test: type xlwt unsupported adapter fixture --- tests/test_adapters.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_adapters.py b/tests/test_adapters.py index 5b466d4..5fc2ea7 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -12,6 +12,7 @@ from excelbench.harness.adapters.openpyxl_adapter import OpenpyxlAdapter from excelbench.harness.adapters.pylightxl_adapter import PylightxlAdapter from excelbench.harness.adapters.xlrd_adapter import XlrdAdapter +from excelbench.harness.adapters.xlwt_adapter import XlwtAdapter from excelbench.models import BorderInfo, CellFormat, CellType, CellValue # ========================================================================= @@ -644,7 +645,7 @@ def test_tier2_returns_empty(self, xlrd_adapter: XlrdAdapter, sample_xls: Path) class TestXlwtUnsupportedOperations: - def test_unsupported_methods_raise(self, xlwt_adapter) -> None: + def test_unsupported_methods_raise(self, xlwt_adapter: XlwtAdapter) -> None: wb = xlwt_adapter.create_workbook() xlwt_adapter.add_sheet(wb, "S1") with pytest.raises(UnsupportedAdapterOperationError): From 95ec64fca6bdae1f9e6575505320b61b7fd8f291 Mon Sep 17 00:00:00 2001 From: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com> Date: Fri, 8 May 2026 01:44:19 -0700 Subject: [PATCH 3/4] test(adapters): migrate unsupported-operation callers --- src/excelbench/harness/adapters/base.py | 4 +- tests/test_adapters.py | 57 +++++++++++++------ tests/test_coverage_gaps.py | 41 ++++++++------ tests/test_lightweight_adapters.py | 75 +++++++++++++++++-------- 4 files changed, 119 insertions(+), 58 deletions(-) diff --git a/src/excelbench/harness/adapters/base.py b/src/excelbench/harness/adapters/base.py index 14707ef..1e2d0fa 100644 --- a/src/excelbench/harness/adapters/base.py +++ b/src/excelbench/harness/adapters/base.py @@ -128,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( diff --git a/tests/test_adapters.py b/tests/test_adapters.py index 5fc2ea7..01c1779 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -1,7 +1,7 @@ """Tests for python-calamine, pylightxl, and xlrd adapters.""" import tempfile -from collections.abc import Iterator +from collections.abc import Callable, Iterator from datetime import date, datetime from pathlib import Path @@ -343,30 +343,37 @@ def test_save_overwrites_existing(self, pylightxl_adapter: PylightxlAdapter) -> path.unlink(missing_ok=True) -class TestPylightxlFormatNoop: - """Verify formatting methods don't crash (they're no-ops).""" +class TestPylightxlUnsupportedWrite: + """Verify unsupported write methods raise structured capability gaps.""" - def test_write_format_noop(self, pylightxl_adapter: PylightxlAdapter) -> None: + def test_write_format_unsupported(self, pylightxl_adapter: PylightxlAdapter) -> None: wb = pylightxl_adapter.create_workbook() pylightxl_adapter.add_sheet(wb, "S1") - pylightxl_adapter.write_cell_format(wb, "S1", "A1", CellFormat(bold=True)) + with pytest.raises(UnsupportedAdapterOperationError): + pylightxl_adapter.write_cell_format(wb, "S1", "A1", CellFormat(bold=True)) - def test_write_border_noop(self, pylightxl_adapter: PylightxlAdapter) -> None: + def test_write_border_unsupported(self, pylightxl_adapter: PylightxlAdapter) -> None: wb = pylightxl_adapter.create_workbook() pylightxl_adapter.add_sheet(wb, "S1") - pylightxl_adapter.write_cell_border(wb, "S1", "A1", BorderInfo()) + with pytest.raises(UnsupportedAdapterOperationError): + pylightxl_adapter.write_cell_border(wb, "S1", "A1", BorderInfo()) - def test_tier2_write_noops(self, pylightxl_adapter: PylightxlAdapter) -> None: + def test_tier2_write_unsupported(self, pylightxl_adapter: PylightxlAdapter) -> None: wb = pylightxl_adapter.create_workbook() pylightxl_adapter.add_sheet(wb, "S1") - pylightxl_adapter.merge_cells(wb, "S1", "A1:B1") - pylightxl_adapter.add_conditional_format(wb, "S1", {}) - pylightxl_adapter.add_data_validation(wb, "S1", {}) - pylightxl_adapter.add_hyperlink(wb, "S1", {}) - pylightxl_adapter.add_image(wb, "S1", {}) - pylightxl_adapter.add_pivot_table(wb, "S1", {}) - pylightxl_adapter.add_comment(wb, "S1", {}) - pylightxl_adapter.set_freeze_panes(wb, "S1", {}) + calls: list[Callable[[], None]] = [ + lambda: pylightxl_adapter.merge_cells(wb, "S1", "A1:B1"), + lambda: pylightxl_adapter.add_conditional_format(wb, "S1", {}), + lambda: pylightxl_adapter.add_data_validation(wb, "S1", {}), + lambda: pylightxl_adapter.add_hyperlink(wb, "S1", {}), + lambda: pylightxl_adapter.add_image(wb, "S1", {}), + lambda: pylightxl_adapter.add_pivot_table(wb, "S1", {}), + lambda: pylightxl_adapter.add_comment(wb, "S1", {}), + lambda: pylightxl_adapter.set_freeze_panes(wb, "S1", {}), + ] + for call in calls: + with pytest.raises(UnsupportedAdapterOperationError): + call() # ========================================================================= @@ -516,6 +523,11 @@ def xlrd_adapter() -> XlrdAdapter: return XlrdAdapter() +@pytest.fixture +def xlwt_adapter() -> XlwtAdapter: + return XlwtAdapter() + + class TestXlrdInfo: def test_name(self, xlrd_adapter: XlrdAdapter) -> None: assert xlrd_adapter.name == "xlrd" @@ -648,8 +660,17 @@ class TestXlwtUnsupportedOperations: def test_unsupported_methods_raise(self, xlwt_adapter: XlwtAdapter) -> None: wb = xlwt_adapter.create_workbook() xlwt_adapter.add_sheet(wb, "S1") - with pytest.raises(UnsupportedAdapterOperationError): - xlwt_adapter.add_conditional_format(wb, "S1", {}) + calls: list[Callable[[], None]] = [ + lambda: xlwt_adapter.add_conditional_format(wb, "S1", {}), + lambda: xlwt_adapter.add_data_validation(wb, "S1", {}), + lambda: xlwt_adapter.add_hyperlink(wb, "S1", {}), + lambda: xlwt_adapter.add_image(wb, "S1", {}), + lambda: xlwt_adapter.add_pivot_table(wb, "S1", {}), + lambda: xlwt_adapter.add_comment(wb, "S1", {}), + ] + for call in calls: + with pytest.raises(UnsupportedAdapterOperationError): + call() class TestCalamineCloseWorkbook: diff --git a/tests/test_coverage_gaps.py b/tests/test_coverage_gaps.py index 16afc27..d606ac3 100644 --- a/tests/test_coverage_gaps.py +++ b/tests/test_coverage_gaps.py @@ -12,6 +12,7 @@ from __future__ import annotations +from collections.abc import Callable from datetime import date from pathlib import Path from types import ModuleType @@ -21,6 +22,7 @@ import openpyxl as _openpyxl import pytest +from excelbench.harness.adapters.base import UnsupportedAdapterOperationError from excelbench.harness.adapters.calamine_adapter import ( CalamineAdapter, ) @@ -306,12 +308,12 @@ def test_read_cell_border(self, pyexcel_adapter: PyexcelAdapter) -> None: border = pyexcel_adapter.read_cell_border(MagicMock(), "S1", "A1") assert isinstance(border, BorderInfo) - def test_write_cell_border(self, pyexcel_adapter: PyexcelAdapter) -> None: - """Line 235: write_cell_border is a no-op.""" + def test_write_cell_border_unsupported(self, pyexcel_adapter: PyexcelAdapter) -> None: + """write_cell_border raises a structured unsupported-operation error.""" wb = pyexcel_adapter.create_workbook() pyexcel_adapter.add_sheet(wb, "S1") - # Should not raise - pyexcel_adapter.write_cell_border(wb, "S1", "A1", BorderInfo()) + with pytest.raises(UnsupportedAdapterOperationError): + pyexcel_adapter.write_cell_border(wb, "S1", "A1", BorderInfo()) def test_read_cell_format(self, pyexcel_adapter: PyexcelAdapter) -> None: """read_cell_format returns empty CellFormat.""" @@ -1047,21 +1049,26 @@ def test_add_sheet_idempotent(self, pyexcel_adapter: PyexcelAdapter) -> None: pyexcel_adapter.add_sheet(wb, "S1") # Should not duplicate assert wb["_order"].count("S1") == 1 - def test_write_no_op_methods(self, pyexcel_adapter: PyexcelAdapter) -> None: - """Tier 2 write methods are no-ops.""" + def test_write_unsupported_methods_raise(self, pyexcel_adapter: PyexcelAdapter) -> None: + """Unsupported write methods raise structured capability-gap errors.""" wb = pyexcel_adapter.create_workbook() pyexcel_adapter.add_sheet(wb, "S1") - pyexcel_adapter.merge_cells(wb, "S1", "A1:B2") - pyexcel_adapter.add_conditional_format(wb, "S1", {}) - pyexcel_adapter.add_data_validation(wb, "S1", {}) - pyexcel_adapter.add_hyperlink(wb, "S1", {}) - pyexcel_adapter.add_image(wb, "S1", {}) - pyexcel_adapter.add_pivot_table(wb, "S1", {}) - pyexcel_adapter.add_comment(wb, "S1", {}) - pyexcel_adapter.set_freeze_panes(wb, "S1", {}) - pyexcel_adapter.set_row_height(wb, "S1", 1, 20.0) - pyexcel_adapter.set_column_width(wb, "S1", "A", 15.0) - pyexcel_adapter.write_cell_format(wb, "S1", "A1", CellFormat()) + calls: list[Callable[[], None]] = [ + lambda: pyexcel_adapter.merge_cells(wb, "S1", "A1:B2"), + lambda: pyexcel_adapter.add_conditional_format(wb, "S1", {}), + lambda: pyexcel_adapter.add_data_validation(wb, "S1", {}), + lambda: pyexcel_adapter.add_hyperlink(wb, "S1", {}), + lambda: pyexcel_adapter.add_image(wb, "S1", {}), + lambda: pyexcel_adapter.add_pivot_table(wb, "S1", {}), + lambda: pyexcel_adapter.add_comment(wb, "S1", {}), + lambda: pyexcel_adapter.set_freeze_panes(wb, "S1", {}), + lambda: pyexcel_adapter.set_row_height(wb, "S1", 1, 20.0), + lambda: pyexcel_adapter.set_column_width(wb, "S1", "A", 15.0), + lambda: pyexcel_adapter.write_cell_format(wb, "S1", "A1", CellFormat()), + ] + for call in calls: + with pytest.raises(UnsupportedAdapterOperationError): + call() # ═════════════════════════════════════════════════ diff --git a/tests/test_lightweight_adapters.py b/tests/test_lightweight_adapters.py index 2fb9e1b..59d5bff 100644 --- a/tests/test_lightweight_adapters.py +++ b/tests/test_lightweight_adapters.py @@ -6,6 +6,7 @@ from __future__ import annotations +from collections.abc import Callable from datetime import date, datetime from pathlib import Path @@ -16,6 +17,7 @@ from excelbench.harness.adapters.pyexcel_adapter import PyexcelAdapter from excelbench.harness.adapters.pylightxl_adapter import PylightxlAdapter from excelbench.models import ( + BorderInfo, CellFormat, CellType, CellValue, @@ -246,12 +248,23 @@ def test_write_auto_creates_sheet(self, pyxl: PyexcelAdapter, tmp_path: Path) -> def test_write_unsupported_methods_raise(self, pyxl: PyexcelAdapter) -> None: wb = pyxl.create_workbook() pyxl.add_sheet(wb, "S1") - with pytest.raises(UnsupportedAdapterOperationError): - pyxl.write_cell_format(wb, "S1", "A1", CellFormat()) - with pytest.raises(UnsupportedAdapterOperationError): - pyxl.set_row_height(wb, "S1", 1, 30.0) - with pytest.raises(UnsupportedAdapterOperationError): - pyxl.set_column_width(wb, "S1", "A", 20.0) + calls: list[Callable[[], None]] = [ + lambda: pyxl.write_cell_format(wb, "S1", "A1", CellFormat()), + lambda: pyxl.write_cell_border(wb, "S1", "A1", BorderInfo()), + lambda: pyxl.set_row_height(wb, "S1", 1, 30.0), + lambda: pyxl.set_column_width(wb, "S1", "A", 20.0), + lambda: pyxl.merge_cells(wb, "S1", "A1:B2"), + lambda: pyxl.add_conditional_format(wb, "S1", {}), + lambda: pyxl.add_data_validation(wb, "S1", {}), + lambda: pyxl.add_hyperlink(wb, "S1", {}), + lambda: pyxl.add_image(wb, "S1", {}), + lambda: pyxl.add_pivot_table(wb, "S1", {}), + lambda: pyxl.add_comment(wb, "S1", {}), + lambda: pyxl.set_freeze_panes(wb, "S1", {}), + ] + for call in calls: + with pytest.raises(UnsupportedAdapterOperationError): + call() def test_save_empty_sheet( self, pyxl: PyexcelAdapter, opxl: OpenpyxlAdapter, tmp_path: Path @@ -456,20 +469,25 @@ def test_write_cell_types( assert cv1.value == "text" opxl.close_workbook(rb) - def test_write_noop_methods(self, plxl: PylightxlAdapter) -> None: + def test_write_unsupported_methods_raise(self, plxl: PylightxlAdapter) -> None: wb = plxl.create_workbook() plxl.add_sheet(wb, "S1") - plxl.write_cell_format(wb, "S1", "A1", CellFormat()) - plxl.set_row_height(wb, "S1", 1, 20.0) - plxl.set_column_width(wb, "S1", "A", 15.0) - plxl.merge_cells(wb, "S1", "A1:B2") - plxl.add_conditional_format(wb, "S1", {}) - plxl.add_data_validation(wb, "S1", {}) - plxl.add_hyperlink(wb, "S1", {}) - plxl.add_image(wb, "S1", {}) - plxl.add_pivot_table(wb, "S1", {}) - plxl.add_comment(wb, "S1", {}) - plxl.set_freeze_panes(wb, "S1", {}) + calls: list[Callable[[], None]] = [ + lambda: plxl.write_cell_format(wb, "S1", "A1", CellFormat()), + lambda: plxl.set_row_height(wb, "S1", 1, 20.0), + lambda: plxl.set_column_width(wb, "S1", "A", 15.0), + lambda: plxl.merge_cells(wb, "S1", "A1:B2"), + lambda: plxl.add_conditional_format(wb, "S1", {}), + lambda: plxl.add_data_validation(wb, "S1", {}), + lambda: plxl.add_hyperlink(wb, "S1", {}), + lambda: plxl.add_image(wb, "S1", {}), + lambda: plxl.add_pivot_table(wb, "S1", {}), + lambda: plxl.add_comment(wb, "S1", {}), + lambda: plxl.set_freeze_panes(wb, "S1", {}), + ] + for call in calls: + with pytest.raises(UnsupportedAdapterOperationError): + call() def test_save_overwrites_existing(self, plxl: PylightxlAdapter, tmp_path: Path) -> None: """pylightxl removes existing file before writing (path.unlink).""" @@ -961,7 +979,20 @@ class TestPylightxlUnsupportedWrite: def test_unsupported_methods_raise(self, plxl: PylightxlAdapter) -> None: wb = plxl.create_workbook() plxl.add_sheet(wb, "S1") - with pytest.raises(UnsupportedAdapterOperationError): - plxl.write_cell_format(wb, "S1", "A1", CellFormat()) - with pytest.raises(UnsupportedAdapterOperationError): - plxl.merge_cells(wb, "S1", "A1:B2") + calls: list[Callable[[], None]] = [ + lambda: plxl.write_cell_format(wb, "S1", "A1", CellFormat()), + lambda: plxl.write_cell_border(wb, "S1", "A1", BorderInfo()), + lambda: plxl.set_row_height(wb, "S1", 1, 30.0), + lambda: plxl.set_column_width(wb, "S1", "A", 20.0), + lambda: plxl.merge_cells(wb, "S1", "A1:B2"), + lambda: plxl.add_conditional_format(wb, "S1", {}), + lambda: plxl.add_data_validation(wb, "S1", {}), + lambda: plxl.add_hyperlink(wb, "S1", {}), + lambda: plxl.add_image(wb, "S1", {}), + lambda: plxl.add_pivot_table(wb, "S1", {}), + lambda: plxl.add_comment(wb, "S1", {}), + lambda: plxl.set_freeze_panes(wb, "S1", {}), + ] + for call in calls: + with pytest.raises(UnsupportedAdapterOperationError): + call() From 19440cae60748e033c477bf247f5f16ad92d30a3 Mon Sep 17 00:00:00 2001 From: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com> Date: Fri, 8 May 2026 01:48:11 -0700 Subject: [PATCH 4/4] test(adapters): migrate xlwt unsupported roundtrip test --- tests/test_xlwt_xlrd_roundtrip.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/test_xlwt_xlrd_roundtrip.py b/tests/test_xlwt_xlrd_roundtrip.py index 6311156..19afba8 100644 --- a/tests/test_xlwt_xlrd_roundtrip.py +++ b/tests/test_xlwt_xlrd_roundtrip.py @@ -6,11 +6,13 @@ from __future__ import annotations +from collections.abc import Callable from datetime import date, datetime from pathlib import Path import pytest +from excelbench.harness.adapters.base import UnsupportedAdapterOperationError from excelbench.harness.adapters.xlrd_adapter import XlrdAdapter from excelbench.harness.adapters.xlwt_adapter import ( XlwtAdapter, @@ -573,20 +575,24 @@ def test_freeze(self, xlwt: XlwtAdapter, xlrd: XlrdAdapter, tmp_path: Path) -> N xlrd.close_workbook(rb) -# ── xlwt no-op tier 2 methods ──────────────────────────────────────────── +# ── xlwt unsupported tier 2 methods ────────────────────────────────────── -class TestXlwtNoOps: - def test_noop_methods(self, xlwt: XlwtAdapter) -> None: +class TestXlwtUnsupportedOperations: + def test_unsupported_methods(self, xlwt: XlwtAdapter) -> None: wb = xlwt.create_workbook() xlwt.add_sheet(wb, "S1") - # These are all no-ops but should not raise - xlwt.add_conditional_format(wb, "S1", {}) - xlwt.add_data_validation(wb, "S1", {}) - xlwt.add_hyperlink(wb, "S1", {}) - xlwt.add_image(wb, "S1", {}) - xlwt.add_pivot_table(wb, "S1", {}) - xlwt.add_comment(wb, "S1", {}) + calls: list[Callable[[], None]] = [ + lambda: xlwt.add_conditional_format(wb, "S1", {}), + lambda: xlwt.add_data_validation(wb, "S1", {}), + lambda: xlwt.add_hyperlink(wb, "S1", {}), + lambda: xlwt.add_image(wb, "S1", {}), + lambda: xlwt.add_pivot_table(wb, "S1", {}), + lambda: xlwt.add_comment(wb, "S1", {}), + ] + for call in calls: + with pytest.raises(UnsupportedAdapterOperationError): + call() # ── xlrd tier 2 read stubs (empty returns) ───────────────────────────────