diff --git a/.github/dependabot/constraints.txt b/.github/dependabot/constraints.txt index 1483f39c..355a6b33 100644 --- a/.github/dependabot/constraints.txt +++ b/.github/dependabot/constraints.txt @@ -1,18 +1,18 @@ EXtra-data==1.18.0 -ipython==8.29.0 +ipython==8.30.0 kafka-python==2.0.2 -matplotlib==3.9.2 +matplotlib==3.10.0 mplcursors==0.6 mpl-pan-zoom==1.0.0 -numpy==2.1.3 +numpy==2.2.0 openpyxl==3.1.5 pandas==2.2.3 pyflakes==3.2.0 PyQt5==5.15.11 -pytest==8.3.3 +pytest==8.3.4 pytest-qt==4.4.0 pytest-xvfb==3.0.0 QScintilla==2.13.0 supervisor==4.2.5 termcolor==2.5.0 -xarray==2024.10.0 +xarray==2024.11.0 diff --git a/damnit/ctxsupport/ctxrunner.py b/damnit/ctxsupport/ctxrunner.py index 21c846af..e6c404ec 100644 --- a/damnit/ctxsupport/ctxrunner.py +++ b/damnit/ctxsupport/ctxrunner.py @@ -415,7 +415,11 @@ def generate_thumbnail(image): fig.subplots_adjust(left=0, right=1, bottom=0, top=1) vmin = np.nanquantile(image, 0.01) vmax = np.nanquantile(image, 0.99) - ax.imshow(image, vmin=vmin, vmax=vmax, extent=(0, 1, 1, 0)) + if isinstance(image, np.ndarray): + ax.imshow(image, vmin=vmin, vmax=vmax) + else: + # Use DataArray's own plotting method + image.plot(ax=ax, vmin=vmin, vmax=vmax, add_colorbar=False) ax.axis('tight') ax.axis('off') ax.margins(0, 0) @@ -519,7 +523,12 @@ def summarise(self, name): if data.ndim == 0: return data elif data.ndim == 2: - return generate_thumbnail(np.nan_to_num(data)) + if isinstance(data, np.ndarray): + data = np.nan_to_num(data) + else: + data = data.fillna(0) + + return generate_thumbnail(data) else: return f"{data.dtype}: {data.shape}" diff --git a/damnit/gui/editor.py b/damnit/gui/editor.py index ee516e0b..b933f602 100644 --- a/damnit/gui/editor.py +++ b/damnit/gui/editor.py @@ -1,4 +1,5 @@ import sys +import traceback from enum import Enum from io import StringIO from pathlib import Path @@ -26,12 +27,17 @@ class ContextFileCheckerThread(QThread): # ContextTestResult, traceback, lineno, offset, checked_code check_result = pyqtSignal(object, str, int, int, str) - def __init__(self, code, db_dir, context_python, parent=None): + def __init__(self, code, db_dir, context_python, context_getter, parent=None): super().__init__(parent) self.code = code self.db_dir = db_dir self.context_python = context_python + # This is a hack to allow us to test throwing an exception when checking + # the context file. It'll always be get_context_file() except when + # replaced with a Mock by the tests. + self.context_getter = context_getter + def run(self): error_info = None @@ -51,7 +57,16 @@ def run(self): ctx_path = Path(ctx_file.name) ctx_path.write_text(self.code) - ctx, error_info = get_context_file(ctx_path, self.context_python) + try: + ctx, error_info = self.context_getter(ctx_path, self.context_python) + except: + # Not a failure to evalute the context file, but a failure + # to *attempt* to evaluate the context file (e.g. because of + # a missing dependency). + help_msg = "# This is a partial error, please check the terminal for the full error message and ask for help from DA or the DOC." + traceback_str = f"{help_msg}\n\n{traceback.format_exc()}" + self.check_result.emit(ContextTestResult.ERROR, traceback_str, 0, 0, self.code) + return if error_info is not None: stacktrace, lineno, offset = error_info @@ -110,7 +125,7 @@ def __init__(self): def launch_test_context(self, db): context_python = db.metameta.get("context_python") - thread = ContextFileCheckerThread(self.text(), db.path.parent, context_python, parent=self) + thread = ContextFileCheckerThread(self.text(), db.path.parent, context_python, get_context_file, parent=self) thread.check_result.connect(self.on_test_result) thread.finished.connect(thread.deleteLater) thread.start() diff --git a/docs/changelog.md b/docs/changelog.md index 2df537cf..b4b61e47 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,9 @@ Fixed: - Added back grid lines for plots of `DataArray`'s (!334). +- Fixed thumbnails of 2D `DataArray`'s to match what is displayed when the + variable is plotted (!355). +- Fixed crashes when the context file environment is missing dependencies (!356). ## [0.1.4] diff --git a/tests/test_backend.py b/tests/test_backend.py index 680affea..cd5cf738 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -325,13 +325,18 @@ def foo(run): return xr.DataArray([1, 2, 3]) figure_code = """ import numpy as np + import xarray as xr from damnit_ctx import Variable from matplotlib import pyplot as plt - @Variable("2D array") + @Variable("2D ndarray") def twodarray(run): return np.random.rand(1000, 1000) + @Variable("2D xarray") + def twodxarray(run): + return xr.DataArray(np.random.rand(100, 100)) + @Variable(title="Axes") def axes(run): _, ax = plt.subplots() @@ -359,8 +364,9 @@ def figure(run): assert f["axes/data"].ndim == 3 # Test that the summaries are the right size - twodarray_png = Image.open(io.BytesIO(f[".reduced/twodarray"][()])) - assert np.asarray(twodarray_png).shape == (THUMBNAIL_SIZE, THUMBNAIL_SIZE, 4) + for var in ["twodarray", "twodxarray"]: + png = Image.open(io.BytesIO(f[f".reduced/{var}"][()])) + assert np.asarray(png).shape == (THUMBNAIL_SIZE, THUMBNAIL_SIZE, 4) figure_png = Image.open(io.BytesIO(f[".reduced/figure"][()])) assert max(np.asarray(figure_png).shape) == THUMBNAIL_SIZE diff --git a/tests/test_gui.py b/tests/test_gui.py index 93bd99e8..ab73b843 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -1,5 +1,6 @@ import os import re +import sys import textwrap from contextlib import contextmanager from pathlib import Path @@ -151,6 +152,15 @@ def test_editor(mock_db, mock_ctx, qtbot): win.save_context() assert ctx_path.read_text() == warning_code + # Throwing an exception when evaluating the context file in a different + # environment should be handled gracefully. This can happen if running the + # ctxrunner itself fails, e.g. because of a missing dependency. + db.metameta["context_python"] = sys.executable + with qtbot.waitSignal(editor.check_result) as sig, \ + patch("damnit.gui.editor.get_context_file", side_effect=Exception("foo")): + editor.launch_test_context(db) + assert sig.args[0] == ContextTestResult.ERROR + def test_settings(mock_db_with_data, mock_ctx, tmp_path, monkeypatch, qtbot): db_dir, db = mock_db_with_data monkeypatch.chdir(db_dir)