diff --git a/docs/tool-guide.md b/docs/tool-guide.md index a0b807d..ed54a58 100644 --- a/docs/tool-guide.md +++ b/docs/tool-guide.md @@ -36,6 +36,9 @@ Record numbering begins at 1 for the header; it counts parsed CSV records, not physical lines inside quoted values. Entirely blank lines are skipped by the parser. Empty input returns empty lists and zero rows. Malformed quoting returns `valid=False` and an error message. Extra cells are flagged but not counted as named columns. +Header cells that are empty or whitespace-only are reported by zero-based position in +`empty_header_indices`; a blank name is a warning, not an invalid document, and the +original names are kept in `columns` unchanged. ## hash_text(text, algorithm="sha256") diff --git a/src/mcp_tools_lab/tools/csv_tool.py b/src/mcp_tools_lab/tools/csv_tool.py index 4fd3e19..9235197 100644 --- a/src/mcp_tools_lab/tools/csv_tool.py +++ b/src/mcp_tools_lab/tools/csv_tool.py @@ -8,7 +8,10 @@ def inspect_csv(text: str) -> dict: """Report rows, empty cells, and ragged records (record 1 is the header). Missing counts include absent trailing cells and whitespace-only cells. - Entirely blank records are skipped by Python's CSV reader. + Entirely blank records are skipped by Python's CSV reader. Header cells that + are empty or whitespace-only are reported by zero-based position in + ``empty_header_indices``; the original names are still kept in ``columns``, + and a blank name is a warning, not an invalid document. """ try: rows = list(csv.reader(io.StringIO(text, newline=""), strict=True)) @@ -16,8 +19,10 @@ def inspect_csv(text: str) -> dict: return {"valid": False, "error": str(error)} if not rows: return {"valid": True, "columns": [], "row_count": 0, - "missing_values": [], "inconsistent_rows": []} + "missing_values": [], "inconsistent_rows": [], + "empty_header_indices": []} header, data = rows[0], rows[1:] + empty_header_indices = [index for index, name in enumerate(header) if not name.strip()] missing = [0] * len(header) inconsistent = [] for record, row in enumerate(data, start=2): @@ -30,4 +35,5 @@ def inspect_csv(text: str) -> dict: "missing_values": [ {"column": name, "index": index, "count": missing[index]} for index, name in enumerate(header) - ], "inconsistent_rows": inconsistent} + ], "inconsistent_rows": inconsistent, + "empty_header_indices": empty_header_indices} diff --git a/tests/test_csv_tool_empty_header.py b/tests/test_csv_tool_empty_header.py new file mode 100644 index 0000000..a755334 --- /dev/null +++ b/tests/test_csv_tool_empty_header.py @@ -0,0 +1,36 @@ +from mcp_tools_lab.tools.csv_tool import inspect_csv + + +def test_empty_header_indices_reports_positions(): + result = inspect_csv("name,,age\nAda,20,x\nBob,30,y") + assert result["empty_header_indices"] == [1] + + +def test_whitespace_only_header_reports_position(): + result = inspect_csv("name, ,age\nAda,20,x") + assert result["empty_header_indices"] == [1] + + +def test_empty_header_indices_lists_every_blank_column(): + result = inspect_csv("a,,c,,e\n1,2,3,4,5") + assert result["empty_header_indices"] == [1, 3] + + +def test_header_names_are_still_columns(): + result = inspect_csv("name,,age\nAda,20,x") + assert result["columns"] == ["name", "", "age"] + + +def test_empty_input_has_no_empty_header_indices(): + assert inspect_csv("")["empty_header_indices"] == [] + + +def test_blank_header_is_still_valid(): + assert inspect_csv("name,,age\nAda,20,x")["valid"] is True + + +def test_malformed_csv_does_not_report_empty_header_indices(): + # A document that will not parse has no header positions to name. + result = inspect_csv('name\n"unfinished') + assert result["valid"] is False + assert "empty_header_indices" not in result