From 1868cdf239164b668de9ddc20fb31beb935fba90 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 27 Dec 2025 15:06:52 +0000
Subject: [PATCH 1/3] Initial plan
From f00f05280c1cd89f0c10015248ca06118e9c92ef Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 27 Dec 2025 15:12:52 +0000
Subject: [PATCH 2/3] Add pandas DataFrame support to Otter reports
Co-authored-by: transientlunatic <4365778+transientlunatic@users.noreply.github.com>
---
README.rst | 17 ++++++++-
docs/usage.rst | 21 +++++++++++
otter/html.py | 32 ++++++++++++-----
pyproject.toml | 3 ++
tests/test_otter.py | 85 +++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 149 insertions(+), 9 deletions(-)
diff --git a/README.rst b/README.rst
index 118e63c..52aacfb 100644
--- a/README.rst
+++ b/README.rst
@@ -56,11 +56,26 @@ This report was generated with just a small number of lines of Python: ::
report + "##Subsection Header"
report + "Fusce vel lectus ultricies,... "
+Otter also supports pandas DataFrames (requires pandas to be installed): ::
+
+ import pandas as pd
+
+ df = pd.DataFrame({
+ 'Name': ['Alice', 'Bob', 'Charlie'],
+ 'Age': [25, 30, 35],
+ 'City': ['New York', 'London', 'Paris']
+ })
+
+ with report:
+ report + "## Data Table"
+ report + df
+
+The DataFrame will be automatically converted to a nicely formatted HTML table.
Features
--------
-* TODO Add support for pandas data tables
+* Support for pandas DataFrames (optional dependency)
* TODO Add support for custom headers and footers
Credits
diff --git a/docs/usage.rst b/docs/usage.rst
index 09872d5..787c0bf 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -57,3 +57,24 @@ This will add a plot of a sinusoid to the report.
Otter will automatically try and handle a number of other data formats automatically, and make them
aesthetically pleasing. Dictionaries get turned into tables, for example, as do numpy arrays, while lists are turned into HTML lists.
+Pandas DataFrames
+^^^^^^^^^^^^^^^^^
+
+If you have pandas installed, Otter can also automatically render pandas DataFrames as HTML tables. For example: ::
+
+ import pandas as pd
+
+ df = pd.DataFrame({
+ 'Name': ['Alice', 'Bob', 'Charlie'],
+ 'Age': [25, 30, 35],
+ 'City': ['New York', 'London', 'Paris']
+ })
+
+ with report:
+ report + "## Employee Data"
+ report + df
+
+The DataFrame will be automatically converted to a nicely formatted HTML table with proper headers and styling that matches the Bootstrap theme.
+
+Note: pandas is an optional dependency. Install it with ``pip install pandas`` or ``pip install otter-report[pandas]`` to enable DataFrame support.
+
diff --git a/otter/html.py b/otter/html.py
index 0412bd0..aab5e95 100644
--- a/otter/html.py
+++ b/otter/html.py
@@ -4,6 +4,14 @@
import markdown
import tabulate
+# Try to import pandas, but make it optional
+try:
+ import pandas as pd
+ PANDAS_AVAILABLE = True
+except ImportError:
+ pd = None
+ PANDAS_AVAILABLE = False
+
md_extensions = [
'markdown.extensions.tables',
'markdown.extensions.extra'
@@ -84,14 +92,6 @@ def dict_to_table(dictionary):
table + Row([key, val])
return table
-handlers = {
- str: lambda x: markdown.markdown(str(x), output_format='xhtml5', extensions=md_extensions),
- matplotlib.figure.Figure: plot.Figure,
- list: OrderedList,
- dict: dict_to_table,
- numpy.ndarray: lambda x: tabulate.tabulate(x, tablefmt=MyHTMLFormat)
-}
-
from functools import partial
def my_html_row_with_attrs(celltag, cell_values, colwidths, colaligns):
alignment = { "left": '',
@@ -113,3 +113,19 @@ def my_html_row_with_attrs(celltag, cell_values, colwidths, colaligns):
headerrow=partial(my_html_row_with_attrs, "th"),
datarow=partial(my_html_row_with_attrs, "td"),
padding=0, with_header_hide=None)
+
+def dataframe_to_table(df):
+ """Convert a pandas DataFrame to an HTML table using tabulate."""
+ return tabulate.tabulate(df, headers='keys', tablefmt=MyHTMLFormat)
+
+handlers = {
+ str: lambda x: markdown.markdown(str(x), output_format='xhtml5', extensions=md_extensions),
+ matplotlib.figure.Figure: plot.Figure,
+ list: OrderedList,
+ dict: dict_to_table,
+ numpy.ndarray: lambda x: tabulate.tabulate(x, tablefmt=MyHTMLFormat)
+}
+
+# Add pandas DataFrame handler if pandas is available
+if PANDAS_AVAILABLE:
+ handlers[pd.DataFrame] = dataframe_to_table
diff --git a/pyproject.toml b/pyproject.toml
index 5e18666..fe5bcac 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,6 +33,9 @@ dependencies = [
]
[project.optional-dependencies]
+pandas = [
+ "pandas",
+]
dev = [
"bumpversion>=0.6.0",
"wheel>=0.42",
diff --git a/tests/test_otter.py b/tests/test_otter.py
index 0fa26db..8872a17 100755
--- a/tests/test_otter.py
+++ b/tests/test_otter.py
@@ -144,6 +144,91 @@ def test_container_creation(self):
self.assertIn("
", output)
+class TestPandasSupport(unittest.TestCase):
+ """Test pandas DataFrame support."""
+
+ def setUp(self):
+ """Set up test fixtures."""
+ try:
+ import pandas as pd
+ self.pandas_available = True
+ self.pd = pd
+ except ImportError:
+ self.pandas_available = False
+ self.pd = None
+
+ def test_pandas_dataframe_handler(self):
+ """Test that pandas DataFrames are converted to HTML tables."""
+ if not self.pandas_available:
+ self.skipTest("pandas is not installed")
+
+ # Create a simple DataFrame
+ df = self.pd.DataFrame({
+ 'A': [1, 2, 3],
+ 'B': [4, 5, 6],
+ 'C': [7, 8, 9]
+ })
+
+ # Check that the handler exists
+ self.assertIn(self.pd.DataFrame, html.handlers)
+
+ # Convert DataFrame to HTML
+ result = html.handlers[self.pd.DataFrame](df)
+
+ # Check that the result is a string containing table HTML
+ self.assertIsInstance(result, str)
+ self.assertIn("
", result)
+
+ def test_pandas_dataframe_in_report(self):
+ """Test adding a pandas DataFrame to an Otter report."""
+ if not self.pandas_available:
+ self.skipTest("pandas is not installed")
+
+ # Create a test report
+ test_dir = tempfile.mkdtemp()
+ test_file = os.path.join(test_dir, "test_pandas.html")
+
+ try:
+ report = otter.Otter(test_file, title="Pandas Test")
+
+ # Create a simple DataFrame
+ df = self.pd.DataFrame({
+ 'Name': ['Alice', 'Bob', 'Charlie'],
+ 'Age': [25, 30, 35],
+ 'City': ['New York', 'London', 'Paris']
+ })
+
+ # Add DataFrame to report
+ report + df
+
+ # Check that it was added
+ self.assertEqual(len(report.items), 1)
+
+ # Render the report
+ report.show()
+
+ # Check that the file was created
+ self.assertTrue(os.path.exists(test_file))
+
+ # Check that the file contains the DataFrame data
+ with open(test_file, 'r') as f:
+ content = f.read()
+ self.assertIn("Alice", content)
+ self.assertIn("Bob", content)
+ self.assertIn("Charlie", content)
+
+ finally:
+ # Clean up
+ if os.path.exists(test_dir):
+ try:
+ shutil.rmtree(test_dir)
+ except (OSError, PermissionError):
+ import time
+ time.sleep(0.1)
+ shutil.rmtree(test_dir)
+
+
if __name__ == '__main__':
import sys
sys.exit(unittest.main())
From a97a8ba0f24f0dd93dfdb894052e023d1844a410 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 27 Dec 2025 16:39:25 +0000
Subject: [PATCH 3/3] Refactor TestPandasSupport to use tearDown for cleanup
consistency
Co-authored-by: transientlunatic <4365778+transientlunatic@users.noreply.github.com>
---
tests/test_otter.py | 80 ++++++++++++++++++++++-----------------------
1 file changed, 39 insertions(+), 41 deletions(-)
diff --git a/tests/test_otter.py b/tests/test_otter.py
index 8872a17..b85c9d7 100755
--- a/tests/test_otter.py
+++ b/tests/test_otter.py
@@ -156,6 +156,19 @@ def setUp(self):
except ImportError:
self.pandas_available = False
self.pd = None
+ self.test_dir = tempfile.mkdtemp()
+ self.test_file = os.path.join(self.test_dir, "test_pandas.html")
+
+ def tearDown(self):
+ """Clean up test fixtures."""
+ if os.path.exists(self.test_dir):
+ try:
+ shutil.rmtree(self.test_dir)
+ except (OSError, PermissionError):
+ # On some platforms (e.g., Windows), files may not be released immediately.
+ import time
+ time.sleep(0.1)
+ shutil.rmtree(self.test_dir)
def test_pandas_dataframe_handler(self):
"""Test that pandas DataFrames are converted to HTML tables."""
@@ -185,48 +198,33 @@ def test_pandas_dataframe_in_report(self):
if not self.pandas_available:
self.skipTest("pandas is not installed")
- # Create a test report
- test_dir = tempfile.mkdtemp()
- test_file = os.path.join(test_dir, "test_pandas.html")
+ report = otter.Otter(self.test_file, title="Pandas Test")
- try:
- report = otter.Otter(test_file, title="Pandas Test")
-
- # Create a simple DataFrame
- df = self.pd.DataFrame({
- 'Name': ['Alice', 'Bob', 'Charlie'],
- 'Age': [25, 30, 35],
- 'City': ['New York', 'London', 'Paris']
- })
-
- # Add DataFrame to report
- report + df
-
- # Check that it was added
- self.assertEqual(len(report.items), 1)
-
- # Render the report
- report.show()
-
- # Check that the file was created
- self.assertTrue(os.path.exists(test_file))
-
- # Check that the file contains the DataFrame data
- with open(test_file, 'r') as f:
- content = f.read()
- self.assertIn("Alice", content)
- self.assertIn("Bob", content)
- self.assertIn("Charlie", content)
-
- finally:
- # Clean up
- if os.path.exists(test_dir):
- try:
- shutil.rmtree(test_dir)
- except (OSError, PermissionError):
- import time
- time.sleep(0.1)
- shutil.rmtree(test_dir)
+ # Create a simple DataFrame
+ df = self.pd.DataFrame({
+ 'Name': ['Alice', 'Bob', 'Charlie'],
+ 'Age': [25, 30, 35],
+ 'City': ['New York', 'London', 'Paris']
+ })
+
+ # Add DataFrame to report
+ report + df
+
+ # Check that it was added
+ self.assertEqual(len(report.items), 1)
+
+ # Render the report
+ report.show()
+
+ # Check that the file was created
+ self.assertTrue(os.path.exists(self.test_file))
+
+ # Check that the file contains the DataFrame data
+ with open(self.test_file, 'r') as f:
+ content = f.read()
+ self.assertIn("Alice", content)
+ self.assertIn("Bob", content)
+ self.assertIn("Charlie", content)
if __name__ == '__main__':