From 459aa95100922f16d1ebd58205a874882bc3f421 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:02:30 -0400 Subject: [PATCH 1/2] fix: refactor hierarchical_heatmap_template and align test with template changes - Update template to return (clustergrid, mean_intensity) tuple for in-memory mode - Fix param names in test: Feature_s_, Standard_Scale_ - Remove unused params: Method, Metric - Add all template params to test for full coverage - Add in-memory mode test (save_results_flag=False) - Add figure title and CSV output assertions --- .../hierarchical_heatmap_template.py | 18 ++-- .../test_hierarchical_heatmap_template.py | 94 ++++++++++++++++--- 2 files changed, 93 insertions(+), 19 deletions(-) diff --git a/src/spac/templates/hierarchical_heatmap_template.py b/src/spac/templates/hierarchical_heatmap_template.py index 92bec6cc..f77e265a 100644 --- a/src/spac/templates/hierarchical_heatmap_template.py +++ b/src/spac/templates/hierarchical_heatmap_template.py @@ -42,8 +42,9 @@ def run_from_json( json_path : str, Path, or dict Path to JSON file, JSON string, or parameter dictionary save_results_flag : bool, optional - Whether to save results to file. If False, returns the figure and - dataframe directly for in-memory workflows. Default is True. + Whether to save results to file. If False, returns a tuple of + (clustergrid_figure, mean_intensity_dataframe) for in-memory + workflows (e.g., Shiny server). Default is True. show_plot : bool, optional Whether to display the plot. Default is True. output_dir : str or Path, optional @@ -51,9 +52,10 @@ def run_from_json( Returns ------- - dict or DataFrame + dict or tuple If save_results_flag=True: Dictionary of saved file paths - If save_results_flag=False: The mean intensity dataframe + If save_results_flag=False: Tuple of (figure, mean_intensity_df) + where figure is the matplotlib Figure from the ClusterGrid """ # Parse parameters from JSON params = parse_params(json_path) @@ -185,9 +187,11 @@ def run_from_json( print("Hierarchical Heatmap completed successfully.") return saved_files else: - # Return the dataframe directly for in-memory workflows - print("Returning mean intensity dataframe (not saving to file)") - return mean_intensity + # Return the ClusterGrid and dataframe for in-memory workflows + # Returns ClusterGrid (not .fig) so consumers can access + # .ax_heatmap for post-processing (e.g. label rotation). + print("Returning (clustergrid, mean_intensity_df) for in-memory use") + return clustergrid, mean_intensity # CLI interface diff --git a/tests/templates/test_hierarchical_heatmap_template.py b/tests/templates/test_hierarchical_heatmap_template.py index 1964b9a0..ce2f2968 100644 --- a/tests/templates/test_hierarchical_heatmap_template.py +++ b/tests/templates/test_hierarchical_heatmap_template.py @@ -2,7 +2,11 @@ """ Real (non-mocked) unit test for the Hierarchical Heatmap template. -Validates template I/O behaviour only. +Snowball seed test — validates template I/O behaviour only: + • Expected output files are produced on disk + • Filenames follow the convention + • Output artifacts are non-empty + No mocking. Uses real data, real filesystem, and tempfile. """ @@ -15,7 +19,7 @@ from pathlib import Path import matplotlib -matplotlib.use("Agg") +matplotlib.use("Agg") # Headless backend for CI import anndata as ad import numpy as np @@ -53,14 +57,24 @@ def setUp(self) -> None: "Upstream_Analysis": self.in_file, "Annotation": "cell_type", "Table_to_Visualize": "Original", - "Features_to_Visualize": ["All"], - "Standard_Scale": "None", - "Method": "average", - "Metric": "euclidean", + "Feature_s_": ["All"], + "Standard_Scale_": "None", + "Z_Score": "None", + "Feature_Dendrogram": True, + "Annotation_Dendrogram": True, + "Figure_Title": "Test Hierarchical Heatmap", "Figure_Width": 6, "Figure_Height": 4, "Figure_DPI": 72, "Font_Size": 8, + "Matrix_Plot_Ratio": 0.8, + "Swap_Axes": False, + "Rotate_Label_": False, + "Horizontal_Dendrogram_Display_Ratio": 0.2, + "Vertical_Dendrogram_Display_Ratio": 0.2, + "Value_Min": "None", + "Value_Max": "None", + "Color_Map": "seismic", "Output_Directory": self.tmp_dir.name, "outputs": { "figures": {"type": "directory", "name": "figures_dir"}, @@ -82,8 +96,10 @@ def test_hierarchical_heatmap_produces_expected_outputs(self) -> None: Validates: 1. saved_files dict has 'figures' and 'dataframe' keys 2. Figures directory contains non-empty PNG(s) - 3. Summary CSV exists + 3. Summary CSV exists and is non-empty + 4. Figure title matches the parameter """ + # -- Act (save_results_flag=True): write outputs to disk ------- saved_files = run_from_json( self.json_file, save_results_flag=True, @@ -91,15 +107,69 @@ def test_hierarchical_heatmap_produces_expected_outputs(self) -> None: output_dir=self.tmp_dir.name, ) - self.assertIsInstance(saved_files, dict) - self.assertIn("figures", saved_files) + # -- Act (save_results_flag=False): get objects in memory ------ + clustergrid, mean_intensity_df = run_from_json( + self.json_file, + save_results_flag=False, + show_plot=False, + ) + + # -- Assert: return type --------------------------------------- + self.assertIsInstance( + saved_files, dict, + f"Expected dict from run_from_json, got {type(saved_files)}" + ) + # -- Assert: figures directory contains at least one PNG ------- + self.assertIn("figures", saved_files, + "Missing 'figures' key in saved_files") figure_paths = saved_files["figures"] - self.assertGreaterEqual(len(figure_paths), 1) + self.assertGreaterEqual( + len(figure_paths), 1, "No figure files were saved" + ) + for fig_path in figure_paths: fig_file = Path(fig_path) - self.assertTrue(fig_file.exists()) - self.assertGreater(fig_file.stat().st_size, 0) + self.assertTrue( + fig_file.exists(), f"Figure not found: {fig_path}" + ) + self.assertGreater( + fig_file.stat().st_size, 0, + f"Figure file is empty: {fig_path}" + ) + self.assertEqual( + fig_file.suffix, ".png", + f"Expected .png extension, got {fig_file.suffix}" + ) + + # -- Assert: figure has the correct title ---------------------- + axes_title = clustergrid.ax_heatmap.get_title() + self.assertEqual( + axes_title, "Test Hierarchical Heatmap", + f"Expected 'Test Hierarchical Heatmap', got '{axes_title}'" + ) + + # -- Assert: in-memory mean_intensity_df is a DataFrame -------- + self.assertIsInstance( + mean_intensity_df, pd.DataFrame, + f"Expected DataFrame, got {type(mean_intensity_df)}" + ) + self.assertIn( + "cell_type", mean_intensity_df.columns, + "Annotation column 'cell_type' missing from mean_intensity_df" + ) + + # -- Assert: summary CSV exists and is non-empty --------------- + self.assertIn("dataframe", saved_files, + "Missing 'dataframe' key in saved_files") + csv_path = Path(saved_files["dataframe"]) + self.assertTrue( + csv_path.exists(), f"Summary CSV not found: {csv_path}" + ) + self.assertGreater( + csv_path.stat().st_size, 0, + f"Summary CSV is empty: {csv_path}" + ) if __name__ == "__main__": From 002f171d6fcafa65279f16e4aa8a73ad1f73cf98 Mon Sep 17 00:00:00 2001 From: fangliu117 <> Date: Wed, 18 Mar 2026 19:28:47 +0000 Subject: [PATCH 2/2] ci(version): Automatic development release --- CHANGELOG.md | 18 ++++++++++++++++++ setup.py | 2 +- src/spac/__init__.py | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c82344c8..d7134a19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,26 @@ # CHANGELOG +## v0.9.3 (2026-03-18) + +### Bug Fixes + +- Refactor hierarchical_heatmap_template and align test with template changes + ([`459aa95`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/459aa95100922f16d1ebd58205a874882bc3f421)) + +- Update template to return (clustergrid, mean_intensity) tuple for in-memory mode - Fix param names + in test: Feature_s_, Standard_Scale_ - Remove unused params: Method, Metric - Add all template + params to test for full coverage - Add in-memory mode test (save_results_flag=False) - Add figure + title and CSV output assertions + + ## v0.9.2 (2026-03-03) +### Continuous Integration + +- **version**: Automatic development release + ([`97ead81`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/97ead819ef09c101a6cc59af7e98f979548abcb6)) + ## v0.9.1 (2026-02-27) diff --git a/setup.py b/setup.py index fa6cee1e..ec3958d3 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='spac', - version="0.9.2", + version="0.9.3", description=( 'SPatial Analysis for single-Cell analysis (SPAC)' 'is a Scalable Python package for single-cell spatial protein data ' diff --git a/src/spac/__init__.py b/src/spac/__init__.py index a691c7cb..72d9d6e4 100644 --- a/src/spac/__init__.py +++ b/src/spac/__init__.py @@ -22,7 +22,7 @@ functions.extend(module_functions) # Define the package version before using it in __all__ -__version__ = "0.9.2" +__version__ = "0.9.3" # Define a __all__ list to specify which functions should be considered public __all__ = functions