From 4a0c0e7538a9c7592f59a2824a216ee2849e54bc Mon Sep 17 00:00:00 2001 From: Tanishka Singh Date: Tue, 23 Jun 2026 01:52:45 +0530 Subject: [PATCH] fix(plotting): add show=True to PlotMethods.__call__ for blocking display in scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit camelot.plot(table).show() calls matplotlib's Figure.show(), which returns immediately in non-interactive (Agg / script) backends — the window closes before the user sees it (issue #296). The correct entry point for blocking display in a plain Python script is plt.show(block=True). Users reported that passing block=True to the Figure directly raised TypeError, because Figure.show() does not accept that argument. Fix: add show=False to PlotMethods.__call__(). When show=True, call plt.show(block=True) after creating the figure. The default remains False so all existing callers (Jupyter notebooks, image-comparison tests, file-save paths) are unaffected. Usage after this change: fig = camelot.plot(tables[0], kind='grid', show=True) Closes #296 --- camelot/plotting.py | 12 +++++-- tests/test_plot_show.py | 79 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 tests/test_plot_show.py diff --git a/camelot/plotting.py b/camelot/plotting.py index 32883389..009d178a 100644 --- a/camelot/plotting.py +++ b/camelot/plotting.py @@ -176,7 +176,7 @@ def prepare_plot(table, ax=None): class PlotMethods: """Classmethod for plotting methods.""" - def __call__(self, table, kind="text", filename=None, ax=None): + def __call__(self, table, kind="text", filename=None, ax=None, show=False): """Plot elements found on PDF page based on kind specified. Useful for debugging and playing with different @@ -193,6 +193,11 @@ def __call__(self, table, kind="text", filename=None, ax=None): filename: str, optional (default: None) Absolute path for saving the generated plot. ax : matplotlib.axes.Axes (optional) + show : bool, optional (default: False) + If True, call ``plt.show(block=True)`` after creating the figure + so the window stays open until closed by the user. Useful when + running camelot in a plain Python script where + ``figure.show()`` returns immediately. Returns ------- @@ -215,7 +220,10 @@ def __call__(self, table, kind="text", filename=None, ax=None): fig.savefig(filename) return None - return plot_method(table, ax) + fig = plot_method(table, ax) + if show: + plt.show(block=True) + return fig def text(self, table, ax=None): """Generate a plot for all text elements present on the PDF page. diff --git a/tests/test_plot_show.py b/tests/test_plot_show.py new file mode 100644 index 00000000..08e884b8 --- /dev/null +++ b/tests/test_plot_show.py @@ -0,0 +1,79 @@ +"""Unit tests for the ``show`` parameter of ``PlotMethods.__call__`` (issue #296). + +``camelot.plot(table).show()`` calls matplotlib's ``Figure.show()`` which +returns immediately in non-interactive (script) backends, so the window +closes before the user can see it. + +The fix adds ``show=True`` to ``camelot.plot()`` which internally calls +``plt.show(block=True)`` — the correct blocking entry point for scripts. +These tests verify that the parameter is wired correctly without needing +a real PDF or a display. +""" + +from unittest.mock import MagicMock, patch, call +import pytest + + +def _make_table(flavor="lattice"): + """Return a minimal mock that satisfies PlotMethods.__call__.""" + table = MagicMock() + table.flavor = flavor + return table + + +@pytest.fixture() +def plot_methods(): + from camelot.plotting import PlotMethods + + return PlotMethods() + + +class TestPlotMethodsShowParameter: + """Regression tests for the show=True blocking fix (issue #296).""" + + def test_show_false_does_not_call_plt_show(self, plot_methods): + """Default (show=False) must not call plt.show() — existing behaviour unchanged.""" + table = _make_table() + mock_fig = MagicMock() + + with patch.object(plot_methods, "text", return_value=mock_fig) as mock_plot, \ + patch("camelot.plotting.plt.show") as mock_plt_show: + plot_methods(table, kind="text", show=False) + + mock_plt_show.assert_not_called() + + def test_show_true_calls_plt_show_with_block(self, plot_methods): + """show=True must call plt.show(block=True) so scripts stay open.""" + table = _make_table() + mock_fig = MagicMock() + + with patch.object(plot_methods, "text", return_value=mock_fig), \ + patch("camelot.plotting.plt.show") as mock_plt_show: + result = plot_methods(table, kind="text", show=True) + + mock_plt_show.assert_called_once_with(block=True) + assert result is mock_fig + + def test_show_true_returns_figure(self, plot_methods): + """show=True must still return the figure so callers can further inspect it.""" + table = _make_table() + sentinel_fig = MagicMock(name="sentinel_fig") + + with patch.object(plot_methods, "text", return_value=sentinel_fig), \ + patch("camelot.plotting.plt.show"): + result = plot_methods(table, kind="text", show=True) + + assert result is sentinel_fig + + def test_show_ignored_when_filename_given(self, plot_methods): + """When filename is provided the figure is saved, not displayed.""" + table = _make_table() + mock_fig = MagicMock() + + with patch.object(plot_methods, "text", return_value=mock_fig), \ + patch("camelot.plotting.plt.show") as mock_plt_show: + result = plot_methods(table, kind="text", filename="/tmp/out.png", show=True) + + mock_plt_show.assert_not_called() + mock_fig.savefig.assert_called_once_with("/tmp/out.png") + assert result is None