diff --git a/.gitignore b/.gitignore index 2ac512d..8d37e36 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ dist/ .pytest_cache/ .mypy_cache/ # Jupyter Notebook checkpoints -.ipynb_checkpoints/ \ No newline at end of file +.ipynb_checkpoints/ +# Test coverage artifacts +.coverage +htmlcov/ diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index dae7963..0000000 --- a/setup.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[flake8] -max-line-length = 100 -extend-ignore = E203, W503 - -[isort] -profile = black -line_length = 100 -known_first_party = simicpipeline - -[tool:pytest] -testpaths = tests -addopts = -q - -[mypy] -python_version = 3.9 -ignore_missing_imports = True -strict_optional = True -warn_unused_ignores = True diff --git a/src/simicpipeline/__init__.py b/src/simicpipeline/__init__.py index 65c8dd7..989885e 100644 --- a/src/simicpipeline/__init__.py +++ b/src/simicpipeline/__init__.py @@ -18,7 +18,7 @@ try: from simicpipeline.core.aucprocessor import AUCProcessor - __all__.append("AUProcessor") + __all__.append("AUCProcessor") except ImportError as e: import warnings warnings.warn(f"Could not import AUCprocessor: {e}") diff --git a/src/simicpipeline/_jupyter.py b/src/simicpipeline/_jupyter.py index fb75025..df318c2 100644 --- a/src/simicpipeline/_jupyter.py +++ b/src/simicpipeline/_jupyter.py @@ -1,5 +1,4 @@ # src/simicpipeline/_jupyter.py -import os import subprocess def jupyter(): diff --git a/src/simicpipeline/core/__init__.py b/src/simicpipeline/core/__init__.py index e0a1034..00925ac 100644 --- a/src/simicpipeline/core/__init__.py +++ b/src/simicpipeline/core/__init__.py @@ -2,12 +2,7 @@ Core functionality for SimiC Pipeline """ -# This module is part of simicpipeline.core -# All imports are handled at the package level (simicpipeline.__init__.py) -# Individual modules can be imported as: from simicpipeline.core.base import SimiCBase + __all__ = ["SimiCBase"] -# from simicpipeline.core.simicvisualization import SimiCVisualization -# from simicpipeline.core.simicpreprocess import MagicPipeline, ExperimentSetup -# from simicpipeline.core.aucprocessor import AUCprocessor diff --git a/src/simicpipeline/core/aucprocessor.py b/src/simicpipeline/core/aucprocessor.py index 869e84c..c35a46e 100644 --- a/src/simicpipeline/core/aucprocessor.py +++ b/src/simicpipeline/core/aucprocessor.py @@ -14,32 +14,6 @@ from simicpipeline.core import SimiCBase from typing import Optional, Union -# class BaseProcessor(SimiCBase): -# """ -# Base class for SimiC processors. -# Provides common functionality for file handling and data loading. -# """ - - -# def __init__(self, p2df, p2res): -# """ -# Initialize the base processor. - -# Args: -# p2df (str): Path to dataframe pickle file -# p2res (str): Path to results pickle file -# """ -# self.p2df = Path(p2df) -# self.p2res = Path(p2res) -# self.validate_files() - -# def validate_files(self): -# """Validate that required files exist.""" -# if not self.p2df.exists(): -# raise FileNotFoundError(f"Data file not found: {self.p2df}") -# if not self.p2res.exists(): -# raise FileNotFoundError(f"Results file not found: {self.p2res}") - class AUCProcessor(SimiCBase): """ diff --git a/src/simicpipeline/core/simicpreprocess.py b/src/simicpipeline/core/simicpreprocess.py index c6150ab..07e1d99 100644 --- a/src/simicpipeline/core/simicpreprocess.py +++ b/src/simicpipeline/core/simicpreprocess.py @@ -18,31 +18,6 @@ from simicpipeline.core import SimiCBase -# class SimiCPreprocess(SimiCBase): -# """ -# Base preprocessing pipeline for SimiC analysis. -# Provides common utilities for both MAGIC imputation and experiment setup. -# """ - -# def __init__(self, -# project_dir: Union[str, Path], -# ): -# """ -# Initialize base preprocessing pipeline. - -# Args: -# project_dir: Directory for project files -# """ -# super().__init__(project_dir = project_dir) - -# def _create_directory_structure(self) -> None: -# """Create standard SimiC directory structure.""" - -# self.input_files_dir = self.input_path -# self.output_simic_dir = self.output_path -# self.figures_dir = self.figures_path -# self.matrices_dir = self.matrices_path - class MagicPipeline(SimiCBase): """ diff --git a/src/simicpipeline/data/__init__.py b/src/simicpipeline/data/__init__.py index e69de29..754fc4a 100644 --- a/src/simicpipeline/data/__init__.py +++ b/src/simicpipeline/data/__init__.py @@ -0,0 +1,3 @@ +"""Data files bundled with SimiCPipeline.""" + +__all__: list[str] = [] diff --git a/src/simicpipeline/utils/__init__.py b/src/simicpipeline/utils/__init__.py index 82ef492..f560c86 100644 --- a/src/simicpipeline/utils/__init__.py +++ b/src/simicpipeline/utils/__init__.py @@ -4,16 +4,14 @@ load_from_matrix_market, load_from_anndata, write_pickle, - install_package, format_time, - print_tree + print_tree, ) __all__ = [ "load_from_matrix_market", "load_from_anndata", "write_pickle", - "install_package", "format_time", - "print_tree" + "print_tree", ] \ No newline at end of file diff --git a/src/simicpipeline/utils/io.py b/src/simicpipeline/utils/io.py index fd37f98..a2b3c2d 100644 --- a/src/simicpipeline/utils/io.py +++ b/src/simicpipeline/utils/io.py @@ -1,19 +1,15 @@ from __future__ import annotations -from typing import Optional, Tuple, Union +from typing import Optional, Union from pathlib import Path import pickle import pandas as pd -import numpy as np -from scipy.sparse import coo_matrix -# Install packages -def install_package(package_name): - """Install a package using pip3.""" - import subprocess - import sys - subprocess.check_call([sys.executable, "-m", "pip", "install", package_name]) -# Directories +def _install_package(package_name: str) -> None: + """Install a package using pip (internal utility, not part of public API).""" + import subprocess + import sys + subprocess.check_call([sys.executable, "-m", "pip", "install", package_name]) def print_tree(directory: Union[str,Path] = Path('.'), @@ -40,7 +36,7 @@ def print_tree(directory: Union[str,Path] = Path('.'), │ └── utils/ │ └── io.py """ - # Print the root directory name at the top + # Print the root directory name at the top directory = Path(directory) if current_depth == 0: print(f"{directory.name}/") @@ -62,7 +58,7 @@ def print_tree(directory: Union[str,Path] = Path('.'), extension = " " if is_last else "│ " print_tree(item, prefix + extension, max_depth, current_depth + 1) -# Loaders + def load_from_anndata( path: Union[str, Path], ) -> object: @@ -83,15 +79,15 @@ def load_from_anndata( path = Path(path) adata = ad.read_h5ad(str(path)) - if adata.raw == None: + if adata.raw is None: print("No raw attribute found in AnnData object.") return adata def load_from_matrix_market( matrix_path: Union[str, Path], - genes_path: Union[str, Path] = None, - cells_path: Union[str, Path] = None, + genes_path: Optional[Union[str, Path]] = None, + cells_path: Optional[Union[str, Path]] = None, transpose: bool = False, cells_index_name: str = "Cell", ) -> object: @@ -127,9 +123,9 @@ def load_from_matrix_market( # Load names genes = None cells = None - if genes_path and genes_path.exists(): + if genes_path is not None and Path(genes_path).exists(): genes = pd.read_csv(genes_path, header=None, sep="\t").iloc[:, 0].astype(str).tolist() - if cells_path and cells_path.exists(): + if cells_path is not None and Path(cells_path).exists(): cells = pd.read_csv(cells_path, header=None, sep="\t").iloc[:, 0].astype(str).tolist() # Convert to dense for simplicity; adjust if large matrices are expected @@ -150,7 +146,7 @@ def load_from_matrix_market( return df -# Writers + def write_pickle(obj, file_path): file_path = Path(file_path) file_path.parent.mkdir(parents=True, exist_ok=True) @@ -164,7 +160,6 @@ def write_pickle(obj, file_path): print(f"Pickle failed with error: {e}") -@staticmethod def format_time(seconds: float) -> str: """ Format time duration in human-readable format. @@ -185,5 +180,3 @@ def format_time(seconds: float) -> str: return f"{minutes}min {secs}s" else: return f"{secs}s" - -########################################################################################### diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_core_base.py b/tests/test_core_base.py new file mode 100644 index 0000000..670c5f9 --- /dev/null +++ b/tests/test_core_base.py @@ -0,0 +1,48 @@ +"""Smoke tests for core base class.""" + +from pathlib import Path + +import pytest + +from simicpipeline.core.base import SimiCBase, _load_assignment_file + + +class TestSimiCBase: + """Smoke tests for SimiCBase initialization and helpers.""" + + def test_creates_project_dir(self, tmp_path): + project_dir = tmp_path / "my_project" + assert not project_dir.exists() + base = SimiCBase(project_dir) + assert project_dir.exists() + assert base.project_dir == project_dir + + def test_existing_project_dir(self, tmp_path): + base = SimiCBase(tmp_path) + assert base.project_dir == tmp_path + + def test_format_time_delegates(self, tmp_path): + base = SimiCBase(tmp_path) + assert base.format_time(60) == "1min 0s" + + def test_print_project_info(self, tmp_path, capsys): + base = SimiCBase(tmp_path) + base.print_project_info(max_depth=1) + captured = capsys.readouterr() + assert tmp_path.name in captured.out + + +class TestLoadAssignmentFile: + """Tests for _load_assignment_file helper.""" + + def test_new_csv_format(self, tmp_path): + csv_file = tmp_path / "assignment.csv" + csv_file.write_text("category,label\ncell1,0\ncell2,1\ncell3,0\n") + labels = _load_assignment_file(csv_file) + assert list(labels) == [0, 1, 0] + + def test_legacy_format(self, tmp_path): + tsv_file = tmp_path / "assignment.txt" + tsv_file.write_text("0\n1\n1\n0\n") + labels = _load_assignment_file(tsv_file) + assert list(labels) == [0, 1, 1, 0] diff --git a/tests/test_utils_io.py b/tests/test_utils_io.py new file mode 100644 index 0000000..078e0c4 --- /dev/null +++ b/tests/test_utils_io.py @@ -0,0 +1,59 @@ +"""Tests for simicpipeline.utils.io utilities.""" + +import pickle +from pathlib import Path + +import pytest + +from simicpipeline.utils.io import format_time, print_tree, write_pickle + + +class TestFormatTime: + """Tests for the format_time utility function.""" + + def test_seconds_only(self): + assert format_time(45) == "45s" + + def test_minutes_and_seconds(self): + assert format_time(90) == "1min 30s" + + def test_hours_minutes_seconds(self): + assert format_time(3661) == "1h 1min 1s" + + def test_zero(self): + assert format_time(0) == "0s" + + def test_returns_string(self): + assert isinstance(format_time(100), str) + + +class TestWritePickle: + """Tests for the write_pickle utility function.""" + + def test_writes_and_reloads(self, tmp_path): + obj = {"key": [1, 2, 3]} + out = tmp_path / "test.pkl" + write_pickle(obj, out) + assert out.exists() + with open(out, "rb") as f: + loaded = pickle.load(f) + assert loaded == obj + + def test_creates_parent_directories(self, tmp_path): + obj = "hello" + out = tmp_path / "nested" / "dir" / "test.pkl" + write_pickle(obj, out) + assert out.exists() + + +class TestPrintTree: + """Tests for the print_tree utility function.""" + + def test_runs_without_error(self, tmp_path, capsys): + (tmp_path / "a.txt").write_text("x") + sub = tmp_path / "sub" + sub.mkdir() + (sub / "b.txt").write_text("y") + print_tree(tmp_path, max_depth=2) + captured = capsys.readouterr() + assert tmp_path.name in captured.out