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๏
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_cconvolveextension (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 is0.1.7, usedev_1.8for 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
Parameterssection and runtime/public state in anAttributessection.
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.autodocandsphinx.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:
InformativeBaseModelNote that
InformativeBaseModeluses apost_initmethod that wraps pydanticโsmodel_post_initUse
Fieldfor defaults/descriptions; prefer strict types when coercion is dangerous.Validate with
@field_validator/@model_validatorand raise specific exceptions.Avoid hidden coercions (e.g.,
2 -> True). UseStrictBoolfor 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:orexcept 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.