Repository: rapidsai/cucim (language: python)

Issue:
Forward-merge branch-24.08 into branch-24.10
Forward-merge triggered by push to branch-24.08 that creates a PR to keep branch-24.10 up-to-date. If this PR is unable to be immediately merged due to conflicts, it will remain open for the team to manually merge. See [forward-merger docs](https://docs.rapids.ai/maintainers/forward-merger/) for more info.
Replace or drop `pytest-lazy-fixture`
After some recent unrelated testing work, we learned that `pytest-lazy-fixture` is incompatible with `pytest` version `8.0.0` or newer. This has been the case for 6 months now as this issue notes: https://github.com/TvoroG/pytest-lazy-fixture/issues/65

Given this we may want to drop or replace this dependency

One option on the replacement side is `pytest-lazy-fixtures`, which it looks straightforward to switch to
Add support for Python 3.12
## Description

Contributes to https://github.com/rapidsai/build-planning/issues/40

This PR adds support for Python 3.12.

## Notes for Reviewers

This is part of ongoing work to add Python 3.12 support across RAPIDS.
It temporarily introduces a build/test matrix including Python 3.12, from https://github.com/rapidsai/shared-workflows/pull/213.

A follow-up PR will revert back to pointing at the `branch-24.10` branch of `shared-workflows` once all
RAPIDS repos have added Python 3.12 support.

### This will fail until all dependencies have been updates to Python 3.12

CI here is expected to fail until all of this project's upstream dependencies support Python 3.12.

This can be merged whenever all CI jobs are passing.

add test-time dependency on setuptools
## Summary

Contributes to #775, follow-up to #773.

Some nightly Python 3.12 tests are failing in this repo, as a result of the following:

* some tests here rely on `GPUtil`
* `GPUtil` was last updated 5+ years ago, and unconditionally imports `distutils` ([code link](https://github.com/anderskm/gputil/blob/42ef071dfcb6469b7ab5ab824bde6ca9f6d0ab8a/GPUtil/GPUtil.py#L34))
* `distutils` was removed from the Python standard library in Python 3.12

This proposes a **temporary fix**... adding a test-time dependency on `setuptools`. `import distutils` will work in an environment where `setuptools` is installed.

## Benefits of these changes

Fixes nightly tests, without imposing any new requirements of users of `cucim`.

## Notes for Reviewers

At some point, `setuptools` will likely remove `distutils` or the parts of it that `GPUtil` needs, and these tests will break in this way again. I've listed out some alternatives in #775, those should be explored at some point.

## How I tested this

On a machine with CUDA 12.2 and V100s, I can reproduce the test failures like this:

```shell
docker run \
    --rm \
    --gpus 1 \
    -v $(pwd):/opt/work \
    -w /opt/work \
    -it rapidsai/citestwheel:cuda12.5.1-ubuntu22.04-py3.12 \
    bash

python -m pip install \
    'cucim-cu12[test]==24.10.*,>=0.0.0a0'

python -m pytest \
    --numprocesses=8 \
    --dist=worksteal \
    ./python/cucim/tests/performance/clara
```

After running `pip install setuptools`, those tests pass 😁 


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index f18070040..6d5f94f8c 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -28,7 +28,7 @@ concurrency:
 jobs:
   cpp-build:
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.10
     with:
       build_type: ${{ inputs.build_type || 'branch' }}
       branch: ${{ inputs.branch }}
@@ -38,7 +38,7 @@ jobs:
     if: github.ref_type == 'branch'
     needs: [python-build]
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10
     with:
       arch: "amd64"
       branch: ${{ inputs.branch }}
@@ -51,7 +51,7 @@ jobs:
   python-build:
     needs: [cpp-build]
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.10
     with:
       build_type: ${{ inputs.build_type || 'branch' }}
       branch: ${{ inputs.branch }}
@@ -60,7 +60,7 @@ jobs:
   upload-conda:
     needs: [cpp-build, python-build]
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@branch-24.10
     with:
       build_type: ${{ inputs.build_type || 'branch' }}
       branch: ${{ inputs.branch }}
@@ -68,7 +68,7 @@ jobs:
       sha: ${{ inputs.sha }}
   wheel-build:
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10
     with:
       build_type: ${{ inputs.build_type || 'branch' }}
       branch: ${{ inputs.branch }}
@@ -78,7 +78,7 @@ jobs:
   wheel-publish:
     needs: wheel-build
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.10
     with:
       build_type: ${{ inputs.build_type || 'branch' }}
       branch: ${{ inputs.branch }}
diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml
index 5020f00c3..7b0c112c4 100644
--- a/.github/workflows/pr.yaml
+++ b/.github/workflows/pr.yaml
@@ -20,32 +20,32 @@ jobs:
       - wheel-build
       - wheel-tests
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@branch-24.10
   checks:
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@branch-24.10
   conda-cpp-build:
     needs: checks
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.10
     with:
       build_type: pull-request
   conda-python-build:
     needs: conda-cpp-build
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.10
     with:
       build_type: pull-request
   conda-python-tests:
     needs: conda-python-build
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.10
     with:
       build_type: pull-request
   docs-build:
     needs: conda-python-build
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.10
     with:
       build_type: pull-request
       node_type: "gpu-v100-latest-1"
@@ -55,14 +55,14 @@ jobs:
   wheel-build:
     needs: checks
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.10
     with:
       build_type: pull-request
       script: ci/build_wheel.sh
   wheel-tests:
     needs: wheel-build
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10
     with:
       build_type: pull-request
       script: ci/test_wheel.sh
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d2e786b7c..b912cf46a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,11 +16,11 @@ repos:
       - id: black
         args: ["--config", "python/cucim/pyproject.toml"]
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.1.3
+    rev: v0.6.1
     hooks:
       - id: ruff
         types_or: [python, pyi]
-        args: [--fix, --exit-non-zero-on-fix]
+        args: [--fix, --exit-non-zero-on-fix, --config, "python/cucim/pyproject.toml"]
   - repo: https://github.com/codespell-project/codespell
     rev: v2.2.6
     hooks:
@@ -29,7 +29,7 @@ repos:
         additional_dependencies:
           - tomli
   - repo: https://github.com/rapidsai/pre-commit-hooks
-    rev: v0.2.0
+    rev: v0.4.0
     hooks:
       - id: verify-alpha-spec
   - repo: https://github.com/rapidsai/dependency-file-generator
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 28e824cd2..00191a0e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,28 @@
+# cucim 24.10.00 (9 Oct 2024)
+
+## 🐛 Bug Fixes
+
+- Use cupy to measure memory leak ([#777](https://github.com/rapidsai/cucim/pull/777)) [@bdice](https://github.com/bdice)
+- Fix wheel tests for Rocky Linux 8. ([#774](https://github.com/rapidsai/cucim/pull/774)) [@bdice](https://github.com/bdice)
+- Disable custom 2D separable filtering kernels on windows ([#770](https://github.com/rapidsai/cucim/pull/770)) [@grlee77](https://github.com/grlee77)
+- chan_vese: pass all constants to `_fused_variance_kernel2` as device scalars ([#764](https://github.com/rapidsai/cucim/pull/764)) [@grlee77](https://github.com/grlee77)
+- Fix &quot;compatibility&quot; spelling in CHANGELOG ([#759](https://github.com/rapidsai/cucim/pull/759)) [@jakirkham](https://github.com/jakirkham)
+- Fix error in dependencies.yaml causing incomplete pyproject.toml generation ([#757](https://github.com/rapidsai/cucim/pull/757)) [@grlee77](https://github.com/grlee77)
+
+## 🛠️ Improvements
+
+- Do not depends on unused libraries for libtiff ([#785](https://github.com/rapidsai/cucim/pull/785)) [@gigony](https://github.com/gigony)
+- Fix a couple of performance issues in `peak_local_max` (improve performance of blob detectors and `corner_peaks`) ([#782](https://github.com/rapidsai/cucim/pull/782)) [@grlee77](https://github.com/grlee77)
+- update vendored CUDA includes to match CuPy &gt;= 13.3 ([#781](https://github.com/rapidsai/cucim/pull/781)) [@grlee77](https://github.com/grlee77)
+- Use CI workflow branch &#39;branch-24.10&#39; again ([#780](https://github.com/rapidsai/cucim/pull/780)) [@jameslamb](https://github.com/jameslamb)
+- Add support for Python 3.12 ([#773](https://github.com/rapidsai/cucim/pull/773)) [@jameslamb](https://github.com/jameslamb)
+- Update rapidsai/pre-commit-hooks ([#772](https://github.com/rapidsai/cucim/pull/772)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA)
+- bump scikit-image upper bound (allow 0.24.x) ([#769](https://github.com/rapidsai/cucim/pull/769)) [@grlee77](https://github.com/grlee77)
+- Drop Python 3.9 support ([#766](https://github.com/rapidsai/cucim/pull/766)) [@jameslamb](https://github.com/jameslamb)
+- Remove NumPy &lt;2 pin ([#762](https://github.com/rapidsai/cucim/pull/762)) [@seberg](https://github.com/seberg)
+- Update pre-commit hooks ([#760](https://github.com/rapidsai/cucim/pull/760)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA)
+- Switch to pytest-lazy-fixtures ([#756](https://github.com/rapidsai/cucim/pull/756)) [@gigony](https://github.com/gigony)
+
 # cucim 24.08.00 (7 Aug 2024)
 
 ## 🛠️ Improvements
@@ -5,7 +30,7 @@
 - Drop NumPy build dependency ([#751](https://github.com/rapidsai/cucim/pull/751)) [@jakirkham](https://github.com/jakirkham)
 - Use workflow branch 24.08 again ([#749](https://github.com/rapidsai/cucim/pull/749)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA)
 - Build and test with CUDA 12.5.1 ([#747](https://github.com/rapidsai/cucim/pull/747)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA)
-- Minor fixes for NumPy 2.0 compatiblity ([#746](https://github.com/rapidsai/cucim/pull/746)) [@grlee77](https://github.com/grlee77)
+- Minor fixes for NumPy 2.0 compatibility ([#746](https://github.com/rapidsai/cucim/pull/746)) [@grlee77](https://github.com/grlee77)
 - skip CMake 3.30.0, require CMake &gt;=3.26.4 ([#745](https://github.com/rapidsai/cucim/pull/745)) [@jameslamb](https://github.com/jameslamb)
 - Use verify-alpha-spec hook ([#744](https://github.com/rapidsai/cucim/pull/744)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA)
 - remove .gitattributes ([#740](https://github.com/rapidsai/cucim/pull/740)) [@jameslamb](https://github.com/jameslamb)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a05dee929..9dc5c6232 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -123,7 +123,7 @@ cd $CUCIM_HOME
 
 Conda can be used to setup an environment which includes all of the necessary dependencies (as shown in `./conda/environments/all_cuda-118_arch-x86_64.yaml`) for building cuCIM.
 
-Otherwise, you may need to install dependencies (such as zlib, xz, yasm) through your OS's package manager (`apt`, `yum`, and so on).
+Otherwise, you may need to install dependencies (such as yasm) through your OS's package manager (`apt`, `yum`, and so on).
 
 
 ### Creating the Conda Development Environment `cucim`
diff --git a/LICENSE-3rdparty.md b/LICENSE-3rdparty.md
index 984d0e952..a2e46342c 100644
--- a/LICENSE-3rdparty.md
+++ b/LICENSE-3rdparty.md
@@ -243,10 +243,11 @@ libcuckoo
 - Copyright: Carnegie Mellon University and Intel Corporation
 - Usage: Using concurrent hash table implementation for cache mechanism.
 
-pytest-lazy-fixture
+pytest-lazy-fixtures
 - License: MIT License
-  - https://github.com/TvoroG/pytest-lazy-fixture/blob/master/LICENSE
-- Copyright: Marsel Zaripov
+- https://github.com/dev-petrov/pytest-lazy-fixtures
+  - https://github.com/dev-petrov/pytest-lazy-fixtures/blob/master/LICENSE
+- Copyright: Anton Petrov
 - Usage: Using lazy fixture feature in PyTest.
 
 psutil
diff --git a/VERSION b/VERSION
index ec8489fda..7c7ba0443 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-24.08.00
+24.10.00
diff --git a/benchmarks/skimage/_image_bench.py b/benchmarks/skimage/_image_bench.py
index abb2ffbbb..b04100154 100644
--- a/benchmarks/skimage/_image_bench.py
+++ b/benchmarks/skimage/_image_bench.py
@@ -12,6 +12,7 @@
 import pandas as pd
 import scipy.ndimage
 import skimage.data
+
 from cucim.time import repeat
 
 
@@ -23,7 +24,7 @@ def product_dict(**kwargs):
         yield dict(zip(keys, instance))
 
 
-class ImageBench(object):
+class ImageBench:
     def __init__(
         self,
         function_name,
diff --git a/benchmarks/skimage/bench_convolve.py b/benchmarks/skimage/bench_convolve.py
index a43636931..f44714306 100644
--- a/benchmarks/skimage/bench_convolve.py
+++ b/benchmarks/skimage/bench_convolve.py
@@ -3,6 +3,8 @@
 """
 import cupy as cp
 import cupyx.scipy.ndimage as ndi
+from cupyx.profiler import benchmark
+
 from cucim.skimage._vendored.ndimage import (  # noqa: F401
     convolve1d,
     correlate1d,
@@ -16,7 +18,6 @@
     uniform_filter,
     uniform_filter1d,
 )
-from cupyx.profiler import benchmark
 
 d = cp.cuda.Device()
 
diff --git a/benchmarks/skimage/cucim_color_bench.py b/benchmarks/skimage/cucim_color_bench.py
index 8f394daef..e4fe9fe95 100644
--- a/benchmarks/skimage/cucim_color_bench.py
+++ b/benchmarks/skimage/cucim_color_bench.py
@@ -2,8 +2,6 @@
 import os
 import pickle
 
-import cucim.skimage
-import cucim.skimage.color
 import cupy
 import cupy as cp
 import cupyx.scipy.ndimage
@@ -14,6 +12,9 @@
 import skimage.color
 from _image_bench import ImageBench
 
+import cucim.skimage
+import cucim.skimage.color
+
 func_name_choices = [
     "convert_colorspace",
     "rgb2hed",
@@ -223,7 +224,7 @@ def main(args):
     try:
         import tabular  # noqa: F401
 
-        with open(fbase + ".md", "wt") as f:
+        with open(fbase + ".md", "w") as f:
             f.write(all_results.to_markdown())
     except ImportError:
         pass
diff --git a/benchmarks/skimage/cucim_exposure_bench.py b/benchmarks/skimage/cucim_exposure_bench.py
index 558a2282f..bb922d619 100644
--- a/benchmarks/skimage/cucim_exposure_bench.py
+++ b/benchmarks/skimage/cucim_exposure_bench.py
@@ -2,8 +2,6 @@
 import os
 import pickle
 
-import cucim.skimage
-import cucim.skimage.exposure
 import cupy
 import cupy as cp
 import numpy as np
@@ -12,6 +10,9 @@
 import skimage.exposure
 from _image_bench import ImageBench
 
+import cucim.skimage
+import cucim.skimage.exposure
+
 
 class ExposureBench(ImageBench):
     def set_args(self, dtype):
@@ -129,7 +130,7 @@ def main(args):
     fbase = os.path.splitext(pfile)[0]
     all_results.to_csv(fbase + ".csv")
     all_results.to_pickle(pfile)
-    with open(fbase + ".md", "wt") as f:
+    with open(fbase + ".md", "w") as f:
         f.write(all_results.to_markdown())
 
 
diff --git a/benchmarks/skimage/cucim_feature_bench.py b/benchmarks/skimage/cucim_feature_bench.py
index 4142d7965..9b1d67bca 100644
--- a/benchmarks/skimage/cucim_feature_bench.py
+++ b/benchmarks/skimage/cucim_feature_bench.py
@@ -3,17 +3,18 @@
 import os
 import pickle
 
-import cucim.skimage
-import cucim.skimage.feature
 import cupy as cp
 import numpy as np
 import pandas as pd
 import skimage
 import skimage.feature
 from _image_bench import ImageBench
-from cucim.skimage import exposure
 from skimage import data, draw
 
+import cucim.skimage
+import cucim.skimage.feature
+from cucim.skimage import exposure
+
 
 class BlobDetectionBench(ImageBench):
     def set_args(self, dtype):
@@ -218,7 +219,7 @@ def main(args):
     fbase = os.path.splitext(pfile)[0]
     all_results.to_csv(fbase + ".csv")
     all_results.to_pickle(pfile)
-    with open(fbase + ".md", "wt") as f:
+    with open(fbase + ".md", "w") as f:
         f.write(all_results.to_markdown())
 
 
diff --git a/benchmarks/skimage/cucim_filters_bench.py b/benchmarks/skimage/cucim_filters_bench.py
index 09a47e346..f78d60904 100644
--- a/benchmarks/skimage/cucim_filters_bench.py
+++ b/benchmarks/skimage/cucim_filters_bench.py
@@ -2,14 +2,15 @@
 import os
 import pickle
 
-import cucim.skimage
-import cucim.skimage.filters
 import numpy as np
 import pandas as pd
 import skimage
 import skimage.filters
 from _image_bench import ImageBench
 
+import cucim.skimage
+import cucim.skimage.filters
+
 
 def main(args):
     pfile = "cucim_filters_results.pickle"
@@ -189,7 +190,7 @@ def main(args):
     fbase = os.path.splitext(pfile)[0]
     all_results.to_csv(fbase + ".csv")
     all_results.to_pickle(pfile)
-    with open(fbase + ".md", "wt") as f:
+    with open(fbase + ".md", "w") as f:
         f.write(all_results.to_markdown())
 
 
diff --git a/benchmarks/skimage/cucim_measure_bench.py b/benchmarks/skimage/cucim_measure_bench.py
index e7e7b40df..7f8c09f17 100644
--- a/benchmarks/skimage/cucim_measure_bench.py
+++ b/benchmarks/skimage/cucim_measure_bench.py
@@ -3,8 +3,6 @@
 import os
 import pickle
 
-import cucim.skimage
-import cucim.skimage.measure
 import cupy as cp
 import numpy as np
 import pandas as pd
@@ -13,6 +11,9 @@
 from _image_bench import ImageBench
 from cucim_metrics_bench import MetricsBench
 
+import cucim.skimage
+import cucim.skimage.measure
+
 
 class LabelBench(ImageBench):
     def __init__(
@@ -328,7 +329,7 @@ def main(args):
     fbase = os.path.splitext(pfile)[0]
     all_results.to_csv(fbase + ".csv")
     all_results.to_pickle(pfile)
-    with open(fbase + ".md", "wt") as f:
+    with open(fbase + ".md", "w") as f:
         f.write(all_results.to_markdown())
 
 
diff --git a/benchmarks/skimage/cucim_metrics_bench.py b/benchmarks/skimage/cucim_metrics_bench.py
index 585dd7be8..4dd8aae49 100644
--- a/benchmarks/skimage/cucim_metrics_bench.py
+++ b/benchmarks/skimage/cucim_metrics_bench.py
@@ -2,14 +2,15 @@
 import os
 import pickle
 
-import cucim.skimage
-import cucim.skimage.metrics
 import cupy as cp
 import numpy as np
 import pandas as pd
 import skimage
 import skimage.metrics
 from _image_bench import ImageBench
+
+import cucim.skimage
+import cucim.skimage.metrics
 from cucim.skimage import data, measure
 
 
@@ -176,7 +177,7 @@ def main(args):
     fbase = os.path.splitext(pfile)[0]
     all_results.to_csv(fbase + ".csv")
     all_results.to_pickle(pfile)
-    with open(fbase + ".md", "wt") as f:
+    with open(fbase + ".md", "w") as f:
         f.write(all_results.to_markdown())
 
 
diff --git a/benchmarks/skimage/cucim_morphology_bench.py b/benchmarks/skimage/cucim_morphology_bench.py
index 68c5aeefc..8056ad507 100644
--- a/benchmarks/skimage/cucim_morphology_bench.py
+++ b/benchmarks/skimage/cucim_morphology_bench.py
@@ -6,8 +6,6 @@
 import os
 import pickle
 
-import cucim.skimage
-import cucim.skimage.morphology
 import cupy as cp
 import numpy as np
 import pandas as pd
@@ -17,6 +15,9 @@
 import skimage.morphology
 from _image_bench import ImageBench
 
+import cucim.skimage
+import cucim.skimage.morphology
+
 
 class BinaryMorphologyBench(ImageBench):
     def __init__(
@@ -281,7 +282,7 @@ def main(args):
     try:
         import tabular  # noqa: F401
 
-        with open(fbase + ".md", "wt") as f:
+        with open(fbase + ".md", "w") as f:
             f.write(all_results.to_markdown())
     except ImportError:
         pass
diff --git a/benchmarks/skimage/cucim_registration_bench.py b/benchmarks/skimage/cucim_registration_bench.py
index be5b94594..e35eaebf1 100644
--- a/benchmarks/skimage/cucim_registration_bench.py
+++ b/benchmarks/skimage/cucim_registration_bench.py
@@ -3,8 +3,6 @@
 import os
 import pickle
 
-import cucim.skimage
-import cucim.skimage.registration
 import cupy as cp
 import numpy as np
 import pandas as pd
@@ -12,6 +10,9 @@
 import skimage.registration
 from _image_bench import ImageBench
 
+import cucim.skimage
+import cucim.skimage.registration
+
 
 class RegistrationBench(ImageBench):
     def set_args(self, dtype):
@@ -145,7 +146,7 @@ def main(args):
     try:
         import tabular  # noqa: F401
 
-        with open(fbase + ".md", "wt") as f:
+        with open(fbase + ".md", "w") as f:
             f.write(all_results.to_markdown())
     except ImportError:
         pass
diff --git a/benchmarks/skimage/cucim_restoration_bench.py b/benchmarks/skimage/cucim_restoration_bench.py
index c876cae12..c87eb84f0 100644
--- a/benchmarks/skimage/cucim_restoration_bench.py
+++ b/benchmarks/skimage/cucim_restoration_bench.py
@@ -3,8 +3,6 @@
 import os
 import pickle
 
-import cucim.skimage
-import cucim.skimage.restoration
 import cupy as cp
 import cupyx.scipy.ndimage as ndi
 import numpy as np
@@ -12,9 +10,12 @@
 import skimage
 import skimage.restoration
 from _image_bench import ImageBench
-from cucim.skimage.restoration import denoise_tv_chambolle as tv_gpu
 from skimage.restoration import denoise_tv_chambolle as tv_cpu
 
+import cucim.skimage
+import cucim.skimage.restoration
+from cucim.skimage.restoration import denoise_tv_chambolle as tv_gpu
+
 
 class DenoiseBench(ImageBench):
     def set_args(self, dtype):
@@ -180,7 +181,7 @@ def main(args):
     try:
         import tabular  # noqa: F401
 
-        with open(fbase + ".md", "wt") as f:
+        with open(fbase + ".md", "w") as f:
             f.write(all_results.to_markdown())
     except ImportError:
         pass
diff --git a/benchmarks/skimage/cucim_segmentation_bench.py b/benchmarks/skimage/cucim_segmentation_bench.py
index 77bbae630..012420173 100644
--- a/benchmarks/skimage/cucim_segmentation_bench.py
+++ b/benchmarks/skimage/cucim_segmentation_bench.py
@@ -3,13 +3,14 @@
 import os
 import pickle
 
-import cucim.skimage
 import cupy as cp
 import numpy as np
 import pandas as pd
 import skimage
 import skimage.segmentation
 from _image_bench import ImageBench
+
+import cucim.skimage
 from cucim.skimage import data, measure
 
 
@@ -296,7 +297,7 @@ def main(args):
     try:
         import tabular  # noqa: F401
 
-        with open(fbase + ".md", "wt") as f:
+        with open(fbase + ".md", "w") as f:
             f.write(all_results.to_markdown())
     except ImportError:
         pass
diff --git a/benchmarks/skimage/cucim_transform_bench.py b/benchmarks/skimage/cucim_transform_bench.py
index 6cc007af9..23b7832f2 100644
--- a/benchmarks/skimage/cucim_transform_bench.py
+++ b/benchmarks/skimage/cucim_transform_bench.py
@@ -2,14 +2,15 @@
 import os
 import pickle
 
-import cucim.skimage
-import cucim.skimage.transform
 import numpy as np
 import pandas as pd
 import skimage
 import skimage.transform
 from _image_bench import ImageBench
 
+import cucim.skimage
+import cucim.skimage.transform
+
 
 def main(args):
     pfile = "cucim_transform_results.pickle"
@@ -164,7 +165,7 @@ def main(args):
     try:
         import tabular  # noqa: F401
 
-        with open(fbase + ".md", "wt") as f:
+        with open(fbase + ".md", "w") as f:
             f.write(all_results.to_markdown())
     except ImportError:
         pass
diff --git a/benchmarks/skimage/cupyx_scipy_ndimage_filter_bench.py b/benchmarks/skimage/cupyx_scipy_ndimage_filter_bench.py
index a820c699e..451040bf5 100644
--- a/benchmarks/skimage/cupyx_scipy_ndimage_filter_bench.py
+++ b/benchmarks/skimage/cupyx_scipy_ndimage_filter_bench.py
@@ -129,5 +129,5 @@ def set_args(self, dtype):
 fbase = os.path.splitext(pfile)[0]
 all_results.to_csv(fbase + ".csv")
 all_results.to_pickle(pfile)
-with open(fbase + ".md", "wt") as f:
+with open(fbase + ".md", "w") as f:
     f.write(all_results.to_markdown())
diff --git a/benchmarks/skimage/cupyx_scipy_ndimage_fourier_bench.py b/benchmarks/skimage/cupyx_scipy_ndimage_fourier_bench.py
index 92a4795e0..c2e4160e7 100644
--- a/benchmarks/skimage/cupyx_scipy_ndimage_fourier_bench.py
+++ b/benchmarks/skimage/cupyx_scipy_ndimage_fourier_bench.py
@@ -50,5 +50,5 @@ def set_args(self, dtype):
 fbase = os.path.splitext(pfile)[0]
 all_results.to_csv(fbase + ".csv")
 all_results.to_pickle(pfile)
-with open(fbase + ".md", "wt") as f:
+with open(fbase + ".md", "w") as f:
     f.write(all_results.to_markdown())
diff --git a/benchmarks/skimage/cupyx_scipy_ndimage_interp_bench.py b/benchmarks/skimage/cupyx_scipy_ndimage_interp_bench.py
index 63773732c..c6977cef4 100644
--- a/benchmarks/skimage/cupyx_scipy_ndimage_interp_bench.py
+++ b/benchmarks/skimage/cupyx_scipy_ndimage_interp_bench.py
@@ -153,5 +153,5 @@ def set_args(self, dtype):
 fbase = os.path.splitext(pfile)[0]
 all_results.to_csv(fbase + ".csv")
 all_results.to_pickle(pfile)
-with open(fbase + ".md", "wt") as f:
+with open(fbase + ".md", "w") as f:
     f.write(all_results.to_markdown())
diff --git a/benchmarks/skimage/cupyx_scipy_ndimage_measurements_bench.py b/benchmarks/skimage/cupyx_scipy_ndimage_measurements_bench.py
index 61b3de5a4..e3975972b 100644
--- a/benchmarks/skimage/cupyx_scipy_ndimage_measurements_bench.py
+++ b/benchmarks/skimage/cupyx_scipy_ndimage_measurements_bench.py
@@ -185,5 +185,5 @@ def set_args(self, dtype):
 fbase = os.path.splitext(pfile)[0]
 all_results.to_csv(fbase + ".csv")
 all_results.to_pickle(pfile)
-with open(fbase + ".md", "wt") as f:
+with open(fbase + ".md", "w") as f:
     f.write(all_results.to_markdown())
diff --git a/benchmarks/skimage/cupyx_scipy_ndimage_morphology_bench.py b/benchmarks/skimage/cupyx_scipy_ndimage_morphology_bench.py
index a177c99f9..a2e509a72 100644
--- a/benchmarks/skimage/cupyx_scipy_ndimage_morphology_bench.py
+++ b/benchmarks/skimage/cupyx_scipy_ndimage_morphology_bench.py
@@ -152,5 +152,5 @@ def set_args(self, dtype):
 fbase = os.path.splitext(pfile)[0]
 all_results.to_csv(fbase + ".csv")
 all_results.to_pickle(pfile)
-with open(fbase + ".md", "wt") as f:
+with open(fbase + ".md", "w") as f:
     f.write(all_results.to_markdown())
diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml
index 9e0bbb497..66b66b94f 100644
--- a/conda/environments/all_cuda-118_arch-x86_64.yaml
+++ b/conda/environments/all_cuda-118_arch-x86_64.yaml
@@ -6,7 +6,6 @@ channels:
 - conda-forge
 - nvidia
 dependencies:
-- GPUtil>=1.4.0
 - c-compiler
 - click
 - cmake>=3.26.4,!=3.30.0
@@ -17,17 +16,15 @@ dependencies:
 - gcc_linux-64=11.*
 - imagecodecs>=2021.6.8
 - ipython
-- jbig
 - lazy_loader>=0.1
 - libcufile-dev=1.4.0.31
 - libcufile=1.4.0.31
 - libnvjpeg-dev=11.6.0.55
 - libnvjpeg=11.6.0.55
-- libwebp-base
 - matplotlib-base
 - nbsphinx
 - ninja
-- numpy>=1.23.4,<2.0a0
+- numpy>=1.23.4,<3.0a0
 - numpydoc>=1.5
 - nvcc_linux-64=11.8
 - openslide-python>=1.3.0
@@ -37,21 +34,18 @@ dependencies:
 - psutil>=5.8.0
 - pydata-sphinx-theme
 - pytest-cov>=2.12.1
-- pytest-lazy-fixture>=0.6.3
+- pytest-lazy-fixtures>=1.0.0
 - pytest-xdist
 - pytest>=6.2.4,<8.0.0a0
-- python>=3.8,<3.12
+- python>=3.10,<3.13
 - pywavelets>=1.0
 - recommonmark
-- scikit-image>=0.19.0,<0.24.0a0
+- scikit-image>=0.19.0,<0.25.0a0
 - scipy>=1.6.0
 - sphinx<6
 - sysroot_linux-64==2.17
 - tifffile>=2022.7.28
-- xz
 - yasm
-- zlib
-- zstd
 - pip:
   - opencv-python-headless>=4.6
 name: all_cuda-118_arch-x86_64
diff --git a/conda/environments/all_cuda-125_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml
index d183e447d..392850e85 100644
--- a/conda/environments/all_cuda-125_arch-x86_64.yaml
+++ b/conda/environments/all_cuda-125_arch-x86_64.yaml
@@ -6,7 +6,6 @@ channels:
 - conda-forge
 - nvidia
 dependencies:
-- GPUtil>=1.4.0
 - c-compiler
 - click
 - cmake>=3.26.4,!=3.30.0
@@ -18,16 +17,14 @@ dependencies:
 - gcc_linux-64=11.*
 - imagecodecs>=2021.6.8
 - ipython
-- jbig
 - lazy_loader>=0.1
 - libcufile-dev
 - libnvjpeg-dev
 - libnvjpeg-static
-- libwebp-base
 - matplotlib-base
 - nbsphinx
 - ninja
-- numpy>=1.23.4,<2.0a0
+- numpy>=1.23.4,<3.0a0
 - numpydoc>=1.5
 - openslide-python>=1.3.0
 - pip
@@ -36,21 +33,18 @@ dependencies:
 - psutil>=5.8.0
 - pydata-sphinx-theme
 - pytest-cov>=2.12.1
-- pytest-lazy-fixture>=0.6.3
+- pytest-lazy-fixtures>=1.0.0
 - pytest-xdist
 - pytest>=6.2.4,<8.0.0a0
-- python>=3.8,<3.12
+- python>=3.10,<3.13
 - pywavelets>=1.0
 - recommonmark
-- scikit-image>=0.19.0,<0.24.0a0
+- scikit-image>=0.19.0,<0.25.0a0
 - scipy>=1.6.0
 - sphinx<6
 - sysroot_linux-64==2.17
 - tifffile>=2022.7.28
-- xz
 - yasm
-- zlib
-- zstd
 - pip:
   - opencv-python-headless>=4.6
 name: all_cuda-125_arch-x86_64
diff --git a/conda/recipes/cucim/meta.yaml b/conda/recipes/cucim/meta.yaml
index ed800d086..7bd205bb6 100644
--- a/conda/recipes/cucim/meta.yaml
+++ b/conda/recipes/cucim/meta.yaml
@@ -65,7 +65,7 @@ requirements:
     - libcucim ={{ version }}
     - python
     - rapids-build-backend >=0.3.0,<0.4.0.dev0
-    - scikit-image >=0.19.0,<0.23.0a0
+    - scikit-image >=0.19.0,<0.25.0a0
     - scipy >=1.6
     - setuptools >=24.2.0
   run:
@@ -73,13 +73,13 @@ requirements:
     {% if cuda_major != "11" %}
     - cuda-cudart
     {% endif %}
-    - numpy >=1.23,<2.0a0
+    - numpy >=1.23,<3.0a0
     - click
     - cupy >=12.0.0
     - lazy_loader >=0.1
     - libcucim ={{ version }}
     - python
-    - scikit-image >=0.19.0,<0.23.0a0
+    - scikit-image >=0.19.0,<0.25.0a0
     - scipy >=1.6
   run_constrained:
     - openslide-python >=1.3.0
diff --git a/conda/recipes/libcucim/meta.yaml b/conda/recipes/libcucim/meta.yaml
index b373ffbbe..7ab230806 100644
--- a/conda/recipes/libcucim/meta.yaml
+++ b/conda/recipes/libcucim/meta.yaml
@@ -17,7 +17,6 @@ build:
   number: {{ GIT_DESCRIBE_NUMBER }}
   string: cuda{{ cuda_major }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }}
   ignore_run_exports:
-    - libwebp-base
     - openslide
   ignore_run_exports_from:
     {% if cuda_major == "11" %}
@@ -74,13 +73,8 @@ requirements:
     - libnvjpeg-dev
     - libnvjpeg-static
     {% endif %}
-    - jbig
-    - libwebp-base
     - nvtx-c >=3.1.0
     - openslide
-    - xz
-    - zlib
-    - zstd
   run:
     - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }}
     {% if cuda_major == "11" %}
@@ -91,11 +85,6 @@ requirements:
     - libcufile  # [linux64]
     - libnvjpeg
     {% endif %}
-    - {{ pin_compatible('libwebp-base', max_pin='x.x') }}
-    - jbig
-    - xz
-    - zlib
-    - zstd
   run_constrained:
     - {{ pin_compatible('openslide') }}
 
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/libtiff.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/libtiff.cmake
index 8328bbd71..fd20e7f0e 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/libtiff.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/libtiff.cmake
@@ -53,6 +53,17 @@ if (NOT TARGET deps::libtiff)
     set(JPEG_LIBRARIES deps::libjpeg-turbo)
     # for jpeglib.h and jconfig.h/jconfigint.h
     set(TIFF_INCLUDES ${deps-libjpeg-turbo_SOURCE_DIR} ${deps-libjpeg-turbo_BINARY_DIR} )
+
+    # Explicitly disable external codecs
+    set(zlib OFF)
+    set(pixarlog OFF)
+    set(lzma OFF)
+    set(old-jpeg OFF)
+    set(jpeg12 OFF)
+    set(zstd OFF)
+    set(jbig OFF)
+    set(webp OFF)
+
     add_subdirectory(${deps-libtiff_SOURCE_DIR} ${deps-libtiff_BINARY_DIR} EXCLUDE_FROM_ALL)
 
     # Disable visibility to not expose unnecessary symbols
diff --git a/cpp/plugins/cucim.kit.cuslide/src/cuslide/jpeg2k/gen_color_table.py b/cpp/plugins/cucim.kit.cuslide/src/cuslide/jpeg2k/gen_color_table.py
index 9d9de21f3..2b1d1b366 100644
--- a/cpp/plugins/cucim.kit.cuslide/src/cuslide/jpeg2k/gen_color_table.py
+++ b/cpp/plugins/cucim.kit.cuslide/src/cuslide/jpeg2k/gen_color_table.py
@@ -138,7 +138,7 @@ def gen_b_cb():
     """
     b_cb = [0] * 256
     for i in range(256):
-        b_cb[i] = int((1.77204 * (i - 128)))
+        b_cb[i] = int(1.77204 * (i - 128))
     return b_cb
 
 
diff --git a/cucim.code-workspace b/cucim.code-workspace
index 82eafdcd1..3c395fb16 100644
--- a/cucim.code-workspace
+++ b/cucim.code-workspace
@@ -33,7 +33,7 @@
 					"CUCIM_TESTDATA_FOLDER": "${workspaceDirectory}/test_data",
 					// Add cuslide plugin's library path to LD_LIBRARY_PATH
 					"LD_LIBRARY_PATH": "${workspaceDirectory}/build-debug/lib:${workspaceDirectory}/cpp/plugins/cucim.kit.cuslide/build-debug/lib:${workspaceDirectory}/temp/cuda/lib64:${os_env:LD_LIBRARY_PATH}",
-					"CUCIM_TEST_PLUGIN_PATH": "cucim.kit.cuslide@24.08.00.so"
+					"CUCIM_TEST_PLUGIN_PATH": "cucim.kit.cuslide@24.10.00.so"
 				},
 				"cwd": "${workspaceDirectory}",
 				"catch2": {
@@ -226,7 +226,7 @@
 					},
 					{
 						"name": "CUCIM_TEST_PLUGIN_PATH",
-						"value": "cucim.kit.cuslide@24.08.00.so"
+						"value": "cucim.kit.cuslide@24.10.00.so"
 					}
 				],
 				"console": "externalTerminal",
@@ -254,7 +254,7 @@
 					},
 					{
 						"name": "CUCIM_TEST_PLUGIN_PATH",
-						"value": "cucim.kit.cuslide@24.08.00.so"
+						"value": "cucim.kit.cuslide@24.10.00.so"
 					}
 				],
 				"console": "externalTerminal",
@@ -286,7 +286,7 @@
 					},
 					{
 						"name": "CUCIM_TEST_PLUGIN_PATH",
-						"value": "cucim.kit.cuslide@24.08.00.so"
+						"value": "cucim.kit.cuslide@24.10.00.so"
 					}
 				],
 				"console": "externalTerminal",
diff --git a/dependencies.yaml b/dependencies.yaml
index 840cea8eb..e25c6f909 100644
--- a/dependencies.yaml
+++ b/dependencies.yaml
@@ -69,7 +69,7 @@ files:
       key: developer
     includes:
       - develop
-  py_develop:
+  py_docs:
     output: pyproject
     pyproject_dir: python/cucim
     extras:
@@ -232,14 +232,6 @@ dependencies:
     specific:
       - output_types: conda
         matrices:
-          - matrix:
-              py: "3.8"
-            packages:
-              - python=3.8
-          - matrix:
-              py: "3.9"
-            packages:
-              - python=3.9
           - matrix:
               py: "3.10"
             packages:
@@ -248,9 +240,13 @@ dependencies:
               py: "3.11"
             packages:
               - python=3.11
+          - matrix:
+              py: "3.12"
+            packages:
+              - python=3.12
           - matrix:
             packages:
-              - python>=3.8,<3.12
+              - python>=3.10,<3.13
   rapids_build_setuptools:
     common:
       - output_types: [conda, requirements, pyproject]
@@ -263,23 +259,12 @@ dependencies:
         packages:
           - click
           - lazy_loader>=0.1
-          - numpy>=1.23.4,<2.0a0
-          - scikit-image>=0.19.0,<0.24.0a0
+          - numpy>=1.23.4,<3.0a0
+          - scikit-image>=0.19.0,<0.25.0a0
           - scipy>=1.6.0
       - output_types: conda
         packages:
           - cupy>=12.0.0
-          # All dependencies below this point are specific to `cucim.clara` and
-          # are not needed for either `cucim.core` or `cucim.skimage`. I did
-          # not include these under a "pyproject" output so that it is still
-          # possible to run `pip install .` from the python/cucim folder
-          # without having build the C++ library at all. This allows, usage of
-          # usage of `cucim.skimage` on Windows, for example.
-          - jbig
-          - libwebp-base
-          - xz
-          - zlib
-          - zstd
           # Not sure where these go, if anywhere:
           # - openslide
           # - xorg-libxcb
@@ -299,11 +284,10 @@ dependencies:
     common:
       - output_types: [conda, requirements, pyproject]
         packages:
-          - GPUtil>=1.4.0
           - psutil>=5.8.0
           - pytest>=6.2.4,<8.0.0a0
           - pytest-cov>=2.12.1
-          - pytest-lazy-fixture>=0.6.3
+          - pytest-lazy-fixtures>=1.0.0
           - pytest-xdist
           - tifffile>=2022.7.28
           - pooch>=1.6.0  # needed to download scikit-image sample data
diff --git a/docs/source/conf.py b/docs/source/conf.py
index a7d832627..ee7ef06c8 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
 #
 # Copyright (c) 2018, NVIDIA CORPORATION.
 #
@@ -21,6 +20,7 @@
 #
 import os
 import sys
+
 from packaging.version import Version
 
 import cucim
diff --git a/examples/python/distance_transform_edt_demo.py b/examples/python/distance_transform_edt_demo.py
index d9813b2f4..405cd0f23 100644
--- a/examples/python/distance_transform_edt_demo.py
+++ b/examples/python/distance_transform_edt_demo.py
@@ -10,10 +10,11 @@
     print("This demo requires the matplotlib and colorcet packages.")
     raise (e)
 
+from skimage import data
+
 from cucim.core.operations.morphology import distance_transform_edt
 from cucim.skimage.color import label2rgb
 from cucim.skimage.segmentation import relabel_sequential
-from skimage import data
 
 
 def coords_to_labels(coords):
diff --git a/examples/python/gds_whole_slide/benchmark_round_trip.py b/examples/python/gds_whole_slide/benchmark_round_trip.py
index 2335e73a0..21f4ec278 100644
--- a/examples/python/gds_whole_slide/benchmark_round_trip.py
+++ b/examples/python/gds_whole_slide/benchmark_round_trip.py
@@ -1,15 +1,16 @@
 import os
 from time import time
 
-import cucim.skimage.filters
 import cupy as cp
 import kvikio
 import kvikio.defaults
 import numpy as np
-from cucim.core.operations.color import image_to_absorbance
 from cupyx.profiler import benchmark
 from demo_implementation import cupy_to_zarr, read_tiled
 
+import cucim.skimage.filters
+from cucim.core.operations.color import image_to_absorbance
+
 data_dir = os.environ.get("WHOLE_SLIDE_DATA_DIR", os.path.dirname("__file__"))
 fname = os.path.join(data_dir, "resize.tiff")
 if not os.path.exists(fname):
diff --git a/examples/python/gds_whole_slide/benchmark_zarr_write.py b/examples/python/gds_whole_slide/benchmark_zarr_write.py
index f1f4a021b..13376f410 100644
--- a/examples/python/gds_whole_slide/benchmark_zarr_write.py
+++ b/examples/python/gds_whole_slide/benchmark_zarr_write.py
@@ -4,11 +4,12 @@
 import cupy as cp
 import kvikio.defaults
 import numpy as np
-from cucim.core.operations.color import image_to_absorbance
 from cupyx.profiler import benchmark
 from demo_implementation import cupy_to_zarr, get_n_tiles, read_tiled
 from tifffile import TiffFile
 
+from cucim.core.operations.color import image_to_absorbance
+
 data_dir = os.environ.get("WHOLE_SLIDE_DATA_DIR", os.path.dirname("__file__"))
 fname = os.path.join(data_dir, "resize.tiff")
 if not os.path.exists(fname):
diff --git a/examples/python/gds_whole_slide/demo_implementation.py b/examples/python/gds_whole_slide/demo_implementation.py
index 89cb69342..7d02c8407 100644
--- a/examples/python/gds_whole_slide/demo_implementation.py
+++ b/examples/python/gds_whole_slide/demo_implementation.py
@@ -8,13 +8,14 @@
 import numpy as np
 import openslide
 import tifffile
-from cucim.clara import filesystem
 from kvikio.cufile import IOFuture
 from kvikio.zarr import GDSStore
 from tifffile import TiffFile
 from zarr import DirectoryStore
 from zarr.creation import init_array
 
+from cucim.clara import filesystem
+
 """
 Developed with Dask 2022.05.2
 zarr >= 2.13.2
diff --git a/examples/python/gds_whole_slide/lz4_nvcomp.py b/examples/python/gds_whole_slide/lz4_nvcomp.py
index 19fc5f3d9..a02d6c962 100644
--- a/examples/python/gds_whole_slide/lz4_nvcomp.py
+++ b/examples/python/gds_whole_slide/lz4_nvcomp.py
@@ -66,9 +66,7 @@ def ensure_contiguous_ndarray(buf, max_buffer_size=None, flatten=True):
         raise ValueError("an array with contiguous memory is required")
 
     if max_buffer_size is not None and arr.nbytes > max_buffer_size:
-        msg = "Codec does not support buffers of > {} bytes".format(
-            max_buffer_size
-        )
+        msg = f"Codec does not support buffers of > {max_buffer_size} bytes"
         raise ValueError(msg)
 
     return arr
@@ -148,7 +146,7 @@ def decode(self, buf, out=None):
         return ndarray_copy(decompressed, out)
 
     def __repr__(self):
-        r = "%s" % type(self).__name__
+        r = f"{type(self).__name__}"
         return r
 
 
diff --git a/examples/python/tiff_image/main.py b/examples/python/tiff_image/main.py
index 42adf36e0..c36149a9e 100644
--- a/examples/python/tiff_image/main.py
+++ b/examples/python/tiff_image/main.py
@@ -16,9 +16,10 @@
 import json
 
 import numpy as np
-from cucim import CuImage
 from PIL import Image
 
+from cucim import CuImage
+
 img = CuImage("image.tif")
 
 print(img.is_loaded)  # True if image data is loaded & available.
diff --git a/experiments/Supporting_Aperio_SVS_Format/benchmark.py b/experiments/Supporting_Aperio_SVS_Format/benchmark.py
index db5deac70..24787ed6d 100644
--- a/experiments/Supporting_Aperio_SVS_Format/benchmark.py
+++ b/experiments/Supporting_Aperio_SVS_Format/benchmark.py
@@ -19,9 +19,10 @@
 from itertools import repeat
 from time import perf_counter
 
+from openslide import OpenSlide
+
 from cucim import CuImage
 from cucim.clara.filesystem import discard_page_cache  # noqa: F401
-from openslide import OpenSlide
 
 
 class Timer(ContextDecorator):
@@ -40,7 +41,7 @@ def __enter__(self):
     def __exit__(self, exc_type, exc, exc_tb):
         if not self.end:
             self.elapsed_time()
-        print("{} : {}".format(self.message, self.end - self.start))
+        print(f"{self.message} : {self.end - self.start}")
 
 
 def load_tile_openslide(slide, start_loc, patch_size):
diff --git a/experiments/Using_Cache/benchmark.py b/experiments/Using_Cache/benchmark.py
index b1f69c5dc..c811b6f9a 100644
--- a/experiments/Using_Cache/benchmark.py
+++ b/experiments/Using_Cache/benchmark.py
@@ -21,10 +21,11 @@
 
 import numpy as np
 import rasterio
-from cucim import CuImage
 from openslide import OpenSlide
 from rasterio.windows import Window
 
+from cucim import CuImage
+
 
 class Timer(ContextDecorator):
     def __init__(self, message):
@@ -42,7 +43,7 @@ def __enter__(self):
     def __exit__(self, exc_type, exc, exc_tb):
         if not self.end:
             self.elapsed_time()
-        print("{} : {}".format(self.message, self.end - self.start))
+        print(f"{self.message} : {self.end - self.start}")
 
 
 def load_tile_openslide(slide, start_loc, patch_size):
diff --git a/python/cucim/pyproject.toml b/python/cucim/pyproject.toml
index 806419559..25248b3b2 100644
--- a/python/cucim/pyproject.toml
+++ b/python/cucim/pyproject.toml
@@ -1,4 +1,4 @@
-# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved.
+# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved.
 # See file LICENSE for terms.
 
 [build-system]
@@ -22,13 +22,13 @@ authors = [
     { name = "NVIDIA Corporation" },
 ]
 license = { text = "Apache 2.0" }
-requires-python = ">=3.9"
+requires-python = ">=3.10"
 dependencies = [
     "click",
     "cupy-cuda11x>=12.0.0",
     "lazy_loader>=0.1",
-    "numpy>=1.23.4,<2.0a0",
-    "scikit-image>=0.19.0,<0.24.0a0",
+    "numpy>=1.23.4,<3.0a0",
+    "scikit-image>=0.19.0,<0.25.0a0",
     "scipy>=1.6.0",
 ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`.
 classifiers = [
@@ -57,7 +57,6 @@ Tracker = "https://github.com/rapidsai/cucim/issues"
 
 [project.optional-dependencies]
 test = [
-    "GPUtil>=1.4.0",
     "imagecodecs>=2021.6.8; platform_machine=='x86_64'",
     "matplotlib",
     "numpydoc>=1.5",
@@ -66,7 +65,7 @@ test = [
     "pooch>=1.6.0",
     "psutil>=5.8.0",
     "pytest-cov>=2.12.1",
-    "pytest-lazy-fixture>=0.6.3",
+    "pytest-lazy-fixtures>=1.0.0",
     "pytest-xdist",
     "pytest>=6.2.4,<8.0.0a0",
     "pywavelets>=1.0",
@@ -147,9 +146,6 @@ filterwarnings = [
 ]
 
 [tool.ruff]
-# see: https://docs.astral.sh/ruff/rules/
-select = ["E", "F", "W", "I", "UP"]
-fixable = ["ALL"]
 exclude = [
     # TODO: Remove this in a follow-up where we fix __all__.
     ".tox",
@@ -159,17 +155,29 @@ exclude = [
     "dist",
     ".git",
     "__pycache__",
-    "doc/conf.py",
-    "doc/sphinxext",
+    "docs/source/conf.py",
+    "docs/source/ext",
     "__init__.py",
+    "python/cucim/src/cucim/skimage/_vendored",
 ]
 line-length = 80
 fix = true
 
-[tool.ruff.per-file-ignores]
+[tool.ruff.lint]
+# see: https://docs.astral.sh/ruff/rules/
+select = ["E", "F", "W", "I", "UP"]
+ignore = [
+    # (pycodestyle) Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
+    "E721",
+    # (pyupgrade) Use `X | Y` in `isinstance` call instead of `(X, Y)`
+    "UP038",
+]
+fixable = ["ALL"]
+
+[tool.ruff.lint.per-file-ignores]
 # "src/cucim/skimage/util/tests/test_shape.py" = ["E201", "E202"]
 
-[tool.ruff.isort]
+[tool.ruff.lint.isort]
 combine-as-imports = true
 force-single-line = false
 known-first-party = ["cucim"]
@@ -177,7 +185,7 @@ order-by-type = true
 
 [tool.black]
 line-length = 80
-target-version = ["py39"]
+target-version = ["py310"]
 include = '\.py?$'
 exclude = '''
 /(
diff --git a/python/cucim/src/cucim/core/operations/color/jitter.py b/python/cucim/src/cucim/core/operations/color/jitter.py
index 67b70dfc2..313b24d99 100755
--- a/python/cucim/src/cucim/core/operations/color/jitter.py
+++ b/python/cucim/src/cucim/core/operations/color/jitter.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 import numbers
-from typing import Any, Optional
+from typing import Any
 
 import cupy
 import numpy as np
@@ -51,17 +51,11 @@ def _check_input(
 
 
 def _get_params(
-    brightness: Optional[list[float]],
-    contrast: Optional[list[float]],
-    saturation: Optional[list[float]],
-    hue: Optional[list[float]],
-) -> tuple[
-    np.ndarray,
-    Optional[float],
-    Optional[float],
-    Optional[float],
-    Optional[float],
-]:
+    brightness: list[float] | None,
+    contrast: list[float] | None,
+    saturation: list[float] | None,
+    hue: list[float] | None,
+) -> tuple[np.ndarray, float | None, float | None, float | None, float | None,]:
     fn_idx = np.random.permutation(4)
 
     b = None
diff --git a/python/cucim/src/cucim/core/operations/color/stain_normalizer.py b/python/cucim/src/cucim/core/operations/color/stain_normalizer.py
index 84638b4e0..08c0f9129 100644
--- a/python/cucim/src/cucim/core/operations/color/stain_normalizer.py
+++ b/python/cucim/src/cucim/core/operations/color/stain_normalizer.py
@@ -13,7 +13,6 @@
 # limitations under the License.
 
 import math
-from typing import Union
 
 import cupy as cp
 import numpy as np
@@ -446,12 +445,13 @@ def normalize_colors_pca(
     source_intensity: float = 240.0,
     alpha: float = 1.0,
     beta: float = 0.345,
-    ref_stain_coeff: Union[tuple, cp.ndarray] = (
+    ref_stain_coeff: tuple
+    | cp.ndarray = (
         (0.5626, 0.2159),
         (0.7201, 0.8012),
         (0.4062, 0.5581),
     ),
-    ref_max_conc: Union[tuple, cp.ndarray] = (1.9705, 1.0308),
+    ref_max_conc: tuple | cp.ndarray = (1.9705, 1.0308),
     image_type: str = "intensity",
     channel_axis: int = 0,
 ):
diff --git a/python/cucim/src/cucim/core/operations/intensity/zoom.py b/python/cucim/src/cucim/core/operations/intensity/zoom.py
index 3312a0638..b875bc3b6 100644
--- a/python/cucim/src/cucim/core/operations/intensity/zoom.py
+++ b/python/cucim/src/cucim/core/operations/intensity/zoom.py
@@ -14,7 +14,7 @@
 
 import math
 from collections.abc import Sequence
-from typing import Any, Union
+from typing import Any
 
 import cupy
 import numpy as np
@@ -224,8 +224,8 @@ def get_block_size(output_size_cu, H, W):
 
 
 def get_zoom_factor(
-    min_zoom: Union[Sequence[float], float] = 0.9,
-    max_zoom: Union[Sequence[float], float] = 1.1,
+    min_zoom: Sequence[float] | float = 0.9,
+    max_zoom: Sequence[float] | float = 1.1,
 ):
     R = np.random.RandomState()
     try:
@@ -243,8 +243,8 @@ def get_zoom_factor(
 
 def rand_zoom(
     img: Any,
-    min_zoom: Union[Sequence[float], float] = 0.9,
-    max_zoom: Union[Sequence[float], float] = 1.1,
+    min_zoom: Sequence[float] | float = 0.9,
+    max_zoom: Sequence[float] | float = 1.1,
     prob: float = 0.1,
     whole_batch: bool = False,
 ):
diff --git a/python/cucim/src/cucim/skimage/_shared/coord.py b/python/cucim/src/cucim/skimage/_shared/coord.py
index 5ee981bd8..d7846ba5c 100644
--- a/python/cucim/src/cucim/skimage/_shared/coord.py
+++ b/python/cucim/src/cucim/skimage/_shared/coord.py
@@ -40,7 +40,7 @@ def _ensure_spacing(coord, spacing, p_norm, max_out):
             # keep current point and the points at exactly spacing from it
             candidates.remove(idx)
             dist = distance.cdist(
-                [coord[idx]], coord[candidates], distance.minkowski, p=p_norm
+                [coord[idx]], coord[candidates], "minkowski", p=p_norm
             ).reshape(-1)
             candidates = [c for c, d in zip(candidates, dist) if d < spacing]
 
diff --git a/python/cucim/src/cucim/skimage/_shared/version_requirements.py b/python/cucim/src/cucim/skimage/_shared/version_requirements.py
index 17c7afcc5..e5bf41e36 100644
--- a/python/cucim/src/cucim/skimage/_shared/version_requirements.py
+++ b/python/cucim/src/cucim/skimage/_shared/version_requirements.py
@@ -79,9 +79,12 @@ def is_installed(name, version=None):
         symb = version[: match.start()]
         if not symb:
             symb = "="
-        assert symb in (">=", ">", "=", "<"), (
-            "Invalid version condition '%s'" % symb
-        )
+        assert symb in (
+            ">=",
+            ">",
+            "=",
+            "<",
+        ), f"Invalid version condition '{symb}'"
         version = version[match.start() :]
         return _check_version(actver, version, symb)
 
@@ -115,10 +118,9 @@ def func_wrapped(*args, **kwargs):
             if is_installed(name, version):
                 return obj(*args, **kwargs)
             else:
-                msg = '"%s" in "%s" requires "%s'
-                msg = msg % (obj, obj.__module__, name)
+                msg = f'"{obj}" in "{obj.__module__}" requires "{name}"'
                 if version is not None:
-                    msg += " %s" % version
+                    msg += f" {version}"
                 raise ImportError(msg + '"')
 
         return func_wrapped
diff --git a/python/cucim/src/cucim/skimage/_vendored/_ndimage_filters.py b/python/cucim/src/cucim/skimage/_vendored/_ndimage_filters.py
index b7e2ab4a3..e98fb3eb8 100644
--- a/python/cucim/src/cucim/skimage/_vendored/_ndimage_filters.py
+++ b/python/cucim/src/cucim/skimage/_vendored/_ndimage_filters.py
@@ -1,5 +1,6 @@
 """A vendored subset of cupyx.scipy.ndimage._filters"""
 import math
+import platform
 import warnings
 
 import cupy
@@ -22,6 +23,8 @@
 except ImportError:
     compile_errors = (ResourceLimitError,)
 
+_is_not_windows = platform.system() != "Windows"
+
 
 def correlate(input, weights, output=None, mode="reflect", cval=0.0, origin=0):
     """Multi-dimensional correlate.
@@ -231,7 +234,7 @@ def _correlate_or_convolve1d(
     default_algorithm = False
     if algorithm is None:
         default_algorithm = True
-        if input.ndim == 2 and weights.size <= 256:
+        if _is_not_windows and input.ndim == 2 and weights.size <= 256:
             algorithm = "shared_memory"
         else:
             algorithm = "elementwise"
diff --git a/python/cucim/src/cucim/skimage/_vendored/_ndimage_filters_core.py b/python/cucim/src/cucim/skimage/_vendored/_ndimage_filters_core.py
index 788d34497..be5348da4 100644
--- a/python/cucim/src/cucim/skimage/_vendored/_ndimage_filters_core.py
+++ b/python/cucim/src/cucim/skimage/_vendored/_ndimage_filters_core.py
@@ -3,12 +3,15 @@
 
 import cupy
 import numpy
+from packaging.version import parse
 
 from cucim.skimage._vendored import (
     _internal as internal,
     _ndimage_util as _util,
 )
 
+CUPY_GTE_13_3_0 = parse(cupy.__version__) >= parse("13.3.0")
+
 
 def _origins_to_offsets(origins, w_shape):
     return tuple(x // 2 + o for x, o in zip(w_shape, origins))
@@ -182,13 +185,24 @@ def _call_kernel(
     return output
 
 
-_ndimage_includes = r"""
+if CUPY_GTE_13_3_0:
+    _includes = r"""
+#include <cupy/cuda_workaround.h>  // provide std:: coverage
+"""
+else:
+    _includes = r"""
 #include <type_traits>  // let Jitify handle this
+"""
+
+_ndimage_includes = (
+    _includes
+    + r"""
 #include <cupy/math_constants.h>
 
 template<> struct std::is_floating_point<float16> : std::true_type {};
 template<> struct std::is_signed<float16> : std::true_type {};
 """
+)
 
 
 _ndimage_CAST_FUNCTION = """
@@ -352,7 +366,11 @@ def _generate_nd_kernel(
     if has_mask:
         name += "_with_mask"
     preamble = _ndimage_includes + _ndimage_CAST_FUNCTION + preamble
-    options += ("--std=c++11", "-DCUPY_USE_JITIFY")
+
+    if CUPY_GTE_13_3_0:
+        options += ("--std=c++11",)
+    else:
+        options += ("--std=c++11", "-DCUPY_USE_JITIFY")
     return cupy.ElementwiseKernel(
         in_params,
         out_params,
diff --git a/python/cucim/src/cucim/skimage/_vendored/pad.py b/python/cucim/src/cucim/skimage/_vendored/pad.py
index 249a2c1ed..a3d22a7a0 100644
--- a/python/cucim/src/cucim/skimage/_vendored/pad.py
+++ b/python/cucim/src/cucim/skimage/_vendored/pad.py
@@ -666,9 +666,7 @@ def pad(array, pad_width, mode="constant", **kwargs):
         raise ValueError(f"mode '{mode}' is not supported")
     if unsupported_kwargs:
         raise ValueError(
-            "unsupported keyword arguments for mode '{}': {}".format(
-                mode, unsupported_kwargs
-            )
+            f"unsupported keyword arguments for mode '{mode}': {unsupported_kwargs}"
         )
 
     if _use_elementwise_kernel(array, mode, kwargs):
diff --git a/python/cucim/src/cucim/skimage/_vendored/time.py b/python/cucim/src/cucim/skimage/_vendored/time.py
index 5ec1156ce..e55027212 100644
--- a/python/cucim/src/cucim/skimage/_vendored/time.py
+++ b/python/cucim/src/cucim/skimage/_vendored/time.py
@@ -36,9 +36,7 @@ def _to_str_per_item(device_name, t):
 
         s = f"    {device_name}:{t_us.mean():9.03f} us"
         if t.size > 1:
-            s += "   +/-{:6.03f} (min:{:9.03f} / max:{:9.03f}) us".format(
-                t_us.std(), t_us.min(), t_us.max()
-            )
+            s += f"   +/-{t_us.std():6.03f} (min:{t_us.min():9.03f} / max:{t_us.max():9.03f}) us"
         return s
 
     def to_str(self, show_gpu=False):
diff --git a/python/cucim/src/cucim/skimage/color/colorconv.py b/python/cucim/src/cucim/skimage/color/colorconv.py
index b7d33fc10..903412c95 100644
--- a/python/cucim/src/cucim/skimage/color/colorconv.py
+++ b/python/cucim/src/cucim/skimage/color/colorconv.py
@@ -1121,10 +1121,13 @@ def gray2rgba(image, alpha=None, *, channel_axis=-1, check_alpha=True):
         _, alpha = dtype_limits(image, clip_negative=False)
 
     if np.isscalar(alpha):
+        # Convert to NumPy.  As of NumPy 2.1 (and cupy 13.3) `cp.full`
+        # will raise for out-of-bound Python integers otherwise.
+        alpha = np.asarray(alpha)
         if check_alpha:
             # do not use np.can_cast here for NumPy 2.0 compatibility
             with np.errstate(over="ignore", under="ignore"):
-                alpha_cast = np.asarray(alpha).astype(image.dtype)
+                alpha_cast = alpha.astype(image.dtype)
             if alpha_cast != alpha:
                 warn(
                     'alpha cannot be safely cast to image dtype '
diff --git a/python/cucim/src/cucim/skimage/feature/peak.py b/python/cucim/src/cucim/skimage/feature/peak.py
index 58e0f1899..16c9ea0aa 100644
--- a/python/cucim/src/cucim/skimage/feature/peak.py
+++ b/python/cucim/src/cucim/skimage/feature/peak.py
@@ -31,9 +31,10 @@ def _get_high_intensity_peaks(image, mask, num_peaks, min_distance, p_norm):
     else:
         max_out = None
 
-    coord = ensure_spacing(
-        coord, spacing=min_distance, p_norm=p_norm, max_out=max_out
-    )
+    if min_distance > 1:
+        coord = ensure_spacing(
+            coord, spacing=min_distance, p_norm=p_norm, max_out=max_out
+        )
 
     if len(coord) > num_peaks:
         coord = coord[:num_peaks]
@@ -53,7 +54,7 @@ def _get_peak_mask(image, footprint, threshold, mask=None):
     out = image == image_max
 
     # no peak for a trivial image
-    image_is_trivial = np.all(out) if mask is None else np.all(out[mask])
+    image_is_trivial = cp.all(out) if mask is None else cp.all(out[mask])
     if image_is_trivial:  # synchronize
         out[:] = False
         if mask is not None:
diff --git a/python/cucim/src/cucim/skimage/filters/_separable_filtering.py b/python/cucim/src/cucim/skimage/filters/_separable_filtering.py
index 31effe313..7f2da7cf0 100644
--- a/python/cucim/src/cucim/skimage/filters/_separable_filtering.py
+++ b/python/cucim/src/cucim/skimage/filters/_separable_filtering.py
@@ -5,6 +5,7 @@
 from cucim.skimage._vendored import _ndimage_util as util
 from cucim.skimage._vendored._internal import _normalize_axis_index
 from cucim.skimage._vendored._ndimage_filters_core import (
+    CUPY_GTE_13_3_0,
     _ndimage_CAST_FUNCTION,
     _ndimage_includes,
 )
@@ -917,7 +918,10 @@ def _get_separable_conv_kernel(
         patch_per_block=patch_per_block,
         flip_kernel=flip_kernel,
     )
-    options = ("--std=c++11", "-DCUPY_USE_JITIFY")
+    if CUPY_GTE_13_3_0:
+        options = ("--std=c++11",)
+    else:
+        options = ("--std=c++11", "-DCUPY_USE_JITIFY")
     m = cp.RawModule(code=code, options=options)
     return m.get_function(func_name), block, patch_per_block
 
diff --git a/python/cucim/src/cucim/skimage/measure/_regionprops.py b/python/cucim/src/cucim/skimage/measure/_regionprops.py
index c0b41c691..84badd7a0 100644
--- a/python/cucim/src/cucim/skimage/measure/_regionprops.py
+++ b/python/cucim/src/cucim/skimage/measure/_regionprops.py
@@ -1246,7 +1246,7 @@ def regionprops(
         Centroid coordinate tuple ``(row, col)``, relative to region bounding
         box, weighted with intensity image.
     **coords_scaled** : (K, 2) ndarray
-        Coordinate list ``(row, col)``of the region scaled by ``spacing``.
+        Coordinate list ``(row, col)`` of the region scaled by ``spacing``.
     **coords** : (K, 2) ndarray
         Coordinate list ``(row, col)`` of the region.
     **eccentricity** : float
diff --git a/python/cucim/src/cucim/skimage/metrics/_structural_similarity.py b/python/cucim/src/cucim/skimage/metrics/_structural_similarity.py
index 04ad88ab2..d89e28c70 100644
--- a/python/cucim/src/cucim/skimage/metrics/_structural_similarity.py
+++ b/python/cucim/src/cucim/skimage/metrics/_structural_similarity.py
@@ -269,7 +269,7 @@ def structural_similarity(
         if cp.issubdtype(im1.dtype, cp.integer) and (im1.dtype != cp.uint8):
             warn(
                 "Setting data_range based on im1.dtype. "
-                + ("data_range = %.0f. " % data_range)
+                + f"data_range = {data_range:.0f}. "
                 + "Please specify data_range explicitly to avoid mistakes.",
                 stacklevel=2,
             )
diff --git a/python/cucim/src/cucim/skimage/segmentation/_chan_vese.py b/python/cucim/src/cucim/skimage/segmentation/_chan_vese.py
index ea33f26b5..6827eebdd 100644
--- a/python/cucim/src/cucim/skimage/segmentation/_chan_vese.py
+++ b/python/cucim/src/cucim/skimage/segmentation/_chan_vese.py
@@ -65,7 +65,9 @@ def _fused_variance_kernel2(
     difference_term += term2
 
     new_phi = phi + (dt * delta_phi) * (mu * K + difference_term)
-    out = new_phi / (1 + mu * dt * delta_phi * Csum)
+    out_denom = mu * dt * delta_phi * Csum
+    out_denom += out_denom.dtype.type(1)
+    out = new_phi / out_denom
     return out
 
 
@@ -430,6 +432,9 @@ def chan_vese(
     phivar = tol + 1
 
     dt = cp.asarray(dt, dtype=float_dtype)
+    mu = cp.asarray(mu, dtype=float_dtype)
+    lambda1 = cp.asarray(lambda1, dtype=float_dtype)
+    lambda2 = cp.asarray(lambda2, dtype=float_dtype)
     while phivar > tol and i < max_num_iter:
         # Save old level set values
         oldphi = phi
diff --git a/python/cucim/src/cucim/skimage/transform/_geometric.py b/python/cucim/src/cucim/skimage/transform/_geometric.py
index 25347db26..47cc133b8 100644
--- a/python/cucim/src/cucim/skimage/transform/_geometric.py
+++ b/python/cucim/src/cucim/skimage/transform/_geometric.py
@@ -1936,7 +1936,7 @@ def estimate_transform(ttype, src, dst, *args, **kwargs):
     ttype = ttype.lower()
     if ttype not in TRANSFORMS:
         raise ValueError(
-            "the transformation type '%s' is not" "implemented" % ttype
+            f"the transformation type '{ttype}' is not" "implemented"
         )
 
     tform = TRANSFORMS[ttype](dimensionality=src.shape[1])


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 7205ec4e4..a28f40187 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -16,7 +16,7 @@ on:
 jobs:
   conda-python-tests:
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.10
     with:
       build_type: nightly
       branch: ${{ inputs.branch }}
@@ -24,7 +24,7 @@ jobs:
       sha: ${{ inputs.sha }}
   wheel-tests:
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.08
+    uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.10
     with:
       build_type: nightly
       branch: ${{ inputs.branch }}
diff --git a/3rdparty/LICENSE.pytest-lazy-fixture b/3rdparty/LICENSE.pytest-lazy-fixtures
similarity index 86%
rename from 3rdparty/LICENSE.pytest-lazy-fixture
rename to 3rdparty/LICENSE.pytest-lazy-fixtures
index 6c128cff6..f2c87d0e5 100644
--- a/3rdparty/LICENSE.pytest-lazy-fixture
+++ b/3rdparty/LICENSE.pytest-lazy-fixtures
@@ -1,6 +1,6 @@
-The MIT License (MIT)
+MIT License
 
-Copyright (c) 2016 Marsel Zaripov
+Copyright (c) 2023 Anton Petrov
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:
 
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/ci/test_wheel.sh b/ci/test_wheel.sh
index 6b0f9a10b..70f28c6ad 100755
--- a/ci/test_wheel.sh
+++ b/ci/test_wheel.sh
@@ -11,8 +11,13 @@ python -m pip install $(echo ./dist/cucim*.whl)[test]
 
 CUDA_MAJOR_VERSION=${RAPIDS_CUDA_VERSION:0:2}
 
-DEBIAN_FRONTEND=noninteractive apt update
-DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends libopenslide0
+if type -f yum > /dev/null 2>&1; then
+    yum update -y
+    yum install -y openslide
+else
+    DEBIAN_FRONTEND=noninteractive apt update
+    DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends libopenslide0
+fi
 
 if [[ ${CUDA_MAJOR_VERSION} == "11" ]]; then
     # Omit I/O-related tests in ./python/cucim/tests due to known CUDA bug
diff --git a/python/cucim/src/cucim/skimage/filters/tests/test_separable_filtering.py b/python/cucim/src/cucim/skimage/filters/tests/test_separable_filtering.py
index 026b8c84b..186eb529d 100644
--- a/python/cucim/src/cucim/skimage/filters/tests/test_separable_filtering.py
+++ b/python/cucim/src/cucim/skimage/filters/tests/test_separable_filtering.py
@@ -1,3 +1,5 @@
+import platform
+
 import cupy as cp
 import pytest
 
@@ -227,6 +229,13 @@ def test_separable_image_shapes_and_modes(shape, axis, kernel_size, mode):
 )
 
 
+@pytest.mark.skipif(
+    platform.system() == "Windows",
+    reason=(
+        "custom separable kernels disabled on Windows until dtype-related "
+        "test failures can be resolved"
+    ),
+)
 @pytest.mark.parametrize("axis", (0, 1))
 @pytest.mark.parametrize("image_dtype", image_dtypes_tested)
 @pytest.mark.parametrize(
@@ -246,6 +255,13 @@ def test_separable_image_and_kernel_dtypes(axis, image_dtype, kernel_dtype):
     )
 
 
+@pytest.mark.skipif(
+    platform.system() == "Windows",
+    reason=(
+        "custom separable kernels disabled on Windows until dtype-related "
+        "test failures can be resolved"
+    ),
+)
 @pytest.mark.parametrize("axis", (0, 1))
 @pytest.mark.parametrize("image_dtype", image_dtypes_tested)
 @pytest.mark.parametrize(
diff --git a/python/cucim/src/localtest.py b/python/cucim/src/localtest.py
index f5794dd43..c80949f7b 100644
--- a/python/cucim/src/localtest.py
+++ b/python/cucim/src/localtest.py
@@ -148,7 +148,5 @@ def load_tile_cucim(slide, start_loc, tile_size):
 print("Total time (OpenSlide):", openslide_tot_time)
 print("Total time (cuCIM):", cucim_tot_time)
 print(
-    "Average performance gain (OpenSlide/cuCIM): {}".format(
-        openslide_tot_time / cucim_tot_time
-    )
+    f"Average performance gain (OpenSlide/cuCIM): {openslide_tot_time / cucim_tot_time}"  # noqa: E501
 )
diff --git a/python/cucim/tests/fixtures/testimage.py b/python/cucim/tests/fixtures/testimage.py
index a4a9b9583..f9ef6a7d4 100644
--- a/python/cucim/tests/fixtures/testimage.py
+++ b/python/cucim/tests/fixtures/testimage.py
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2021, NVIDIA CORPORATION.
+# Copyright (c) 2021-2024, NVIDIA CORPORATION.
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at
@@ -16,7 +16,7 @@
 import shutil
 
 import pytest
-from pytest_lazyfixture import lazy_fixture
+from pytest_lazy_fixtures import lf as lazy_fixture
 
 from ..util.gen_image import ImageGenerator
 
diff --git a/python/cucim/tests/performance/clara/test_read_region_memory_usage.py b/python/cucim/tests/performance/clara/test_read_region_memory_usage.py
index 8f259b910..9b17b1549 100644
--- a/python/cucim/tests/performance/clara/test_read_region_memory_usage.py
+++ b/python/cucim/tests/performance/clara/test_read_region_memory_usage.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 #
 
+import cupy as cp
 import pytest
 
 from ...util.io import open_image_cucim
@@ -22,23 +23,24 @@
 
 
 def test_read_region_cuda_memleak(testimg_tiff_stripe_4096x4096_256_jpeg):
-    import GPUtil
-
-    gpus = GPUtil.getGPUs()
-
-    if len(gpus) == 0:
+    def get_used_gpu_memory_mib():
+        """Get the used GPU memory in MiB."""
+        dev = cp.cuda.Device()
+        free, total = dev.mem_info
+        memory_used = (total - free) / (2**20)
+        return memory_used
+
+    num_gpus = cp.cuda.runtime.getDeviceCount()
+    if num_gpus == 0:
         pytest.skip("No gpu available")
 
     img = open_image_cucim(testimg_tiff_stripe_4096x4096_256_jpeg)
 
-    gpu = gpus[0]
-    mem_usage_history = [gpu.memoryUsed]
+    mem_usage_history = [get_used_gpu_memory_mib()]
 
     for i in range(10):
         _ = img.read_region(device="cuda")
-        gpus = GPUtil.getGPUs()
-        gpu = gpus[0]
-        mem_usage_history.append(gpu.memoryUsed)
+        mem_usage_history.append(get_used_gpu_memory_mib())
 
     print(mem_usage_history)