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__':