From 0751efabddfe5fa9683f0490b4e69a9f4d24fa2c Mon Sep 17 00:00:00 2001 From: "Caihao (Chris) Cui" Date: Sun, 24 May 2026 10:40:53 +1000 Subject: [PATCH 1/4] Fix tiling edge cases --- src/splitraster/geo.py | 18 ++-- src/splitraster/io.py | 26 +++++- tests/test_splitraster.py | 182 +++++++++++++++++++------------------- 3 files changed, 126 insertions(+), 100 deletions(-) diff --git a/src/splitraster/geo.py b/src/splitraster/geo.py index 9044d50..38a2057 100644 --- a/src/splitraster/geo.py +++ b/src/splitraster/geo.py @@ -4,6 +4,8 @@ import numpy as np +from splitraster.io import validate_tiling_params + try: from osgeo import gdal, gdal_array except ImportError: @@ -16,6 +18,8 @@ def read_rasterArray(image_path: str) -> Tuple[np.ndarray, Tuple[float, ...], str]: dataset = gdal.Open(image_path, gdal.GA_ReadOnly) + if dataset is None: + raise FileNotFoundError(f"Can not open raster file: {image_path}") image = dataset.ReadAsArray() # get the rasterArray # convert 2D raster to [1, H, W] format if len(image.shape) == 2: @@ -98,7 +102,7 @@ def split_image( print(f"Input Image File Shape (D, H, W):{img.shape}") - stride = int(crop_size * (1 - repetition_rate)) + stride = validate_tiling_params(crop_size, repetition_rate) print(f"crop_size = {crop_size}, stride = {stride}") padded_img = padding_mul_image(img, stride) @@ -164,12 +168,12 @@ def random_crop_image( label_ext (str): extension for label files overwrite (bool): overwrite existing files """ - img, geotrans, proj = read_rasterArray(label_path) + img, img_geotrans, img_proj = read_rasterArray(img_path) if img is None: print("Input image is missing") return None - label, geotrans, proj = read_rasterArray(img_path) + label, label_geotrans, label_proj = read_rasterArray(label_path) if label is None: print("Label image is missing") return None @@ -188,8 +192,8 @@ def random_crop_image( if overwrite: new_name = 1 else: - img_cnt = count_files(img_path) - label_cnt = count_files(label_path) + img_cnt = count_files(img_save_path) + label_cnt = count_files(label_save_path) new_name = img_cnt + 1 print(f"There are {img_cnt} files in the {img_save_path}") print(f"There are {label_cnt} files in the {label_save_path}") @@ -222,11 +226,11 @@ def random_crop_image( # save image pairs crop_image_name = f"{new_name:04d}{img_ext}" crop_image_path = Path(img_save_path) / crop_image_name - save_rasterGeoTIF(imgCrop, geotrans, proj, str(crop_image_path)) + save_rasterGeoTIF(imgCrop, img_geotrans, img_proj, str(crop_image_path)) crop_image_name = f"{new_name:04d}{label_ext}" crop_image_path = Path(label_save_path) / crop_image_name - save_rasterGeoTIF(labelCrop, geotrans, proj, str(crop_image_path)) + save_rasterGeoTIF(labelCrop, label_geotrans, label_proj, str(crop_image_path)) new_name += 1 # update image name crop_cnt += 1 # add crop count diff --git a/src/splitraster/io.py b/src/splitraster/io.py index 70dd319..baf1778 100644 --- a/src/splitraster/io.py +++ b/src/splitraster/io.py @@ -53,6 +53,26 @@ def count_files(folder_path): return count +def validate_tiling_params(crop_size, repetition_rate) -> int: + """ + Validate crop and overlap settings, then return the stride. + Args: + crop_size: crop size + repetition_rate: repetition rate + Returns: + stride + """ + if crop_size <= 0: + raise ValueError("crop_size must be greater than 0") + if not 0 <= repetition_rate < 1: + raise ValueError("repetition_rate must be greater than or equal to 0 and less than 1") + + stride = int(crop_size * (1 - repetition_rate)) + if stride <= 0: + raise ValueError("crop_size and repetition_rate must produce a positive stride") + return stride + + def padding_image(img, stride) -> np.ndarray: """ Padding image to the size of multiple of stride @@ -104,7 +124,7 @@ def split_image(img_path, save_path, crop_size, repetition_rate=0, overwrite=Tru print(f"Input Image File Shape (H, W, D):{img.shape}") - stride = int(crop_size * (1 - repetition_rate)) + stride = validate_tiling_params(crop_size, repetition_rate) print(f"crop_size = {crop_size}, stride = {stride}") padded_img = padding_image(img, stride) @@ -195,8 +215,8 @@ def random_crop_image( if overwrite: new_name = 1 else: - img_cnt = count_files(img_path) - label_cnt = count_files(label_path) + img_cnt = count_files(img_save_path) + label_cnt = count_files(label_save_path) new_name = img_cnt + 1 print(f"There are {img_cnt} files in the {img_save_path}") print(f"There are {label_cnt} files in the {label_save_path}") diff --git a/tests/test_splitraster.py b/tests/test_splitraster.py index ba30eda..5c66fa5 100644 --- a/tests/test_splitraster.py +++ b/tests/test_splitraster.py @@ -1,57 +1,54 @@ -# Test the Packages -import os +import importlib +import sys +import types +from pathlib import Path -base_dir = os.path.dirname(os.path.abspath(__file__)) +import numpy as np +import pytest +BASE_DIR = Path(__file__).resolve().parent -# Example A: -def test_rgb_gt_slide_window() -> None: - from splitraster import io - - # Step 1: set input image file path - input_image_path = os.path.join(base_dir, "data/raw/RGB.png") - gt_image_path = os.path.join(base_dir, "data/raw/GT.png") - # Step 2: prepare output directory and splitting configuration - input_save_path = os.path.join(base_dir, "data/processed/RGB") - gt_save_path = os.path.join(base_dir, "data/processed/GT") +def test_rgb_gt_slide_window(tmp_path) -> None: + from splitraster import io - crop_size = 256 - repetition_rate = 0 - overwrite = False + input_image_path = BASE_DIR / "data/raw/RGB.png" + gt_image_path = BASE_DIR / "data/raw/GT.png" + input_save_path = tmp_path / "RGB" + gt_save_path = tmp_path / "GT" - # step 3: split the RGB images n = io.split_image( input_image_path, input_save_path, - crop_size, - repetition_rate=repetition_rate, - overwrite=overwrite, + crop_size=256, + repetition_rate=0, + overwrite=False, ) - print(f"{n} tiles sample of {input_image_path} are added at {input_save_path}") + assert n == 16 + assert len(list(input_save_path.iterdir())) == 16 - # step 4: split the GT images n = io.split_image( gt_image_path, gt_save_path, - crop_size, - repetition_rate=repetition_rate, - overwrite=overwrite, + crop_size=256, + repetition_rate=0, + overwrite=False, ) - print(f"{n} tiles sample of {gt_image_path} are added at {gt_save_path}") - - # Step 5: Use the RGB and GT folders for your deep learning model. + assert n == 16 + assert len(list(gt_save_path.iterdir())) == 16 -# Example B -def test_rgb_gt_random_crop(): +def test_rgb_gt_random_crop_uses_output_folder_count(tmp_path): from splitraster import io - input_image_path = os.path.join(base_dir, "data/raw/RGB.png") - gt_image_path = os.path.join(base_dir, "data/raw/GT.png") - - save_path = os.path.join(base_dir, "data/processed/Rand/RGB") - save_path_gt = os.path.join(base_dir, "data/processed/Rand/GT") + input_image_path = BASE_DIR / "data/raw/RGB.png" + gt_image_path = BASE_DIR / "data/raw/GT.png" + save_path = tmp_path / "Rand/RGB" + save_path_gt = tmp_path / "Rand/GT" + save_path.mkdir(parents=True) + save_path_gt.mkdir(parents=True) + (save_path / "0001.png").touch() + (save_path_gt / "0001.png").touch() n = io.random_crop_image( input_image_path, @@ -59,79 +56,84 @@ def test_rgb_gt_random_crop(): gt_image_path, save_path_gt, crop_size=256, - crop_number=20, + crop_number=1, img_ext=".png", label_ext=".png", - overwrite=True, + overwrite=False, ) - print( - f"{n} sample paris of {input_image_path, gt_image_path} " - f"are added at {save_path, save_path_gt}" - ) + assert n == 1 + assert (save_path / "0002.png").is_file() + assert (save_path_gt / "0002.png").is_file() -# # Example C -# def test_tif_slide_window(): -# from splitraster import geo +def test_invalid_repetition_rate_raises(tmp_path): + from splitraster import io -# input_tif_image_path = os.path.join(base_dir, "data/raw/TIF/RGB5k.tif") -# gt_tif_image_path = os.path.join(base_dir, "data/raw/TIF/GT5k.tif") + input_image_path = BASE_DIR / "data/raw/RGB.png" -# input_save_image_path = os.path.join(base_dir, "data/processed/RGB_TIF") -# gt_save_image_path = os.path.join(base_dir, "data/processed/GT_TIF") + with pytest.raises(ValueError, match="repetition_rate"): + io.split_image(input_image_path, tmp_path / "RGB", crop_size=256, repetition_rate=1) -# crop_size = 500 -# repetition_rate = 0 -# overwrite = True -# n = geo.split_image( -# input_tif_image_path, -# input_save_image_path, -# crop_size, -# repetition_rate, -# overwrite, -# ) +def import_geo_with_fake_gdal(monkeypatch): + fake_gdal = types.SimpleNamespace( + GA_ReadOnly=0, + GDT_Byte=1, + GDT_UInt16=2, + GDT_Float32=3, + Open=lambda *args, **kwargs: None, + ) + fake_gdal_array = types.SimpleNamespace(SaveArray=lambda *args, **kwargs: True) + fake_osgeo = types.SimpleNamespace(gdal=fake_gdal, gdal_array=fake_gdal_array) -# print( -# f"{n} tiles sample of {input_tif_image_path} " -# f"are added at {input_save_image_path}" -# ) + monkeypatch.setitem(sys.modules, "osgeo", fake_osgeo) + monkeypatch.setitem(sys.modules, "osgeo.gdal", fake_gdal) + monkeypatch.setitem(sys.modules, "osgeo.gdal_array", fake_gdal_array) + sys.modules.pop("splitraster.geo", None) -# n = geo.split_image( -# gt_tif_image_path, gt_save_image_path, crop_size, repetition_rate, overwrite -# ) + return importlib.import_module("splitraster.geo") -# print( -# f"{n} tiles sample of {gt_tif_image_path} " -# f"are added at {gt_save_image_path}" -# ) +def test_geo_read_raster_array_raises_for_missing_file(monkeypatch): + geo = import_geo_with_fake_gdal(monkeypatch) + monkeypatch.setattr(geo.gdal, "Open", lambda *args, **kwargs: None) -# # Example D -# def test_tif_random_sample(): -# from splitraster import geo + with pytest.raises(FileNotFoundError, match="Can not open raster file"): + geo.read_rasterArray("missing.tif") -# input_tif_image_path = os.path.join(base_dir, "data/raw/TIF/RGB5k.tif") -# gt_tif_image_path = os.path.join(base_dir, "data/raw/TIF/GT5k.tif") -# input_save_image_path = os.path.join(base_dir, "data/processed/Rand/RGB_TIF") -# gt_save_image_path = os.path.join(base_dir, "data/processed/Rand/GT_TIF") +def test_geo_random_crop_keeps_image_and_label_order(monkeypatch, tmp_path): + geo = import_geo_with_fake_gdal(monkeypatch) + img = np.ones((1, 2, 2), dtype=np.uint8) + label = np.full((1, 2, 2), 2, dtype=np.uint8) + saved = [] -# n = geo.random_crop_image( -# input_tif_image_path, -# input_save_image_path, -# gt_tif_image_path, -# gt_save_image_path, -# crop_size=500, -# crop_number=20, -# overwrite=True, -# ) + def fake_read(path): + if path == "img.tif": + return img, ("img-geotrans",), "img-proj" + if path == "label.tif": + return label, ("label-geotrans",), "label-proj" + raise AssertionError(f"unexpected path: {path}") -# print( -# f"{n} sample paris of {input_tif_image_path, gt_tif_image_path} " -# f"are added at {input_save_image_path, gt_save_image_path}." -# ) + def fake_save(data, geotrans, proj, file_name): + saved.append((data.copy(), geotrans, proj, Path(file_name).name)) + monkeypatch.setattr(geo, "read_rasterArray", fake_read) + monkeypatch.setattr(geo, "save_rasterGeoTIF", fake_save) + + n = geo.random_crop_image( + "img.tif", + tmp_path / "img", + "label.tif", + tmp_path / "label", + crop_size=2, + crop_number=1, + overwrite=True, + ) -print("PASS") + assert n == 1 + assert saved[0][1:] == (("img-geotrans",), "img-proj", "0001.tif") + assert saved[1][1:] == (("label-geotrans",), "label-proj", "0001.tif") + assert np.array_equal(saved[0][0], img) + assert np.array_equal(saved[1][0], label) From cb827f1a09e5644f03293c5408ab47b4c7bcdf49 Mon Sep 17 00:00:00 2001 From: "Caihao (Chris) Cui" Date: Sun, 24 May 2026 10:57:11 +1000 Subject: [PATCH 2/4] Modernize release workflows --- .github/workflows/docs.yml | 35 ++++++ .github/workflows/python-CD.yml | 210 +++++++++++++++----------------- .github/workflows/python-CI.yml | 98 +++++++++------ src/splitraster/__init__.py | 9 +- tests/test_splitraster.py | 7 ++ 5 files changed, 208 insertions(+), 151 deletions(-) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..39739c9 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,35 @@ +name: Docs + +on: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy: + name: Deploy documentation + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: pip + + - name: Install documentation dependencies + run: | + python -m pip install --upgrade pip + python -m pip install mkdocs + + - name: Deploy to GitHub Pages + run: mkdocs gh-deploy --force diff --git a/.github/workflows/python-CD.yml b/.github/workflows/python-CD.yml index b1e5d31..3aec4c7 100644 --- a/.github/workflows/python-CD.yml +++ b/.github/workflows/python-CD.yml @@ -1,140 +1,128 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: SplitRaster Package Release and Publish +name: Release on: push: - branches: [master] tags: - - v* - pull_request: - branches: [master] + - "v*" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + jobs: - format_and_check: - runs-on: ubuntu-22.04 - environment: ${{ github.ref == 'refs/heads/master' && 'production' || 'staging' }} + build: + name: Build distribution + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.10" - - name: Install dependencies + python-version: "3.13" + cache: pip + + - name: Install build dependencies run: | python -m pip install --upgrade pip - pip install ruff - - name: Lint with ruff - run: | - ruff check . - - name: Format check with ruff + python -m pip install build twine + + - name: Verify tag matches package version run: | - ruff format --check . + python - <<'PY' + import os + import tomllib - build: - needs: format_and_check - runs-on: ubuntu-22.04 - environment: ${{ github.ref == 'refs/heads/master' && 'production' || 'staging' }} + with open("pyproject.toml", "rb") as f: + version = tomllib.load(f)["project"]["version"] + + expected_ref = f"refs/tags/v{version}" + actual_ref = os.environ["GITHUB_REF"] + if actual_ref != expected_ref: + raise SystemExit(f"Tag {actual_ref!r} does not match package version {version!r}") + PY + + - name: Build distribution + run: python -m build + + - name: Check distribution metadata + run: python -m twine check dist/* + + - name: Upload distribution artifact + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + test: + name: Test distribution on Python ${{ matrix.python-version }} + needs: build + runs-on: ubuntu-24.04 strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] - steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pytest build - - name: Build Python package - run: | - python -m build - - name: Install Python Package - run: | - pip install dist/*.whl - - name: Test with pytest - run: | - pytest tests/ -v + cache: pip - deploy: - needs: build - runs-on: ubuntu-22.04 - environment: ${{ github.ref == 'refs/heads/master' && 'production' || 'staging' }} - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 + - name: Download distribution artifact + uses: actions/download-artifact@v4 with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pytest build - - name: re-Build Python package - run: | - python -m build - - name: Install Python Package - run: | - pip install dist/*.whl - - name: Install twine + name: python-package-distributions + path: dist/ + + - name: Install test dependencies run: | python -m pip install --upgrade pip - pip install twine - - name: Upload to PyPI - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python -m twine upload --skip-existing dist/* + python -m pip install pytest - release: - needs: deploy - runs-on: ubuntu-22.04 - environment: ${{ github.ref == 'refs/heads/master' && 'production' || 'staging' }} + - name: Install built wheel + run: python -m pip install dist/*.whl + + - name: Run tests + run: pytest tests/ -v + + publish: + name: Publish to PyPI + needs: test + runs-on: ubuntu-24.04 + environment: pypi + permissions: + contents: read + id-token: write steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 + - name: Download distribution artifact + uses: actions/download-artifact@v4 with: - python-version: "3.10" - - name: Get Package version - id: get_version - run: | - echo "version=$(python -c "import re; print(re.search(r'version = \"(.*?)\"', open('pyproject.toml').read()).group(1))")" >> $GITHUB_ENV - - name: Check if release exists - id: check_release - run: | - RELEASE_ID=$(curl --silent --show-error --location --retry 3 --output /dev/null --write-out "%{http_code}" --header "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ env.version }}") - echo "exists=$([[ "$RELEASE_ID" != "404" ]] && echo true || echo false)" >> $GITHUB_ENV - - name: Create Release - id: create_release - if: ${{ !env.exists }} - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ env.version }} - release_name: Release ${{ env.version }} - draft: false - prerelease: false + name: python-package-distributions + path: dist/ - deploy_docs: - needs: build - runs-on: ubuntu-22.04 - environment: ${{ github.ref == 'refs/heads/master' && 'production' || 'staging' }} + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + github_release: + name: Create GitHub release + needs: publish + runs-on: ubuntu-24.04 + permissions: + contents: write steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 + - name: Download distribution artifact + uses: actions/download-artifact@v4 with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install mkdocs - - name: Deploy to GitHub Pages - run: | - mkdocs gh-deploy --force + name: python-package-distributions + path: dist/ + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + files: dist/* diff --git a/.github/workflows/python-CI.yml b/.github/workflows/python-CI.yml index 9c9974f..c593443 100644 --- a/.github/workflows/python-CI.yml +++ b/.github/workflows/python-CI.yml @@ -1,58 +1,80 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: SplitRaster Developmnet Build Install and Test +name: CI on: push: - branches: [develop, feature/*] - + branches: + - develop + - main + - master + - "feature/**" + - "codex/**" pull_request: - branches: [develop, feature/*] + branches: + - develop + - main + - master + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - format_and_check: - runs-on: ubuntu-22.04 - environment: development + lint: + name: Lint and format + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.10" - - name: Install dependencies + python-version: "3.13" + cache: pip + + - name: Install lint dependencies run: | python -m pip install --upgrade pip - pip install ruff - - name: Lint with ruff - run: | - ruff check . - - name: Format check with ruff - run: | - ruff format --check . + python -m pip install "ruff>=0.9.7" + + - name: Run ruff + run: ruff check . - build: - needs: format_and_check - runs-on: ubuntu-22.04 - environment: development + - name: Check formatting + run: ruff format --check . + + test: + name: Build and test Python ${{ matrix.python-version }} + needs: lint + runs-on: ubuntu-24.04 strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + cache: pip + + - name: Install build dependencies run: | python -m pip install --upgrade pip - pip install pytest build - - name: Build Python package - run: | - python -m build - - name: Install Python Package - run: | - pip install dist/*.whl - - name: Test with pytest - run: | - pytest tests/ -v + python -m pip install build pytest twine + + - name: Build distribution + run: python -m build + + - name: Check distribution metadata + run: python -m twine check dist/* + + - name: Install built wheel + run: python -m pip install dist/*.whl + + - name: Run tests + run: pytest tests/ -v diff --git a/src/splitraster/__init__.py b/src/splitraster/__init__.py index b408dbb..0fb9ae3 100644 --- a/src/splitraster/__init__.py +++ b/src/splitraster/__init__.py @@ -1,5 +1,10 @@ -__version__ = "0.4.0" +from importlib.metadata import PackageNotFoundError, version from . import io -__all__ = ["io"] +try: + __version__ = version("splitraster") +except PackageNotFoundError: + __version__ = "0.0.0" + +__all__ = ["__version__", "io"] diff --git a/tests/test_splitraster.py b/tests/test_splitraster.py index ba30eda..9d9798a 100644 --- a/tests/test_splitraster.py +++ b/tests/test_splitraster.py @@ -1,9 +1,16 @@ # Test the Packages import os +from importlib.metadata import version base_dir = os.path.dirname(os.path.abspath(__file__)) +def test_package_version_matches_metadata() -> None: + import splitraster + + assert splitraster.__version__ == version("splitraster") + + # Example A: def test_rgb_gt_slide_window() -> None: from splitraster import io From 9674412f763cabb17d1ce434bbfc813e13bfdb5d Mon Sep 17 00:00:00 2001 From: "Caihao (Chris) Cui" Date: Sun, 24 May 2026 11:08:19 +1000 Subject: [PATCH 3/4] Document docs publishing flow --- docs/.nojekyll | 0 docs/CONTRIBUTING.md | 199 +++++++++++++++++++++++++++++++------------ 2 files changed, 145 insertions(+), 54 deletions(-) delete mode 100644 docs/.nojekyll diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29..0000000 diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 891fc8c..f726701 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,84 +1,175 @@ -# Contribution Guideline +# Contributing to Split Raster -## How to Contribute +Thanks for helping improve `splitraster`. This guide is for developers who want to +change code, tests, documentation, or release tooling. -First of all, thank you for your interest in contributing to this project. This project is still in its early stage and there are many things to do. If you are interested in contributing to this project, please follow the steps below: +## Branching Model -1. Fork the repository -2. Make your changes -3. Submit a pull request -4. and wait for the review +This repository uses a simple develop-to-release flow: -### Clone the repository +- `develop` is the integration branch for day-to-day development. +- `feature/*` and `codex/*` branches are used for focused changes. +- `master` is the stable release branch. +- `v*` tags trigger package release, for example `v0.4.1`. + +Start new work from `develop`: ```bash -# make sure you have the latest version of the code -git clone "https://github.com/cuicaihao/split_raster.git" -# make sure you are in the master branch -git checkout master -# pull the latest code +git switch develop git pull -# create a new branch for your changes -git checkout -b -# make your changes -# add your changes -git add . -# commit your changes -git commit -m "your commit message" -# push your changes -git push origin -# submit a pull request +git switch -c feature/your-change ``` -## Setting up the development environment +Keep changes small and reviewable. Separate unrelated bug fixes, workflow changes, +documentation updates, and releases into different branches when practical. + +## Development Environment + +The project requires Python 3.10 or newer and uses `uv` for dependency management. + +```bash +uv sync +``` -This project is developed using Python >= 3.10. The following packages are required: +Run commands through `uv run` so they use the project environment: -- uv -- tqdm -- numpy -- scikit-image -- (optional) gdal (for GeoTiff support) +```bash +uv run pytest tests/ -v +uv run ruff check . +uv run ruff format --check . +``` -Please use `uv` to manage the virtual environment and dependencies. The following commands will help you set up the development environment. +Optional GeoTIFF support requires GDAL: ```bash -# install uv if you haven't -curl -LsSf https://astral.sh/uv/install.sh | sh +uv pip install "splitraster[geo]" +``` -# sync the environment -uv sync +GDAL often needs system packages as well. If your change does not touch +`src/splitraster/geo.py`, it is acceptable to run the regular test suite without a +local GDAL install. -# run tests +## Quality Checks + +Before opening a pull request, run: + +```bash +uv run ruff check . +uv run ruff format --check . uv run pytest tests/ -v ``` -Then if you run the following command in your shell, you will see the installed packages. +For packaging-related changes, also run: ```bash -❯ uv pip list -... -splitraster (at /path/to/split_raster) -numpy -scikit-image -tqdm -... +uv run python -m build +uv run python -m twine check dist/* ``` -## Testing +The CI workflow repeats the important checks on Python 3.10, 3.11, 3.12, and 3.13. + +## Tests + +Tests should avoid writing generated files into tracked sample-data directories. +Use `tmp_path` for output created during tests. + +Add or update tests when changing: + +- tile naming or overwrite behavior +- crop size, stride, padding, or overlap behavior +- random crop pairing between image and label +- GeoTIFF read/write behavior +- package metadata, build, or release workflow behavior + +GeoTIFF logic can be covered with fake GDAL objects when the test only needs to +verify control flow. Use real GDAL integration tests only when the file I/O itself +is the behavior under test. + +## Pull Requests + +Open pull requests into `develop` unless the maintainer asks for a different +target branch. + +A good pull request includes: -To test your changes, please run the following command: +- a concise description of the change +- the reason for the change +- tests that were run +- any remaining risks, especially around GDAL or release automation + +## Versioning + +The package version is maintained in one place: + +```toml +# pyproject.toml +[project] +version = "0.4.1" +``` + +Do not edit `src/splitraster/__init__.py` to bump the version. The package +`__version__` is read from installed package metadata. + +Use semantic versioning in spirit: + +- patch version for bug fixes and documentation-only release corrections +- minor version for backward-compatible features +- major version for breaking API or output-format changes + +## Release Process + +Releases are tag-driven. The release workflow runs only for tags matching `v*`. +It also verifies that the tag matches the version in `pyproject.toml`. + +For example, to release `0.4.1`: ```bash -❯ pytest tests/ -v -cachedir: .pytest_cache -rootdir: /Users/caihaocui/GitHub/split_raster -collected 2 items +# Update pyproject.toml: +# version = "0.4.1" + +git add pyproject.toml +git commit -m "Bump version to 0.4.1" -tests/test_splitraster.py::test_rgb_gt_slide_window PASSED [ 50%] -tests/test_splitraster.py::test_rgb_gt_random_crop PASSED [100%] +# After the release commit is on master: +git tag v0.4.1 +git push origin master +git push origin v0.4.1 ``` -If you see the above output, it means that you have successfully passed the test. +The release workflow will: + +1. build the source distribution and wheel once +2. validate distribution metadata with `twine check` +3. test the built wheel on Python 3.10 through 3.13 +4. publish to PyPI with Trusted Publishing/OIDC +5. create a GitHub Release with the same artifacts + +PyPI Trusted Publishing must be configured for: + +- repository: `cuicaihao/split_raster` +- workflow: `python-CD.yml` +- environment: `pypi` + +## Documentation + +Documentation source files live in `docs/` and are built with MkDocs. Treat +documentation like code: edit the source Markdown and image files, then let the +docs workflow publish the generated site. + +Repository layout: + +- `docs/` contains tracked documentation source files. +- `mkdocs.yml` contains navigation and MkDocs configuration. +- `site/` is local build output and should stay untracked. +- `gh-pages` is the deployment branch for generated GitHub Pages content. + +Do not manually edit generated files in `site/` or on `gh-pages`. Make the change +in `docs/`, verify it locally, and let the workflow publish it. + +```bash +uv run mkdocs serve +uv run mkdocs build +``` -## END +Documentation deploy is handled separately from package release and runs from the +docs workflow. From 07487b1c98d0492a411e0b4b78af1d429027848c Mon Sep 17 00:00:00 2001 From: "Caihao (Chris) Cui" Date: Sun, 24 May 2026 11:19:19 +1000 Subject: [PATCH 4/4] Bump version to 0.4.1 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 282e1f9..81d959d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "splitraster" -version = "0.4.0" +version = "0.4.1" authors = [{ name = "Chris Cui" }] description = "Provide good support for deep learning and computer vision tasks by creating a tiled output from an input raster dataset." readme = "PyPi.md" diff --git a/uv.lock b/uv.lock index 8e2c7d4..2c7b393 100644 --- a/uv.lock +++ b/uv.lock @@ -2227,7 +2227,7 @@ wheels = [ [[package]] name = "splitraster" -version = "0.4.0" +version = "0.4.1" source = { editable = "." } dependencies = [ { name = "numpy" },