Contributing to SHINIER๏ƒ

Thanks for your interest in improving SHINIER. This document explains how to set up a development environment, coding standards, testing strategy (unit vs. validation), and the pull-request process. Please read it fully before opening a PR.


Table of Contents๏ƒ

  1. Code of Conduct

  2. Development Setup

  3. Branching & Commits

  4. Coding Standards

  5. Testing

  6. Performance & Memory Guidelines

  7. Documentation

  8. PR Checklist

  9. Issue Triage & Feature Proposals

  10. Release Hygiene (maintainers)


Code of Conduct๏ƒ

By participating, you agree to uphold a standard of professional, inclusive, and respectful collaboration. If your organization already uses a Code of Conduct, link it here (e.g., Contributor Covenant). Reports can be sent to the maintainers.


Development Setup๏ƒ

Python: >=3.9, <3.13
OS: macOS / Linux / Windows
Optional: C/C++ toolchain for the Cython-compiled _cconvolve extension (speeds up convolution)

# 1) Fork on GitHub, then clone your fork
git clone https://github.com/Charestlab/shinier.git
cd shinier

# 2) Create a virtual environment (example: venv)
python -m venv .venv
source .venv/bin/activate            # Windows: .venv\Scripts\activate

# 3) Editable install with dev tools
pip install --upgrade pip setuptools wheel
pip install -e ".[dev]"              # includes pytest and external packages for validation

# 4) Optional: ensure a compiler is available (for fast Cython extension)
# macOS:   xcode-select --install
# Ubuntu:  sudo apt update && sudo apt install -y build-essential python3-dev
# Windows: Install "Build Tools for Visual Studio" (C++)

# 5) Pre-commit hooks (formatting/linting on commit)
pre-commit install

# 6) Quick smoke test
pytest -m unit_tests

Coding Standards๏ƒ

Git rules๏ƒ

  • No one has push permissions on main.

  • PR requires 2 approvals + all tests to pass before merging.

  • The official working development branch is dev_X, where X is the subversion number of the next planned PyPI release (e.g., if the current release is 0.1.7, use dev_1.8 for the next development branch).

To do before a PR๏ƒ

  • Merge main on dev_X branch (not the other way around).

  • Make sure all status checks are green.

  • Initiate PR

Language & typing๏ƒ

  • Use type hints everywhere (functions, methods, Pydantic models, tests).

  • Prefer explicit array types (e.g., np.ndarray) and document expected shape and dtype.

  • Keep functions small and pure when possible; avoid implicit mutation unless documented.

Docstrings๏ƒ

  • Use NumPy-style docstrings for all public functions/classes/methods.

  • Keep explicit type information in the docstring descriptions. SHINIER also uses type hints in signatures, but repeated docstring types make the generated Read the Docs API pages clearer and more exhaustive.

  • Preserve scientific details, equations, valid ranges, image shapes, dtype expectations, warnings, and citations when reformatting existing docstrings.

  • For classes, put user-provided constructor inputs in a Parameters section and runtime/public state in an Attributes section.

import numpy as np

def lum_match(img: np.ndarray, target: float) -> np.ndarray:
    """Match the mean luminance of an image to a target.

    Parameters
    ----------
    img : np.ndarray
        Image array in float space, range [0, 1], shape (H, W) or (H, W, 3).
    target : float
        Target mean luminance in [0, 1].

    Returns
    -------
    np.ndarray
        A new image array with adjusted mean luminance (same shape as input).

    Raises
    ------
    TypeError
        If ``img`` is not a NumPy array.
    ValueError
        If ``target`` is outside [0, 1] or ``img`` range is invalid.
    """
    if not isinstance(img, np.ndarray):
        raise TypeError("img must be a numpy.ndarray.")
    if not (0.0 <= float(target) <= 1.0):
        raise ValueError("target must be in [0, 1].")
    if img.size == 0:
        return img.copy()

    current = float(np.clip(img, 0.0, 1.0).mean())
    if current == 0.0:
        # Avoid division by zero; return image filled with target
        return np.full_like(img, target, dtype=img.dtype)

    scale = target / current
    out = np.clip(img.astype(np.float64) * scale, 0.0, 1.0)
    return out.astype(img.dtype, copy=False)

Read the Docs๏ƒ

SHINIER documentation is built with Sphinx and published with Read the Docs. The Sphinx project lives in documentation/readthedocs/.

The documentation combines:

  • narrative Markdown files from documentation/;

  • the project README.md;

  • API pages generated from docstrings with sphinx.ext.autodoc and sphinx.ext.napoleon.

Install the development environment before building the documentation:

pip install -e ".[dev]"

Then build the documentation locally:

python -m sphinx -b html documentation/readthedocs documentation/readthedocs/_build/html

Pydantic models๏ƒ

  • Core classes should be Pydantic v2 models inheriting our customized base-model: InformativeBaseModel

  • Note that InformativeBaseModel uses a post_init method that wraps pydanticโ€™s model_post_init

  • Use Field for defaults/descriptions; prefer strict types when coercion is dangerous.

  • Validate with @field_validator / @model_validator and raise specific exceptions.

  • Avoid hidden coercions (e.g., 2 -> True). Use StrictBool for boolean flags.

  • Keep .model_dump() and .model_json_schema() coherent and documented.

from typing import Literal, Optional, Any
import numpy as np
from shinier.base import InformativeBaseModel
from shinier import REPO_ROOT
from pydantic import Field, ConfigDict, StrictBool, field_validator, model_validator
from pathlib import Path


class Options(InformativeBaseModel):
    """..."""

    model_config = ConfigDict(
        validate_assignment=True,  # Validate every time object updated
        extra="forbid",  # Does not allow unknown attributes
        arbitrary_types_allowed=True,  # Allow non-pydantic types (e.g. np.ndarray)
    )

    # --- I/O ---
    input_folder: Optional[Path] = Field(default=REPO_ROOT / "INPUT")
    output_folder: Path = Field(default=REPO_ROOT / "OUTPUT")

    # --- Masks ---
    masks_folder: Optional[Path] = Field(default=None)

    """..."""

    @field_validator("input_folder", "output_folder", "masks_folder")
    @classmethod
    def validate_existing_path(cls, v: Optional[Path]) -> Optional[Path]:
        if v is not None:
            v = v.resolve()
            if not v.exists():
                raise ValueError(f"Folder does not exist: {v}")
        return v

    """..."""
    def post_init(self, __context: Any) -> None:
        """Put your initialization logic here. It will run after Pydantic validation and only once at instantiation."""
        

Exceptions๏ƒ

  • Never use bare except: or except Exception:. Catch specific exceptions.

  • Error messages must be actionable (what failed + how to fix).

import numpy as np

def safe_std(a: np.ndarray) -> float:
    """Return standard deviation with clear failure modes."""
    if not isinstance(a, np.ndarray):
        raise TypeError("a must be a numpy.ndarray.")
    try:
        return float(np.std(a, dtype=np.float64))
    except (FloatingPointError, ValueError) as err:
        raise ValueError(f"std failed: {err}") from err

Unit-Tests and Validation Tests๏ƒ

  • Run locally and keep CI green.

  • Recommended tools: pytest.

# Tests (fast) and markers
pytest -m unit_tests

# Validation Tests (VERY LONG AND SLOW) and markers
pytest -q -m validation_tests

๐Ÿ›๏ธ Test README๏ƒ

For complete testing procedures (markers, sharding, replay/debug workflow), see the dedicated Testing Guide in tests/README.md.