From ac0d33c0cd7455157dfa43e379fc8d72bbdb06c8 Mon Sep 17 00:00:00 2001 From: bue Date: Thu, 20 Aug 2026 12:40:26 -0400 Subject: [PATCH 1/2] @ pcdl : pip install pcdl now by default installs lightweight with only the core dependencies; users are prompted to install optional libraries if necessary or can do a pip install pcdl[full] to install all possibly necessary libraries at once. special thanks to Heber Rocha! --- README.md | 10 +- man/HOWTO.md | 8 + man/TUTORIAL_blender.md | 19 +- man/TUTORIAL_matlab_octave.md | 2 +- pcdl/dependency.py | 59 +++++ pcdl/neuromancer.py | 12 +- pcdl/output_data.py | 5 +- pcdl/timeseries.py | 12 +- pcdl/timestep.py | 40 +++- pyproject.toml | 32 ++- test/test_commandline_2d.py | 418 ++++++++++++++++++++-------------- test/test_dependency.py | 237 +++++++++++++++++++ test/test_timeseries_2d.py | 6 + test/test_timeseries_3d.py | 6 + test/test_timestep_2d.py | 6 + test/test_timestep_3d.py | 6 + 16 files changed, 677 insertions(+), 201 deletions(-) create mode 100644 pcdl/dependency.py create mode 100644 test/test_dependency.py diff --git a/README.md b/README.md index 91d0adb..a75a231 100644 --- a/README.md +++ b/README.md @@ -130,15 +130,19 @@ Developers, please make pull requests to the https://github.com/elmbeech/physice ## Release Notes: -+ version 4.1.5 (2026-04-05) + ++ version 4.1.6 (2026-08-22): elmbeech/physicelldataloader + + pcdl installs now by default lightweight with only the core dependencies; users are prompted to install optional libraries if necessary. special thanks to Heber Rocha! + ++ version 4.1.5 (2026-04-05): elmbeech/physicelldataloader + bugfix library dependencies and library versions. -+ version 4.1.4 (2026-04-04) ++ version 4.1.4 (2026-04-04): elmbeech/physicelldataloader + pyMCDSts **mcdsts.make_contour** extrema parameter is replaced by vmin and vmax to be compatible with mcds.make\_contour and plt.contour. + **make_cell_vtk** and **make_conc_vtk** now offer an ext parameter, allowing to manually specify the exact file extension. special thanks to Danyon Gedris! + new **make_muspan** TimeStep class and TimeSeris class function and **pcdl_get_muspan** command line command. special thanks to Joshua Moore and Joshua Bull! -+ version 4.1.3 (2026-03-21): ++ version 4.1.3 (2026-03-21): elmbeech/physicelldataloader + new **pcdl.pccmap** color map. this is an adaptation of the physicell pathology paint by number color map. + new TimeStep **get_pcdl_version** function. diff --git a/man/HOWTO.md b/man/HOWTO.md index 1e72517..16bc416 100644 --- a/man/HOWTO.md +++ b/man/HOWTO.md @@ -8,6 +8,14 @@ Below you will find information about how to install, load, update, uninstall, a ```bash pip3 install pcdl ``` +This installs pcdl light weight: only the libraries needed to load PhysiCell output into pandas dataframes and to render basic contour and scatter plots (matplotlib, numpy, pandas, scipy). +A handful of pcdl functions (e.g. get\_anndata) rely on other libraryes (anndata). +If you call such a function and a necessary library is missing, the function will prompt you to install it. + +To install pcdl with all optional dependencies at once, run: +```bash +pip3 install pcdl[full] +``` ## How to load the pcdl library? diff --git a/man/TUTORIAL_blender.md b/man/TUTORIAL_blender.md index cad8ba3..dddd6be 100644 --- a/man/TUTORIAL_blender.md +++ b/man/TUTORIAL_blender.md @@ -19,7 +19,7 @@ This is why the ext parameter explicitly has to be set. pcdl_make_conc_vtk output ``` ```bash -pcdl_make_cell_vtk output --ext .vtp # blender bvtk nodes compatible filename and extension. +pcdl_make_cell_vtk output --ext .vtp # generate blender bvtk nodes compatible filename and extension! ``` ### Generate vtk files from within python @@ -29,7 +29,7 @@ import pcdl mcdsts = pcdl.TimeSeries('output/') mcdsts.make_conc_vtk() -mcdsts.make_cell_vtk(ext='.vtp') # blender bvtk nodes compatible filename and extension. +mcdsts.make_cell_vtk(ext='.vtp') # generate blender bvtk nodes compatible filename and extension! ``` ### Blender vtk nodes plugin installation @@ -49,6 +49,21 @@ including workspace setup. 5. tkXMLPolyDataReader FileName: path/to/output00000000.vtp 6. VTKtoBlenderMesh: click Update Node +### Load vtk polynomial data vtk files as a time course + +Special thank to Danyon Gedris from the Stein-O'Brien Lab, who figuring all of this out! + +1. In the BVTK Node Tree Workspace from the previous section, right click Add / Custom / TimeSelector. +2. Connect vtkXMLPolyDataReader output with input TimeSelector. +3. Connect TimeSelector output with input VTKtoBlenderMesh. +4. In the BVTK Node Editor, press N to open the sidebar. +5. Select the Inspect tab and change: Update Mode → Update All Automatically. +6. Click update node on each element in the BVTK Node Editor and force update upstream on VTKtoBlenderMesh. +7. Set your scene frame range to: ++ Start = 1 (this selects output00000000.vtp). ++ End = # of .vtp files in the sequence (the last output file number plus 1). +8. Press play on the animation. + ### Load rectilinear grid vtk files I was not able to bridge that data yet. diff --git a/man/TUTORIAL_matlab_octave.md b/man/TUTORIAL_matlab_octave.md index 0a9a74f..d801434 100644 --- a/man/TUTORIAL_matlab_octave.md +++ b/man/TUTORIAL_matlab_octave.md @@ -33,7 +33,7 @@ pcdl_get_cell_df output df_conc = readtable("output/timeseries_conc.csv") ``` ```matlab -df_cell = readtable("output/timeseries_conc.csv") +df_cell = readtable("output/timeseries_cell.csv") ``` diff --git a/pcdl/dependency.py b/pcdl/dependency.py new file mode 100644 index 0000000..da4cc27 --- /dev/null +++ b/pcdl/dependency.py @@ -0,0 +1,59 @@ +###### +# title: dependency.py +# +# language: python3 +# date: 2026-08-14 +# license: BSD-3-Clause +# author: Elmar Bucher, Heber Rocha, Claude Anthropic +# +# description: +# pcdl, by default, only installs and imports lightweight core dependencies. +# the code below deals with installation and import of the more specialized +# heavyweight libraries. +###### + + +# library +import importlib + + +# function +def optional_import(s_module, s_attr=None, s_pip=None, s_caller=None): + """ + input: + s_module: string + dotted module path to import, e.g. 'anndata' or 'bioio.writers'. + + s_attr: string; default None + if given, equivalent to from s_module import s_attr. + + s_pip: string; default None + pip install name for the module, if this differs from s_module + (e.g. s_module='skimage' but s_pip='scikit-image'). + if None, the first dot-separated part of s_module is used. + + s_caller: string; default None + name of the calling pcdl function, to mention in the error message. + + output: + the imported module, or, if s_attr is given, the requested attribute + of the imported module. + + description: + function lazily load an optional pcdl dependency. + if the library is not installed, an error message is raised, + pointing the user to install the missing library. + """ + s_pip = s_module.split('.')[0] if (s_pip is None) else s_pip + try: + o_module = importlib.import_module(s_module) + except ImportError as e: + s_fct = s_caller if not (s_caller is None) else ' ' + raise ModuleNotFoundError( + f"Error{s_fct}: this functionality requires the optional dependency '{s_pip}', which is not installed.\n" + + f"pcdl was installed light weight (default), without this and other heavyweight, specialized libraries.\n" + + f"to fix this, either:\n" + + f"+ install the missing library manually: pip install {s_pip}\n" + + f"+ or install pcdl with all optional dependencies: pip install pcdl[full]\n" + ) from e + return o_module if (s_attr is None) else getattr(o_module, s_attr) diff --git a/pcdl/neuromancer.py b/pcdl/neuromancer.py index 7110c64..59a2bae 100644 --- a/pcdl/neuromancer.py +++ b/pcdl/neuromancer.py @@ -39,14 +39,18 @@ # library import argparse -from bioio import BioImage import matplotlib as mpl -import neuroglancer -import neuroglancer.cli import numpy as np -from skimage import exposure, util import sys +# lazy loading heavyweight: bioio, neuroglancer, and scikit-image +from pcdl.dependency import optional_import +BioImage = optional_import('bioio', s_attr='BioImage', s_pip='bioio', s_caller='pcdl.render_neuroglancer') +neuroglancer = optional_import('neuroglancer', s_caller='pcdl.render_neuroglancer') +optional_import('neuroglancer.cli', s_caller='pcdl.render_neuroglancer') # binds neuroglancer.cli as attribute +exposure = optional_import('skimage.exposure', s_pip='scikit-image', s_caller='pcdl.render_neuroglancer') +util = optional_import('skimage.util', s_pip='scikit-image', s_caller='pcdl.render_neuroglancer') + # functions def ometiff2neuro( diff --git a/pcdl/output_data.py b/pcdl/output_data.py index 21ed8cd..a67ae42 100644 --- a/pcdl/output_data.py +++ b/pcdl/output_data.py @@ -23,7 +23,7 @@ import os import pathlib import pcdl -import requests +from pcdl.dependency import optional_import import shutil import tarfile @@ -40,6 +40,9 @@ class install_data: function to install a 2D and 3D PhysiCell output test dataset. """ def __init__(self): + # load optional dependency + requests = optional_import('requests', s_caller='pcdl.install_data') + # get pcdl library installation path s_path = str(pathlib.Path(pcdl.__file__).parent).replace('\\','/') + '/' diff --git a/pcdl/timeseries.py b/pcdl/timeseries.py index 92097bf..439856d 100644 --- a/pcdl/timeseries.py +++ b/pcdl/timeseries.py @@ -16,15 +16,13 @@ # load libraries -import anndata as ad -import bioio_base -from bioio.writers import OmeTiffWriter import glob import matplotlib.pyplot as plt import numpy as np import os import pandas as pd from pcdl import render_neuroglancer +from pcdl.dependency import optional_import from pcdl.timestep import TimeStep, es_coor_cell, es_coor_conc, _anndextract from pcdl.VERSION import __version__ import platform @@ -1176,6 +1174,11 @@ def make_ome_tiff(self, cell_attribute='ID', conc_cutoff={}, focus=None, file=Tr # output 11 ometiff file elif (file and collapse): # 11 + # load optional dependency + OmeTiffWriter = optional_import('bioio.writers', s_attr='OmeTiffWriter', s_pip='bioio', s_caller='TimeSeries.make_ome_tiff') + bioio_base = optional_import('bioio_base', s_pip='bioio', s_caller='TimeSeries.make_ome_tiff') + + # numpy array a_tczyx_img = np.array(l_tczyx_img) if self.verbose: print('a_tczyx_img shape:', a_tczyx_img.shape) @@ -1661,6 +1664,9 @@ def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs', collapse function to transform mcds time steps into one or many anndata objects for downstream analysis. """ + # load optional dependency + ad = optional_import('anndata', s_caller='TimeSeries.get_anndata') + # initialize vaiable l_annmcds = [] df_anncount = None diff --git a/pcdl/timestep.py b/pcdl/timestep.py index e960ff9..b014d01 100644 --- a/pcdl/timestep.py +++ b/pcdl/timestep.py @@ -15,10 +15,6 @@ # load library -import anndata as ad -import bioio_base -from bioio.writers import OmeTiffWriter -import geopandas as gpd import matplotlib.pyplot as plt from matplotlib import cm from matplotlib import colors @@ -26,20 +22,15 @@ import muspan as ms except ModuleNotFoundError: ms = None -import networkx as nx -import neuroglancer import numpy as np import os import pandas as pd from pcdl import imagine from pcdl import pdplt -from pcdl import neuromancer +from pcdl.dependency import optional_import from scipy import io from scipy import sparse -import shapely -import spatialdata as sd import sys -import vtk import warnings import xml.etree.ElementTree as etree from pcdl.VERSION import __version__ @@ -223,6 +214,10 @@ def render_neuroglancer(tiffpathfile, timestep=0, intensity_cmap='gray'): function to load a time step from an ome tiff files, generated with make_ome_tiff, into neuroglancer. """ + # load optional dependencies (neuromancer itself lazy loads bioio and scikit-image) + neuroglancer = optional_import('neuroglancer', s_caller='pcdl.render_neuroglancer') + from pcdl import neuromancer + # start neuroglancer viewer = neuroglancer.Viewer() with viewer.txn() as state: @@ -1404,6 +1399,9 @@ def make_conc_vtk(self, ext='_conc.vtr'): https://www.paraview.org/ """ + # load optional dependency + vtk = optional_import('vtk', s_caller='TimeStep.make_conc_vtk') + # off we go. s_vtkfile = self.xmlfile.replace('.xml', ext) if self.verbose: @@ -1896,6 +1894,9 @@ def make_cell_vtk(self, attribute=['cell_type'], ext='_cell.vtp'): https://www.paraview.org/ """ + # load optional dependency + vtk = optional_import('vtk', s_caller='TimeStep.make_cell_vtk') + # off we go. s_vtkfile = self.xmlfile.replace('.xml', ext) if self.verbose: @@ -2031,6 +2032,11 @@ def make_ome_tiff(self, cell_attribute='ID', conc_cutoff={}, focus=None, file=Tr https://napari.org/stable/ https://fiji.sc/ """ + # load optional dependencies + if file: + OmeTiffWriter = optional_import('bioio.writers', s_attr='OmeTiffWriter', s_pip='bioio', s_caller='TimeStep.make_ome_tiff') + bioio_base = optional_import('bioio_base', s_pip='bioio', s_caller='TimeStep.make_ome_tiff') + # handle channels ls_substrate = self.get_substrate_list() ls_celltype = self.get_celltype_list() @@ -2404,6 +2410,9 @@ def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs'): function to transform a mcds time step into an anndata object for downstream analysis. """ + # load optional dependency + ad = optional_import('anndata', s_caller='TimeStep.get_anndata') + # processing if self.verbose: print(f'processing: 1/1 {round(self.get_time(),9)}[min] mcds into anndata obj.') @@ -2479,6 +2488,12 @@ def get_spatialdata(self, images={'subs'}, labels={}, points={'subs'}, shapes={' function to transform a mcds time step into a spatialdata object for downstream analysis. """ + # load optional dependencies + ad = optional_import('anndata', s_caller='TimeStep.get_spatialdata') + sd = optional_import('spatialdata', s_caller='TimeStep.get_spatialdata') + shapely = optional_import('shapely', s_caller='TimeStep.get_spatialdata') + gpd = optional_import('geopandas', s_caller='TimeStep.get_spatialdata') + # set table spatial element links s_region_subs = None s_region_cell = None @@ -2720,9 +2735,12 @@ def get_muspan(self, z_slice=None, values=1, drop=set(), keep=set()): + https://docs.muspan.co.uk/latest/Documentation.html """ # check if muspan library is installed - if ms is None: + if (ms is None) or (ms.__file__ is None): sys.exit(f'Error @ TimeStep.get_muspa : the muspan Multi Spatial Analysis python3 library is not installed!\nfor instructions check out : https://www.muspan.co.uk/') + # load optional dependency + nx = optional_import('networkx', s_caller='TimeStep.get_muspan') + # get conc and cell dataframe df_conc = self.get_conc_df(values=values, drop=drop, keep=keep) df_cell = self.get_cell_df(values=values, drop=drop, keep=keep) diff --git a/pyproject.toml b/pyproject.toml index 3b12f79..e7625d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,23 +69,33 @@ classifiers = [ ] # bue 2024-12-06: enforcing some versions +# light weight dependencies = [ - "anndata>=0.10.8", + "matplotlib", + "numpy", + "pandas>=2.2.2", # numpy 2.0 comatible + "scipy>=1.13.0", # numpy 2.0 compatible +] + +[project.optional-dependencies] +# bioconda galaxy +jakku = [ + "anndata>=0.10.8", # numpy 2.0 compatible + "requests", + "vtk", +] +# heavy weight +full = [ + "pcdl[jakku]", "bioio>=2.0.0", "bioio-ome-tiff", - "geopandas>=0.14", # spatialdata - "matplotlib", + "geopandas>=0.14", # spatialdata 0.7.3 compatible "networkx", "neuroglancer", - "numpy", - "pandas>=2.2.2", - "requests", "scikit-image>=0.24.0", - "scipy>=1.13.0", - "shapely>=2.0.1", # spatialdata - "spatialdata>=0.7.2", - "vtk", - "ome-zarr<0.14.0", # bue 20260405: bugfix for spatialdata 0.7.2 + "shapely>=2.0.1", # spatialdata 0.7.3 compatible + "spatialdata>=0.7.3", + #"ome-zarr<0.14.0", # bue 20260405: bugfix for spatialdata 0.7.2 ] diff --git a/test/test_commandline_2d.py b/test/test_commandline_2d.py index f14c814..cff4c3b 100644 --- a/test/test_commandline_2d.py +++ b/test/test_commandline_2d.py @@ -1496,14 +1496,18 @@ class TestCommandLineInterfaceMuspan(object): def test_pcdl_get_muspan_timeseries(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_path_2d], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1511,14 +1515,18 @@ def test_pcdl_get_muspan_timeseries(self): def test_pcdl_get_muspan_timeseries_customtype(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--custom_data_type', 'sample:bool'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--custom_data_type', 'sample:bool'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1526,14 +1534,18 @@ def test_pcdl_get_muspan_timeseries_customtype(self): def test_pcdl_get_muspan_timeseries_microenv(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--microenv', 'false'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--microenv', 'false'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1541,14 +1553,18 @@ def test_pcdl_get_muspan_timeseries_microenv(self): def test_pcdl_get_muspan_timeseries_graph(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--graph', 'false'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--graph', 'false'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1556,14 +1572,18 @@ def test_pcdl_get_muspan_timeseries_graph(self): def test_pcdl_get_muspan_timeseries_physiboss(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--physiboss', 'false'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--physiboss', 'false'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1571,14 +1591,18 @@ def test_pcdl_get_muspan_timeseries_physiboss(self): def test_pcdl_get_muspan_timeseries_settingxmlfalse(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--settingxml', 'false'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--settingxml', 'false'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1586,14 +1610,18 @@ def test_pcdl_get_muspan_timeseries_settingxmlfalse(self): def test_pcdl_get_muspan_timeseries_settingxmlnone(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--settingxml', 'none'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--settingxml', 'none'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1601,14 +1629,18 @@ def test_pcdl_get_muspan_timeseries_settingxmlnone(self): def test_pcdl_get_muspan_timeseries_zslice(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '1.1'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '1.1'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1616,14 +1648,18 @@ def test_pcdl_get_muspan_timeseries_zslice(self): def test_pcdl_get_muspan_timeseries_value(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--value', '2'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--value', '2'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1631,14 +1667,18 @@ def test_pcdl_get_muspan_timeseries_value(self): def test_pcdl_get_muspan_timeseries_drop(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--drop', 'cell_type', 'oxygen'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--drop', 'cell_type', 'oxygen'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1646,14 +1686,18 @@ def test_pcdl_get_muspan_timeseries_drop(self): def test_pcdl_get_muspan_timeseries_keep(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--keep', 'cell_type', 'oxygen'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--keep', 'cell_type', 'oxygen'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1662,13 +1706,17 @@ def test_pcdl_get_muspan_timeseries_keep(self): def test_pcdl_get_muspan_timestep(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/output00000024_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1676,13 +1724,17 @@ def test_pcdl_get_muspan_timestep(self): def test_pcdl_get_muspan_timestep_customtype(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--custom_data_type', 'sample:bool'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--custom_data_type', 'sample:bool'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/output00000024_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1690,13 +1742,17 @@ def test_pcdl_get_muspan_timestep_customtype(self): def test_pcdl_get_muspan_timestep_microenv(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--microenv', 'false'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--microenv', 'false'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/output00000024_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1704,13 +1760,17 @@ def test_pcdl_get_muspan_timestep_microenv(self): def test_pcdl_get_muspan_timestep_graph(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--graph', 'false'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--graph', 'false'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/output00000024_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1718,13 +1778,17 @@ def test_pcdl_get_muspan_timestep_graph(self): def test_pcdl_get_muspan_timestep_physiboss(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--physiboss', 'false'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--physiboss', 'false'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/output00000024_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1732,13 +1796,17 @@ def test_pcdl_get_muspan_timestep_physiboss(self): def test_pcdl_get_muspan_timestep_settingxmlfalse(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--settingxml', 'false'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--settingxml', 'false'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/output00000024_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1746,13 +1814,17 @@ def test_pcdl_get_muspan_timestep_settingxmlfalse(self): def test_pcdl_get_muspan_timestep_settingxmlnone(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--settingxml', 'none'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--settingxml', 'none'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/output00000024_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1760,13 +1832,17 @@ def test_pcdl_get_muspan_timestep_settingxmlnone(self): def test_pcdl_get_muspan_timestep_zslice(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '1.1'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '1.1'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/output00000024_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1774,13 +1850,17 @@ def test_pcdl_get_muspan_timestep_zslice(self): def test_pcdl_get_muspan_timestep_value(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--value', '2'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--value', '2'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/output00000024_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1788,13 +1868,17 @@ def test_pcdl_get_muspan_timestep_value(self): def test_pcdl_get_muspan_timestep_drop(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--drop', 'cell_type', 'oxygen'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--drop', 'cell_type', 'oxygen'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/output00000024_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True @@ -1802,13 +1886,17 @@ def test_pcdl_get_muspan_timestep_drop(self): def test_pcdl_get_muspan_timestep_keep(self): try: import muspan as ms - o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--keep', 'cell_type', 'oxygen'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_z0.muspan') - assert o_result.returncode == 0 + if ms.__file__ is None: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + else: + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--keep', 'cell_type', 'oxygen'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/output00000024_z0.muspan') + assert o_result.returncode == 0 except ModuleNotFoundError: print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') assert True diff --git a/test/test_dependency.py b/test/test_dependency.py new file mode 100644 index 0000000..4f24d3d --- /dev/null +++ b/test/test_dependency.py @@ -0,0 +1,237 @@ +##### +# title: test_dependency.py +# +# language: python3 +# author: Elmar Bucher, Heber Rocha, Claude Anthropic +# date: 2026-08-14 +# license: BSD 3-Clause +# +# description: +# pytest unit test library that checks that pcdl installs and runs light +# weight, and that functions requiring an optional, heavyweight dependency +# raise an inforamative error message. +# + https://docs.pytest.org/ +##### + + +# load library +import os +import pathlib +import pytest +import subprocess +import sys +import pcdl +from pcdl.dependency import optional_import + + +# const +s_path_2d = str(pathlib.Path(pcdl.__file__).parent.resolve()/'output_2d') +s_file_2d = 'output00000024.xml' + + +## download test dataset ## +if not os.path.exists(s_path_2d): + pcdl.install_data() + + +# all heavyweight module names +ls_module = [ + 'anndata', + 'bioio', + 'bioio.writers', + 'bioio_base', + 'geopandas', + 'neuroglancer', + 'neuroglancer.cli', + 'requests', + 'skimage.exposure', + 'skimage.util', + 'shapely', + 'spatialdata', + 'vtk', +] + +## pre-warm the import cache for all optional dependencies (if installed), +## so that the missing-dependency tests below, which poison one module at +## a time via sys.modules, only ever fail on the single library they +## specifically poison, not on some sibling library that library depends +## on internally (e.g. spatialdata depends on shapely and geopandas). +for s_module in ls_module: + try: + optional_import(s_module) + except ModuleNotFoundError: + pass + + +######################################### +# pcdl.dependency.optional_import tests # +######################################### + +class TestOptionalImport(object): + ''' tests for the pcdl.dependency.optional_import lazy import function. ''' + + def test_optional_import_module_present(self): + o_module = optional_import('os') + assert o_module is os + + def test_optional_import_attr_present(self): + f_join = optional_import('os.path', s_attr='join') + assert f_join is os.path.join + + def test_optional_import_module_missing(self): + with pytest.raises(ModuleNotFoundError) as o_error: + optional_import('pcdl_not_a_real_dependency_abc') + s_message = str(o_error.value) + assert 'pcdl_not_a_real_dependency_abc' in s_message + assert 'pip install pcdl[full]' in s_message + assert 'pip install pcdl_not_a_real_dependency_abc' in s_message + + def test_optional_import_module_missing_custom_pip_name(self): + with pytest.raises(ModuleNotFoundError) as o_error: + optional_import('skimage_not_a_real_dependency', s_pip='scikit-image') + s_message = str(o_error.value) + assert 'scikit-image' in s_message + assert 'pip install scikit-image' in s_message + + def test_optional_import_module_missing_caller_named(self): + with pytest.raises(ModuleNotFoundError) as o_error: + optional_import('pcdl_not_a_real_dependency_abc', s_caller='TimeStep.get_anndata') + assert 'TimeStep.get_anndata' in str(o_error.value) + + +############################################### +# light weight import and core function tests # +############################################### + +class TestLightWeightImport(object): + ''' + tests, run in a subprocess with a clean sys.modules, that pcdl can be + imported, and that basic, core data loading functions work, even if + none of the optional, heavyweight libraries are installed. + ''' + + def test_pcdl_import_and_core_functions_without_optional_libraries(self): + s_script = f""" +import sys +for s_module in {ls_module!r}: + sys.modules[s_module] = None +import pcdl +mcds = pcdl.TimeStep( + xmlfile={s_file_2d!r}, + output_path={s_path_2d!r}, + verbose=False, +) +df_cell = mcds.get_cell_df() +df_conc = mcds.get_conc_df() +assert df_cell.shape[0] > 0 +assert df_conc.shape[0] > 0 +ax = mcds.plot_scatter() +ax = mcds.plot_contour('oxygen') +print('LIGHT_IMPORT_OK') +""" + o_result = subprocess.run([sys.executable, '-c', s_script], capture_output=True, text=True) + assert 'LIGHT_IMPORT_OK' in o_result.stdout, o_result.stderr + + +####################################################### +# optional dependency function error message tests # +####################################################### + +class TestOptionalDependencyErrors(object): + ''' tests that heavyweight-dependency functions fail informatively if the dependency is missing. ''' + + mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, verbose=False) + + def test_make_conc_vtk_missing_vtk(self, monkeypatch, mcds=mcds): + monkeypatch.setitem(sys.modules, 'vtk', None) + with pytest.raises(ModuleNotFoundError) as o_error: + mcds.make_conc_vtk() + s_message = str(o_error.value) + assert 'vtk' in s_message + assert 'pcdl[full]' in s_message + + def test_make_cell_vtk_missing_vtk(self, monkeypatch, mcds=mcds): + monkeypatch.setitem(sys.modules, 'vtk', None) + with pytest.raises(ModuleNotFoundError) as o_error: + mcds.make_cell_vtk() + assert 'vtk' in str(o_error.value) + + def test_get_anndata_missing_anndata(self, monkeypatch, mcds=mcds): + monkeypatch.setitem(sys.modules, 'anndata', None) + with pytest.raises(ModuleNotFoundError) as o_error: + mcds.get_anndata() + s_message = str(o_error.value) + assert 'anndata' in s_message + assert 'pcdl[full]' in s_message + + def test_get_spatialdata_missing_anndata(self, monkeypatch, mcds=mcds): + monkeypatch.setitem(sys.modules, 'anndata', None) + with pytest.raises(ModuleNotFoundError) as o_error: + mcds.get_spatialdata(images=set(), points={'cell'}, shapes=set()) + assert 'anndata' in str(o_error.value) + + def test_get_spatialdata_missing_spatialdata(self, monkeypatch, mcds=mcds): + monkeypatch.setitem(sys.modules, 'spatialdata', None) + with pytest.raises(ModuleNotFoundError) as o_error: + mcds.get_spatialdata(images=set(), points={'cell'}, shapes=set()) + assert 'spatialdata' in str(o_error.value) + + def test_get_spatialdata_missing_shapely(self, monkeypatch, mcds=mcds): + monkeypatch.setitem(sys.modules, 'shapely', None) + with pytest.raises(ModuleNotFoundError) as o_error: + mcds.get_spatialdata(images=set(), points=set(), shapes={'cell'}) + assert 'shapely' in str(o_error.value) + + def test_get_spatialdata_missing_geopandas(self, monkeypatch, mcds=mcds): + monkeypatch.setitem(sys.modules, 'geopandas', None) + with pytest.raises(ModuleNotFoundError) as o_error: + mcds.get_spatialdata(images=set(), points=set(), shapes={'cell'}) + assert 'geopandas' in str(o_error.value) + + def test_get_spatialdata_does_not_need_bioio(self, monkeypatch, mcds=mcds): + # get_spatialdata renders its own images (file=False), so it must + # not require the bioio ome tiff writer stack. + monkeypatch.setitem(sys.modules, 'bioio', None) + monkeypatch.setitem(sys.modules, 'bioio.writers', None) + monkeypatch.setitem(sys.modules, 'bioio_base', None) + sdata = mcds.get_spatialdata(images={'subs'}, points={'subs'}, shapes={'cell'}) + assert str(type(sdata)).startswith(" 0 + + def test_render_neuroglancer_missing_neuroglancer(self, monkeypatch, mcds=mcds): + monkeypatch.delitem(sys.modules, 'pcdl.neuromancer', raising=False) + monkeypatch.setitem(sys.modules, 'neuroglancer', None) + with pytest.raises(ModuleNotFoundError) as o_error: + pcdl.render_neuroglancer('not_a_real_file.ome.tiff') + s_message = str(o_error.value) + assert 'neuroglancer' in s_message + assert 'pcdl[full]' in s_message + + def test_render_neuroglancer_missing_bioio(self, monkeypatch, mcds=mcds): + monkeypatch.delitem(sys.modules, 'pcdl.neuromancer', raising=False) + monkeypatch.setitem(sys.modules, 'bioio', None) + with pytest.raises(ModuleNotFoundError) as o_error: + pcdl.render_neuroglancer('not_a_real_file.ome.tiff') + assert 'bioio' in str(o_error.value) + + def test_install_data_missing_requests(self, monkeypatch): + monkeypatch.setitem(sys.modules, 'requests', None) + with pytest.raises(ModuleNotFoundError) as o_error: + pcdl.install_data() + s_message = str(o_error.value) + assert 'requests' in s_message + assert 'pcdl[full]' in s_message diff --git a/test/test_timeseries_2d.py b/test/test_timeseries_2d.py index 25e6c83..c32a90f 100644 --- a/test/test_timeseries_2d.py +++ b/test/test_timeseries_2d.py @@ -1019,6 +1019,9 @@ def test_mcdsts_get_muspan_default(self): except ModuleNotFoundError: print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') assert True + except SystemExit: + print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') + assert True def test_mcdsts_get_muspan_zslice(self): try: @@ -1036,6 +1039,9 @@ def test_mcdsts_get_muspan_zslice(self): except ModuleNotFoundError: print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') assert True + except SystemExit: + print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') + assert True ## spatialdata time seris related functions ## diff --git a/test/test_timeseries_3d.py b/test/test_timeseries_3d.py index 2d68e8a..2fb9b13 100644 --- a/test/test_timeseries_3d.py +++ b/test/test_timeseries_3d.py @@ -882,6 +882,9 @@ def test_mcdsts_get_muspan_default(self): except ModuleNotFoundError: print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') assert True + except SystemExit: + print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') + assert True def test_mcdsts_get_muspan_zslice(self): try: @@ -899,6 +902,9 @@ def test_mcdsts_get_muspan_zslice(self): except ModuleNotFoundError: print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') assert True + except SystemExit: + print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') + assert True ## spatialdata time seris related functions ## diff --git a/test/test_timestep_2d.py b/test/test_timestep_2d.py index a0e595b..a6507e7 100644 --- a/test/test_timestep_2d.py +++ b/test/test_timestep_2d.py @@ -1124,6 +1124,9 @@ def test_mcds_get_muspan_default(self): except ModuleNotFoundError: print('Warning @ pytest TestTimeStepMuspan : muspan module not installed.') assert True + except SystemExit: + print('Warning @ pytest TestTimeStepMuspan : muspan module not installed.') + assert True def test_mcds_get_muspan_zslice(self): try: @@ -1141,6 +1144,9 @@ def test_mcds_get_muspan_zslice(self): except ModuleNotFoundError: print('Warning @ pytest TestTimeStepMuspan : muspan module not installed.') assert True + except SystemExit: + print('Warning @ pytest TestTimeStepMuspan : muspan module not installed.') + assert True ## spatialdata time step related functions ## diff --git a/test/test_timestep_3d.py b/test/test_timestep_3d.py index a5edb28..cebf1fc 100644 --- a/test/test_timestep_3d.py +++ b/test/test_timestep_3d.py @@ -697,6 +697,9 @@ def test_mcds_get_muspan_default(self): except ModuleNotFoundError: print('Warning @ pytest TestTimeStepMuspan : muspan module not installed.') assert True + except SystemExit: + print('Warning @ pytest TestTimeStepMuspan : muspan module not installed.') + assert True def test_mcds_get_muspan_zslice(self): try: @@ -714,6 +717,9 @@ def test_mcds_get_muspan_zslice(self): except ModuleNotFoundError: print('Warning @ pytest TestTimeStepMuspan : muspan module not installed.') assert True + except SystemExit: + print('Warning @ pytest TestTimeStepMuspan : muspan module not installed.') + assert True ## spatialdata time step related functions ## From e7343af1ed9903db77d41782b132ddeb77a7e802 Mon Sep 17 00:00:00 2001 From: bue Date: Thu, 20 Aug 2026 12:47:18 -0400 Subject: [PATCH 2/2] @ physicell data loader : next release v4.1.6. --- README.md | 2 +- pcdl/VERSION.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a75a231..96f5afd 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Developers, please make pull requests to the https://github.com/elmbeech/physice ## Release Notes: -+ version 4.1.6 (2026-08-22): elmbeech/physicelldataloader ++ version 4.1.6 (2026-08-20): elmbeech/physicelldataloader + pcdl installs now by default lightweight with only the core dependencies; users are prompted to install optional libraries if necessary. special thanks to Heber Rocha! + version 4.1.5 (2026-04-05): elmbeech/physicelldataloader diff --git a/pcdl/VERSION.py b/pcdl/VERSION.py index 8fcba80..d88f6e6 100644 --- a/pcdl/VERSION.py +++ b/pcdl/VERSION.py @@ -1 +1 @@ -__version__ = '4.1.5' +__version__ = '4.1.6'