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
29 changes: 28 additions & 1 deletion gslides_api/agnostic/element.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import html
import json
import re
from abc import ABC, abstractmethod
from enum import Enum
from typing import Any, List, Literal
from typing import Any, List, Literal, Optional

import marko
from pydantic import BaseModel, Field, field_validator, model_validator
Expand Down Expand Up @@ -53,6 +54,32 @@ def to_markdown(self) -> str:

return "\n".join(lines)

def to_html(self, css_class: Optional[str] = None) -> str:
"""Convert table data to an HTML <table> element.

Args:
css_class: Optional CSS class to apply to the <table> element.

Returns:
HTML string with <table>, <thead>, and <tbody> elements.
"""
if not self.headers:
return ""

cls_attr = f' class="{html.escape(css_class)}"' if css_class else ""
parts = [f"<table{cls_attr}>", "<thead><tr>"]
for header in self.headers:
parts.append(f"<th>{html.escape(str(header))}</th>")
parts.append("</tr></thead><tbody>")
for row in self.rows:
parts.append("<tr>")
for i in range(len(self.headers)):
cell = str(row[i]) if i < len(row) else ""
parts.append(f"<td>{html.escape(cell)}</td>")
parts.append("</tr>")
parts.append("</tbody></table>")
return "".join(parts)

def to_dataframe(self):
"""Convert table data to pandas DataFrame.

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "gslides-api"
version = "0.3.5"
version = "0.3.6"
description = "A Python library for working with Google Slides API using Pydantic domain objects"
authors = ["motley.ai <info@motley.ai>"]
license = "MIT"
Expand Down
69 changes: 69 additions & 0 deletions tests/test_table_data_to_html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Tests for TableData.to_html() method."""

import pytest

from gslides_api.agnostic.element import TableData


class TestTableDataToHtml:
def test_basic_table(self):
td = TableData(headers=["Name", "Value"], rows=[["Alice", "100"], ["Bob", "200"]])
result = td.to_html()
assert "<table>" in result
assert "<thead>" in result
assert "<tbody>" in result
assert "<th>Name</th>" in result
assert "<th>Value</th>" in result
assert "<td>Alice</td>" in result
assert "<td>100</td>" in result
assert "<td>Bob</td>" in result
assert "<td>200</td>" in result

def test_with_css_class(self):
td = TableData(headers=["A"], rows=[["1"]])
result = td.to_html(css_class="dtbl")
assert '<table class="dtbl">' in result

def test_without_css_class(self):
td = TableData(headers=["A"], rows=[["1"]])
result = td.to_html()
assert "<table>" in result
assert "class=" not in result

def test_empty_headers(self):
td = TableData(headers=[], rows=[])
assert td.to_html() == ""

def test_html_escaping(self):
td = TableData(
headers=["<script>", "A&B"],
rows=[["x < y", '"quoted"']],
)
result = td.to_html()
assert "&lt;script&gt;" in result
assert "A&amp;B" in result
assert "x &lt; y" in result
assert "&quot;quoted&quot;" in result
assert "<script>" not in result # must be escaped

def test_css_class_escaping(self):
td = TableData(headers=["A"], rows=[["1"]])
result = td.to_html(css_class='x" onclick="alert(1)')
# The double quote must be escaped so it can't break out of the attribute
assert '&quot;' in result
assert 'class="x&quot; onclick=&quot;alert(1)"' in result

def test_short_row_pads_with_empty(self):
td = TableData(headers=["A", "B", "C"], rows=[["only_one"]])
result = td.to_html()
assert "<td>only_one</td>" in result
assert result.count("<td></td>") == 2

def test_structure_order(self):
td = TableData(headers=["H"], rows=[["R"]])
result = td.to_html()
# Verify structural ordering
assert result.index("<thead>") < result.index("</thead>")
assert result.index("</thead>") < result.index("<tbody>")
assert result.index("<tbody>") < result.index("</tbody>")
assert result.index("</tbody>") < result.index("</table>")
Comment on lines +8 to +69

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add model-validation coverage and move repeated setup into fixtures.

This file verifies HTML serialization well, but it misses domain model validation tests and repeats common TableData setup inline.

Suggested test refactor/extension
 import pytest
+from pydantic import ValidationError
 
 from gslides_api.agnostic.element import TableData
 
+@pytest.fixture
+def single_cell_table() -> TableData:
+    return TableData(headers=["A"], rows=[["1"]])
+
 
 class TestTableDataToHtml:
@@
-    def test_with_css_class(self):
-        td = TableData(headers=["A"], rows=[["1"]])
-        result = td.to_html(css_class="dtbl")
+    def test_with_css_class(self, single_cell_table: TableData):
+        result = single_cell_table.to_html(css_class="dtbl")
         assert '<table class="dtbl">' in result
 
-    def test_without_css_class(self):
-        td = TableData(headers=["A"], rows=[["1"]])
-        result = td.to_html()
+    def test_without_css_class(self, single_cell_table: TableData):
+        result = single_cell_table.to_html()
         assert "<table>" in result
         assert "class=" not in result
+
+    `@pytest.mark.parametrize`(
+        "headers,rows",
+        [
+            (None, [["1"]]),
+            (["A"], None),
+            ("A", [["1"]]),
+        ],
+    )
+    def test_domain_model_validation(self, headers, rows):
+        with pytest.raises(ValidationError):
+            TableData(headers=headers, rows=rows)

As per coding guidelines tests/**/*.py: "Test both API format serialization and domain model validation in test files" and "Use pytest fixtures for common setup patterns in tests".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class TestTableDataToHtml:
def test_basic_table(self):
td = TableData(headers=["Name", "Value"], rows=[["Alice", "100"], ["Bob", "200"]])
result = td.to_html()
assert "<table>" in result
assert "<thead>" in result
assert "<tbody>" in result
assert "<th>Name</th>" in result
assert "<th>Value</th>" in result
assert "<td>Alice</td>" in result
assert "<td>100</td>" in result
assert "<td>Bob</td>" in result
assert "<td>200</td>" in result
def test_with_css_class(self):
td = TableData(headers=["A"], rows=[["1"]])
result = td.to_html(css_class="dtbl")
assert '<table class="dtbl">' in result
def test_without_css_class(self):
td = TableData(headers=["A"], rows=[["1"]])
result = td.to_html()
assert "<table>" in result
assert "class=" not in result
def test_empty_headers(self):
td = TableData(headers=[], rows=[])
assert td.to_html() == ""
def test_html_escaping(self):
td = TableData(
headers=["<script>", "A&B"],
rows=[["x < y", '"quoted"']],
)
result = td.to_html()
assert "&lt;script&gt;" in result
assert "A&amp;B" in result
assert "x &lt; y" in result
assert "&quot;quoted&quot;" in result
assert "<script>" not in result # must be escaped
def test_css_class_escaping(self):
td = TableData(headers=["A"], rows=[["1"]])
result = td.to_html(css_class='x" onclick="alert(1)')
# The double quote must be escaped so it can't break out of the attribute
assert '&quot;' in result
assert 'class="x&quot; onclick=&quot;alert(1)"' in result
def test_short_row_pads_with_empty(self):
td = TableData(headers=["A", "B", "C"], rows=[["only_one"]])
result = td.to_html()
assert "<td>only_one</td>" in result
assert result.count("<td></td>") == 2
def test_structure_order(self):
td = TableData(headers=["H"], rows=[["R"]])
result = td.to_html()
# Verify structural ordering
assert result.index("<thead>") < result.index("</thead>")
assert result.index("</thead>") < result.index("<tbody>")
assert result.index("<tbody>") < result.index("</tbody>")
assert result.index("</tbody>") < result.index("</table>")
import pytest
from pydantic import ValidationError
from gslides_api.agnostic.element import TableData
`@pytest.fixture`
def single_cell_table() -> TableData:
return TableData(headers=["A"], rows=[["1"]])
class TestTableDataToHtml:
def test_basic_table(self):
td = TableData(headers=["Name", "Value"], rows=[["Alice", "100"], ["Bob", "200"]])
result = td.to_html()
assert "<table>" in result
assert "<thead>" in result
assert "<tbody>" in result
assert "<th>Name</th>" in result
assert "<th>Value</th>" in result
assert "<td>Alice</td>" in result
assert "<td>100</td>" in result
assert "<td>Bob</td>" in result
assert "<td>200</td>" in result
def test_with_css_class(self, single_cell_table: TableData):
result = single_cell_table.to_html(css_class="dtbl")
assert '<table class="dtbl">' in result
def test_without_css_class(self, single_cell_table: TableData):
result = single_cell_table.to_html()
assert "<table>" in result
assert "class=" not in result
def test_empty_headers(self):
td = TableData(headers=[], rows=[])
assert td.to_html() == ""
def test_html_escaping(self):
td = TableData(
headers=["<script>", "A&B"],
rows=[["x < y", '"quoted"']],
)
result = td.to_html()
assert "&lt;script&gt;" in result
assert "A&amp;B" in result
assert "x &lt; y" in result
assert "&quot;quoted&quot;" in result
assert "<script>" not in result # must be escaped
def test_css_class_escaping(self):
td = TableData(headers=["A"], rows=[["1"]])
result = td.to_html(css_class='x" onclick="alert(1)')
# The double quote must be escaped so it can't break out of the attribute
assert '&quot;' in result
assert 'class="x&quot; onclick=&quot;alert(1)"' in result
def test_short_row_pads_with_empty(self):
td = TableData(headers=["A", "B", "C"], rows=[["only_one"]])
result = td.to_html()
assert "<td>only_one</td>" in result
assert result.count("<td></td>") == 2
def test_structure_order(self):
td = TableData(headers=["H"], rows=[["R"]])
result = td.to_html()
# Verify structural ordering
assert result.index("<thead>") < result.index("</thead>")
assert result.index("</thead>") < result.index("<tbody>")
assert result.index("<tbody>") < result.index("</tbody>")
assert result.index("</tbody>") < result.index("</table>")
`@pytest.mark.parametrize`(
"headers,rows",
[
(None, [["1"]]),
(["A"], None),
("A", [["1"]]),
],
)
def test_domain_model_validation(self, headers, rows):
with pytest.raises(ValidationError):
TableData(headers=headers, rows=rows)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_table_data_to_html.py` around lines 8 - 69, The tests only
exercise TableData.to_html and repeat TableData setup; add domain model
validation tests and consolidate setup into pytest fixtures: create a fixture
(e.g., tabledata_basic) that returns a TableData instance used by multiple
tests, and add tests asserting model validation behavior on TableData
constructors (e.g., invalid headers types, mismatched row lengths, non-string
cell values) expecting the appropriate exceptions (ValueError/TypeError) instead
of calling to_html; reference the TableData class and its to_html method when
adding these tests and use pytest.fixture to DRY the repeated setup.