Skip to content

Release v2.5.0 - #1

Merged
MichailSemoglou merged 9 commits into
mainfrom
feature/v2.5.0
Aug 2, 2026
Merged

Release v2.5.0#1
MichailSemoglou merged 9 commits into
mainfrom
feature/v2.5.0

Conversation

@MichailSemoglou

@MichailSemoglou MichailSemoglou commented Aug 2, 2026

Copy link
Copy Markdown
Owner

What's new

Added

  • CVD accessibility dashboard (compute_dashboard, DashboardResult, generate_dashboard_report) for comparing all three deficiencies at once
  • Headless GUI smoke tests (6 tests) runnable with QT_QPA_PLATFORM=offscreen
  • --verbose flag on the CLI parser and each subcommand so tracebacks on errors are opt-in
  • Path-containment regression tests guarding against sibling-prefix bypass, path traversal, and multiple-base-directory selection
  • GitHub Actions CI pipeline: lint (black, isort, ruff), mypy type-check, and test matrix across Python 3.10–3.13
  • Ruff configuration in pyproject.toml
  • PyPI publish workflow via Trusted Publisher (OIDC)
  • Dependabot configuration, issue/PR templates, and community health files

Changed

  • CLI builds kwargs from method.parameters, preventing signature clashes with reference-free methods
  • Version centralized in colorcast/_version.py; pyproject.toml uses dynamic = ["version"] reading from it
  • GPU contract: gpu_histogram_matching collapsed to CPU body; CuPy import warning removed; is_gpu_available() is the public entry point
  • Module-level constants extracted (_EPSILON, _LAB_L_BOUNDS, _LAB_AB_BOUNDS) eliminating 6 copies
  • StyleTransferApp derives settings from ColorCastConfig; file-dialog filters from ALLOWED_IMAGE_EXTENSIONS
  • apply_daltonization refactored with _compute_chromaticity_weight helper
  • validate_and_resize_images return annotation tightened
  • CLI intensity rejects values outside [0, 1]; error exit codes remapped
  • batch.py catches FileNotFoundError and OSError
  • __init__.py re-exports CVD and Daltonization symbols
  • Dev dependencies: dropped pylint and pytest-mock; added imageio

Fixed

  • daltonize(intensity=0) now returns the original image instead of the simulated image
  • selective_color_transfer uses smoothstep masks over a ±0.05 luminance band, eliminating hard seams
  • save_image re-raises with from e to preserve the cause chain
  • show_image copies QImage before QPixmap to prevent GC crashes
  • StyleTransferCache is now thread-safe with threading.Lock
  • validate_file_path uses Path.relative_to instead of str.startswith to reject sibling-prefix bypass
  • _compute_hash uses downsampled fingerprint instead of hashing full image bytes
  • MethodComparison.find_best_method infers metric direction automatically
  • ColorCastConfig.load() validates types; enable_parallel wired to BatchProcessor
  • B904 violations fixed throughout

Removed

  • Dead GPU branch in gpu_histogram_matching
  • ColorCastConfig.cache_size unused field
  • Module-level CuPy import warning
  • requirements.txt and requirements-dev.txt
  • Legacy colorcast.py and colorcast/utils/validators.py

Testing

  • 361 tests pass, 1 skipped (coverage: 75%)
  • Lint and format: ruff, black, isort all clean
  • Build verified with python -m build and twine check

Summary by CodeRabbit

  • New Features

    • Added a CVD accessibility dashboard with simulations, chroma-loss heatmaps, summaries, and report generation.
    • Added GUI actions for dashboards and transfer-method comparisons.
    • Added optional CIEDE2000 chromaticity error metrics.
    • Improved Lab-based Daltonization and intensity controls.
    • Added CLI verbosity, stronger validation, configurable settings, and more reliable caching.
  • Bug Fixes

    • Improved image validation, loading, saving, corrupted-file handling, comparison ranking, and GPU fallback behavior.
  • Documentation

    • Updated 2.5.0 release documentation and added contribution, security, conduct, and issue-reporting guidance.

Set release date to 2026-08-02 in CHANGELOG and CITATION.cff.
Update _version.py docstring to reflect dynamic version in pyproject.toml.
Added:
- CVD accessibility dashboard (colorcast/analysis/dashboard.py)
- Headless GUI smoke tests (tests/test_gui.py)
- --verbose flag on CLI with opt-in tracebacks
- Path-containment regression tests (tests/test_validators_enhanced.py)
- Ruff configuration in pyproject.toml
- Daltonization efficacy script (scripts/)

Changed:
- CLI builds kwargs from method.parameters, preventing signature clashes
- Version centralized in _version.py; pyproject.toml uses dynamic version
- GPU contract: gpu_histogram_matching CPU-only, CuPy warning removed
- extract module-level constants (_EPSILON, _LAB_L_BOUNDS, _LAB_AB_BOUNDS)
- StyleTransferApp derives settings from ColorCastConfig
- apply_daltonization refactored with _compute_chromaticity_weight helper
- validate_and_resize_images return annotation tightened
- CLI intensity now validates bounds; error exit codes remapped
- batch.py catches FileNotFoundError and OSError
- __init__.py re-exports CVD and Daltonization symbols

Fixed:
- daltonize(intensity=0) returns original image instead of simulated
- selective_color_transfer uses smoothstep masks (no hard edges)
- save_image re-raises with from e to preserve cause chain
- show_image copies QImage before QPixmap to prevent GC crashes
- StyleTransferCache is now thread-safe with threading.Lock
- validate_file_path rejects sibling-prefix bypass
- _compute_hash uses downsampled fingerprint instead of full image bytes
- MethodComparison.find_best_method infers metric direction
- ColorCastConfig.load() validates types; enable_parallel wired
- B904 violations fixed: raise ... from e

Removed:
- Dead GPU round-trip in gpu_histogram_matching
- ColorCastConfig.cache_size unused field
- Module-level CuPy import warning
- requirements.txt, requirements-dev.txt
- Legacy colorcast.py
Add GitHub Actions CI workflow (lint, mypy, test matrix across Python 3.10-3.13).
Add PyPI publish workflow via Trusted Publisher (OIDC).
Add Dependabot configuration for pip and GitHub Actions.
Add issue/PR templates, CODE_OF_CONDUCT, CONTRIBUTING, and SECURITY.
Update test metrics to match current suite (75% coverage, 359 passed, 1 skipped).
Add CVD dashboard, method comparison, and dashboard report screenshots.
Rewrite Use Cases section to comply with project writing standards.
Add tests/ to lint commands in Contributing section.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

ColorCast 2.5.0 adds CVD accessibility analysis, Lab-space Daltonization, dashboard and comparison dialogs, centralized configuration and versioning, improved CLI validation, cache changes, image validation, CI automation, documentation, and expanded tests.

Changes

ColorCast 2.5.0 release

Layer / File(s) Summary
Project foundation and release metadata
.github/*, CHANGELOG.md, CITATION.cff, CODE_OF_CONDUCT.md, CONTRIBUTING.md, MANIFEST.in, README.md, SECURITY.md, docs/*, pyproject.toml, scripts/*
Project workflows, release metadata, packaging rules, contributor documents, security policy, documentation, and efficacy tooling are updated for version 2.5.0.
Public APIs, CLI, configuration, and validation
colorcast/__init__.py, colorcast/_version.py, colorcast/__main__.py, colorcast/utils/*
The package exports new analysis APIs and uses centralized version metadata. CLI validation, error reporting, configuration persistence, and image validation are updated.
Accessibility analysis and comparison
colorcast/analysis/*
Daltonization now uses Lab-space correction. Error maps support optional CIEDE2000 values. Dashboard computation, summaries, reports, visualization, and comparison handling are added or updated.
Processing, caching, and image handling
colorcast/processing/*
Transfer helpers share numerical constants. Cache keys hash complete image data and isolate array copies. GPU fallback, image loading, batch processing, and registry caching are updated.
Configurable GUI and asynchronous dialogs
colorcast/gui.py
The GUI uses ColorCastConfig, cached transfers, asynchronous dashboard computation, method comparison, image previews, summaries, and report export.
Validation and regression coverage
tests/*
Tests cover the new dashboard, GUI, cache, CLI, configuration, validator, GPU, Daltonization, and processing behavior. Existing integration and property-based tests are updated for the new APIs.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant StyleTransferApp
  participant DashboardDialog
  participant compute_dashboard
  participant generate_dashboard_report
  User->>StyleTransferApp: Select Dashboard
  StyleTransferApp->>DashboardDialog: Open with content image
  DashboardDialog->>compute_dashboard: Run asynchronous analysis
  compute_dashboard-->>DashboardDialog: Return DashboardResult
  User->>DashboardDialog: Export report
  DashboardDialog->>generate_dashboard_report: Save report
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the pull request as the v2.5.0 release, which matches the main changeset objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/v2.5.0

Comment @coderabbitai help to get the list of available commands.

Bump mypy target from 3.10 to 3.12 to match CI interpreter.
Fix pre-existing type errors in comparison, validators, image_loader,
dashboard, and daltonization modules. Suppress PyQt5 stub errors in gui.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
colorcast/analysis/comparison.py (1)

252-262: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the identity exclusion in rank_methods.

The new filter drops every method whose color_distance is close to zero, for all values of primary_metric. generate_comparison_report Lines 309-318 calls rank_methods for each metric, so such a method appears in the metrics table but in none of the ranking sections. Only find_best_method documents this rule at Lines 330-334. Add the same statement to the rank_methods docstring, because rank_methods is a public method.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/analysis/comparison.py` around lines 252 - 262, Update the public
rank_methods docstring to document that methods with color_distance close to
zero are excluded from rankings for every primary_metric, matching the existing
behavior and the explanation in find_best_method. Keep the filtering logic
unchanged.
colorcast/utils/validators_enhanced.py (1)

224-242: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject unsupported formats in the Pillow fallback path too.

The imghdr branch (lines 255-259) rejects a file when expected_ext is empty, treating it as an unsupported type. The Pillow fallback branch (used when imghdr is unavailable, i.e. Python 3.13+) does not perform this check: it calls _check_extension_matches(expected_ext, path_ext, fmt or "unknown") and returns unconditionally. _check_extension_matches returns silently when detected_ext is empty, so a file whose PIL-detected format isn't in _PIL_FORMAT_TO_EXT (for example ICO, PSD, or MPO) passes validation as long as its extension is one of the allowed ones. This bypasses the file-type-spoofing protection this function documents, specifically on the Python version this fallback targets.

Add the same unsupported-type check to the Pillow branch.

🛡️ Proposed fix to reject unmapped formats in the Pillow branch
         path_ext = path_obj.suffix.lower()
         expected_ext = _PIL_FORMAT_TO_EXT.get(fmt or "")
         _check_extension_matches(expected_ext, path_ext, fmt or "unknown")
+        if not expected_ext:
+            raise ValidationError(
+                f"Unsupported image type: {fmt or 'unknown'}. "
+                f"Supported types: JPEG, PNG, BMP, TIFF, GIF, WebP"
+            )
         return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/utils/validators_enhanced.py` around lines 224 - 242, In the Pillow
fallback branch, update the logic after deriving expected_ext from
_PIL_FORMAT_TO_EXT to reject formats whose mapping is missing, matching the
imghdr branch’s unsupported-type behavior. Perform this check before
_check_extension_matches and return only for supported mapped formats.
🧹 Nitpick comments (12)
colorcast/analysis/dashboard.py (1)

23-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Export the deficiency constants publicly.

_DEFICIENCIES and _DEFICIENCY_LABELS use the private naming convention, but colorcast/gui.py Lines 28-33 imports both across module boundaries. Rename them to DEFICIENCIES and DEFICIENCY_LABELS, and list them in the module docstring "Public API" section. This makes the dependency explicit and prevents a later rename from breaking the GUI silently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/analysis/dashboard.py` around lines 23 - 28, Rename _DEFICIENCIES
and _DEFICIENCY_LABELS to the public names DEFICIENCIES and DEFICIENCY_LABELS,
update all references including the imports in colorcast/gui.py, and add both
names to the module docstring’s Public API section.
colorcast/analysis/visualization.py (1)

188-208: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the new histogram rows.

show_histograms defaults to False in visualize_method_comparison, and the calls in tests/test_visualization.py Lines 129-148 never set it to True. The new row-offset arithmetic at Line 193 and the per-method axes indexing at Lines 202-208 are therefore not exercised. Add a test that calls visualize_method_comparison(images, reference, show_histograms=True) for both values of show_difference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/analysis/visualization.py` around lines 188 - 208, Add coverage in
tests/test_visualization.py by calling visualize_method_comparison with
show_histograms=True for both show_difference=True and show_difference=False.
Ensure the assertions exercise the histogram row offset and per-method axes
indexing implemented in the show_histograms block.
colorcast/analysis/comparison.py (1)

130-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reconcile the metric value types.

_make_metrics returns dict[str, float | str] because of the _error key. compare_methods is annotated -> dict[str, dict[str, float]] and stores those dicts in results, which is declared as dict[str, dict[str, float]] at Line 201. A static type checker reports an incompatible assignment here.

Widen the compare_methods return type and the results declaration to dict[str, dict[str, float | str]], and widen the rank_methods, generate_comparison_report, and find_best_method parameters in the same way.

Also applies to: 205-218

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/analysis/comparison.py` around lines 130 - 136, Reconcile metric
container annotations by widening compare_methods’ return type, its results
declaration, and the parameter types of rank_methods,
generate_comparison_report, and find_best_method to dict[str, dict[str, float |
str]], matching _make_metrics and allowing the _error string value.
colorcast/analysis/daltonization.py (1)

108-112: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicated section header.

Lines 108-112 contain two stacked banner comments. Convenience end-to-end pipeline is repeated at lines 198-200 above daltonize, where it belongs. Keep only Core function here.

♻️ Proposed cleanup
-# ---------------------------------------------------------------------------
-# Convenience end-to-end pipeline
-# ---------------------------------------------------------------------------
 # Core function
 # ---------------------------------------------------------------------------
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/analysis/daltonization.py` around lines 108 - 112, Remove the
duplicated “Convenience end-to-end pipeline” banner from the section around
“Core function” in daltonization.py, leaving only the “Core function” header
there; retain the separate convenience-pipeline header above daltonize.
colorcast/gui.py (2)

268-297: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Constrain the image_type role.

image_type is a plain str. Line 288 treats every value other than "content" as the style role, so a typo assigns the image to self.style_image without any error. Annotate the parameter as Literal["content", "style"] so a type checker rejects other values.

♻️ Proposed change
+from typing import Literal
+
     def _load_image_file(
         self,
         path: str,
-        image_type: str,
+        image_type: Literal["content", "style"],
         preview_label: QLabel,
     ) -> None:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/gui.py` around lines 268 - 297, Constrain the _load_image_file
image_type parameter to Literal["content", "style"] instead of plain str, adding
the necessary typing import. Preserve the existing content/style branching while
enabling type checkers to reject invalid role values.

747-758: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the cached transfer and record the failure reason.

Two points:

  1. Line 752 calls instance.transfer directly. apply_style_transfer Line 416 calls registry.transfer_cached. The comparison dialog therefore recomputes every method on each open and does not populate the shared cache. Use registry.transfer_cached(method_id, self._content_image, self._style_image) for consistent behavior.
  2. Line 756 logs at debug level without the exception. A user sees only a reduced count in the status text at Line 776 and has no way to find the cause. Use logger.exception and keep the method name.
♻️ Proposed change
         def _run() -> None:
             for method_id in self._label_widgets:
                 try:
-                    instance = registry.get_method(method_id)
-                    self._results[method_id] = instance.transfer(
-                        self._content_image, self._style_image
+                    self._results[method_id] = registry.transfer_cached(
+                        method_id, self._content_image, self._style_image
                     )
-                except Exception:  # noqa: BLE001 — skip unsupported methods
-                    logger.debug("Skipped method %s in comparison", method_id)
+                except Exception:  # noqa: BLE001 — skip unsupported methods
+                    logger.exception("Skipped method %s in comparison", method_id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/gui.py` around lines 747 - 758, Update _start_computation’s _run
loop to call registry.transfer_cached(method_id, self._content_image,
self._style_image) instead of instance.transfer, preserving shared-cache
behavior. In the exception handler, replace the debug log with logger.exception
while retaining the method identifier so the failure reason and affected method
are recorded.
pyproject.toml (1)

47-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider consolidating overlapping lint/format tools.

Ruff's lint selection now includes "I" (isort rules) at line 106, but isort and black remain separate dev dependencies at lines 48-49, and the PR checklist still requires running isort --check-only separately. Running three tools for overlapping concerns (import sorting, formatting) can produce conflicting fixes over time.

Consider standardizing on ruff format and Ruff's I rules, and dropping black/isort once CI is updated accordingly.

Also applies to: 105-106

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` around lines 47 - 57, Consolidate the formatting and
import-sorting tooling by removing the black and isort development dependencies,
standardizing on ruff format and the existing Ruff I lint rules in the Ruff
configuration, and updating CI and the PR checklist to stop invoking isort or
black separately.
.github/workflows/ci.yml (1)

16-19: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Pin all GitHub Actions to immutable commits.

All action references in .github/workflows/ci.yml and .github/workflows/publish.yml use mutable tags or branches. Pin every action to a full commit SHA. Keep the GitHub Actions Dependabot configuration so the pins remain current.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 16 - 19, Update every action reference
in the workflows’ uses entries, including actions/checkout,
actions/setup-python, and all actions in ci.yml and publish.yml, to a full
immutable commit SHA instead of a tag or branch. Preserve the existing GitHub
Actions Dependabot configuration so these pinned revisions continue receiving
updates.
colorcast/processing/gpu_transfer.py (2)

37-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer CuPy's built-in availability check.

is_gpu_available() reimplements cupy.is_available(). That built-in function performs the driver query and device-count check this code hand-rolls. Use it instead of a custom implementation to reduce maintenance and picking up CuPy's own edge-case handling (for example, driver-initialization failures beyond CUDARuntimeError).

♻️ Proposed refactor
 def is_gpu_available() -> bool:
     """Check whether CuPy is installed and a usable CUDA device is present."""
     if not HAS_CUPY:
         return False
-    try:
-        return cp.cuda.runtime.getDeviceCount() > 0
-    except cp.cuda.runtime.CUDARuntimeError:
-        return False
+    return cp.is_available()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/processing/gpu_transfer.py` around lines 37 - 44, Update
is_gpu_available() to use CuPy’s built-in cp.is_available() result instead of
manually checking HAS_CUPY and querying getDeviceCount(), while preserving the
false result when CuPy is unavailable.

19-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cross-module import of underscore-prefixed "private" symbols.

This module imports _EPSILON, _LAB_AB_BOUNDS, _LAB_L_BOUNDS, and _meanstd_transfer from colorcast.processing.transfer_methods. The leading underscore in Python signals module-private, but these symbols are now part of a cross-module contract between gpu_transfer.py and transfer_methods.py. A future contributor could reasonably rename or inline these thinking they are private to transfer_methods.py, silently breaking gpu_transfer.py.

Rename these without the leading underscore (or move them into a small shared internal module) to make the cross-module dependency explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/processing/gpu_transfer.py` around lines 19 - 25, Make the
cross-module dependencies imported by gpu_transfer explicit by renaming
_EPSILON, _LAB_AB_BOUNDS, _LAB_L_BOUNDS, and _meanstd_transfer in
transfer_methods and updating every reference, including gpu_transfer, to their
non-underscore names; keep validate_and_resize_images unchanged.
colorcast/processing/transfer_methods.py (1)

16-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docstring's dtype claim does not hold for the Lab call site.

The docstring for _meanstd_transfer states that both arrays "must be three-channel float32 images." color_transfer_lab (Line 225) and the Lab fallback in colorcast/processing/gpu_transfer.py call this function with float64 Lab arrays from color.rgb2lab. NumPy does not enforce the claim at runtime, so nothing breaks today, but the inaccurate docstring could mislead a future contributor into adding a float32 assertion that would break the Lab callers.

📝 Proposed docstring fix
     """
     Per-channel mean and standard deviation transfer.

     The caller is responsible for validating and resizing inputs before
-    calling this function.  Both arrays must be three-channel float32
-    images with identical spatial dimensions.
+    calling this function.  Both arrays must be three-channel arrays
+    (float32 RGB or float64 Lab) with identical spatial dimensions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/processing/transfer_methods.py` around lines 16 - 42, Update the
_meanstd_transfer docstring to remove the incorrect requirement that inputs be
float32, and describe them as three-channel floating-point images while
preserving the existing shape and spatial-dimension requirements. Do not add
runtime validation or alter the transfer logic or Lab call sites.
colorcast/processing/cache.py (1)

91-109: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Full-image MD5 hashing on every cache operation adds CPU and memory cost for large images.

_compute_hash now hashes the complete contiguous byte buffer of the image, instead of a sampled subset. image_loader.py allows images up to 50 megapixels. Hashing a large float32 array with MD5 costs measurable CPU time and a temporary byte-buffer copy from tobytes(). StyleTransferCache.generate_key()/get()/set() run this hash on the content image and, when present, the style image on every call. If this cache backs interactive operations (for example, a GUI slider that triggers repeated transfers), the added latency accumulates per interaction.

Consider one of these approaches if this proves to be a bottleneck:

  • Use a faster non-cryptographic hash (for example xxhash or hashlib.blake2b with a small digest size) since collision resistance against adversarial input is not a requirement here.
  • Keep a fast structural fingerprint (shape, dtype, strides) plus a cheap running checksum over strided samples, and fall back to full-content hashing only when the structural fingerprint matches, to avoid a full false-positive cache hit while limiting the common-case cost.

This is a deliberate correctness fix over the old sampled hash, so the cost may be an accepted trade-off. Confirm this is acceptable for the largest images the caching path is expected to process.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/processing/cache.py` around lines 91 - 109, Validate the
performance impact of the full-image hashing implemented by _compute_hash for
the largest images supported by the cache, including repeated generate_key, get,
and set calls with content and style images. If the cost is unacceptable,
replace MD5 with a faster suitable hash or implement a structural fingerprint
with sampled checks and full-content fallback while preserving collision-safe
cache correctness; otherwise retain the current full-content behavior and
document the accepted trade-off.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 44: Update the 2.5.0 test metrics in CHANGELOG.md from 359 passed to the
authoritative 361 passed while retaining 1 skipped and 75% coverage, and ensure
the corresponding README metrics use the same counts.

In `@colorcast/__init__.py`:
- Around line 26-38: Remove the eager analysis imports from the package
initializer, especially MethodComparison, daltonize, and ErrorMap/get_error_map,
so importing colorcast does not initialize colorcast.analysis or require
optional matplotlib. Preserve access to these analysis APIs through lazy imports
or their existing submodules without promoting matplotlib to a base dependency.

In `@colorcast/analysis/daltonization.py`:
- Around line 15-29: Update the Lab-space correction documentation in
apply_daltonization and its repeated description to state that base_lab[:, :, 0]
is preserved, retaining the base image’s L* value rather than restoring the
original image’s lightness. Also revise the ErrorMap.orig_l_star docstring to
remove the claim that apply_daltonization uses it to restore original luminance.

In `@colorcast/analysis/dashboard.py`:
- Around line 54-61: Normalize image_array to float32 in the [0, 1] range inside
compute_dashboard before assigning it to DashboardResult.original, using the
same normalization behavior as ColorBlindSimulator.transform_color_space. Keep
the normalized original separate from the raw input used for simulation if
needed, and preserve the existing result structure and parallel processing.

In `@colorcast/analysis/error_map.py`:
- Line 93: Update the exported ErrorMap NamedTuple definition to keep its
existing positional fields and indexes unchanged; do not add chroma_error_dE00
as a required tuple field. Store the optional dE00 metric outside the positional
tuple contract while preserving compute_dE00=True behavior for the production
reader.

In `@colorcast/gui.py`:
- Around line 71-85: Update the nested _worker function in _run_in_thread so
signals.finished.emit() executes in a finally block around target(), ensuring
completion callbacks run even when the target raises while preserving normal
exception propagation.

In `@docs/index.rst`:
- Around line 41-44: Update the docs/index.rst toctree so the existing
Color-Vision-Background documentation is included using a Sphinx-supported .rst
page, or remove the orphaned docs/wiki/Color-Vision-Background.md file if it
should not be published; do not leave the Markdown page omitted from the
documentation build.

In `@README.md`:
- Line 20: Update the README test-count references at both locations, changing
359 to 361 while preserving the existing “1 skipped” text at the second
location.

In `@SECURITY.md`:
- Around line 20-28: Update the Supported Versions table in SECURITY.md to mark
2.5.x as supported and change the unsupported version entry from < 2.4 to < 2.5,
keeping the existing formatting and support markers.

In `@tests/test_visualization.py`:
- Around line 165-167: Update test_custom_title to capture the result of
visualize_color_channels(image, title="Custom Title") and assert it is an
instance of Figure, matching the return-value checks in test_returns_figure and
test_custom_figsize.

---

Outside diff comments:
In `@colorcast/analysis/comparison.py`:
- Around line 252-262: Update the public rank_methods docstring to document that
methods with color_distance close to zero are excluded from rankings for every
primary_metric, matching the existing behavior and the explanation in
find_best_method. Keep the filtering logic unchanged.

In `@colorcast/utils/validators_enhanced.py`:
- Around line 224-242: In the Pillow fallback branch, update the logic after
deriving expected_ext from _PIL_FORMAT_TO_EXT to reject formats whose mapping is
missing, matching the imghdr branch’s unsupported-type behavior. Perform this
check before _check_extension_matches and return only for supported mapped
formats.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 16-19: Update every action reference in the workflows’ uses
entries, including actions/checkout, actions/setup-python, and all actions in
ci.yml and publish.yml, to a full immutable commit SHA instead of a tag or
branch. Preserve the existing GitHub Actions Dependabot configuration so these
pinned revisions continue receiving updates.

In `@colorcast/analysis/comparison.py`:
- Around line 130-136: Reconcile metric container annotations by widening
compare_methods’ return type, its results declaration, and the parameter types
of rank_methods, generate_comparison_report, and find_best_method to dict[str,
dict[str, float | str]], matching _make_metrics and allowing the _error string
value.

In `@colorcast/analysis/daltonization.py`:
- Around line 108-112: Remove the duplicated “Convenience end-to-end pipeline”
banner from the section around “Core function” in daltonization.py, leaving only
the “Core function” header there; retain the separate convenience-pipeline
header above daltonize.

In `@colorcast/analysis/dashboard.py`:
- Around line 23-28: Rename _DEFICIENCIES and _DEFICIENCY_LABELS to the public
names DEFICIENCIES and DEFICIENCY_LABELS, update all references including the
imports in colorcast/gui.py, and add both names to the module docstring’s Public
API section.

In `@colorcast/analysis/visualization.py`:
- Around line 188-208: Add coverage in tests/test_visualization.py by calling
visualize_method_comparison with show_histograms=True for both
show_difference=True and show_difference=False. Ensure the assertions exercise
the histogram row offset and per-method axes indexing implemented in the
show_histograms block.

In `@colorcast/gui.py`:
- Around line 268-297: Constrain the _load_image_file image_type parameter to
Literal["content", "style"] instead of plain str, adding the necessary typing
import. Preserve the existing content/style branching while enabling type
checkers to reject invalid role values.
- Around line 747-758: Update _start_computation’s _run loop to call
registry.transfer_cached(method_id, self._content_image, self._style_image)
instead of instance.transfer, preserving shared-cache behavior. In the exception
handler, replace the debug log with logger.exception while retaining the method
identifier so the failure reason and affected method are recorded.

In `@colorcast/processing/cache.py`:
- Around line 91-109: Validate the performance impact of the full-image hashing
implemented by _compute_hash for the largest images supported by the cache,
including repeated generate_key, get, and set calls with content and style
images. If the cost is unacceptable, replace MD5 with a faster suitable hash or
implement a structural fingerprint with sampled checks and full-content fallback
while preserving collision-safe cache correctness; otherwise retain the current
full-content behavior and document the accepted trade-off.

In `@colorcast/processing/gpu_transfer.py`:
- Around line 37-44: Update is_gpu_available() to use CuPy’s built-in
cp.is_available() result instead of manually checking HAS_CUPY and querying
getDeviceCount(), while preserving the false result when CuPy is unavailable.
- Around line 19-25: Make the cross-module dependencies imported by gpu_transfer
explicit by renaming _EPSILON, _LAB_AB_BOUNDS, _LAB_L_BOUNDS, and
_meanstd_transfer in transfer_methods and updating every reference, including
gpu_transfer, to their non-underscore names; keep validate_and_resize_images
unchanged.

In `@colorcast/processing/transfer_methods.py`:
- Around line 16-42: Update the _meanstd_transfer docstring to remove the
incorrect requirement that inputs be float32, and describe them as three-channel
floating-point images while preserving the existing shape and spatial-dimension
requirements. Do not add runtime validation or alter the transfer logic or Lab
call sites.

In `@pyproject.toml`:
- Around line 47-57: Consolidate the formatting and import-sorting tooling by
removing the black and isort development dependencies, standardizing on ruff
format and the existing Ruff I lint rules in the Ruff configuration, and
updating CI and the PR checklist to stop invoking isort or black separately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6ef60f84-c4a2-4ad3-a941-6299041d5353

📥 Commits

Reviewing files that changed from the base of the PR and between 7324cd5 and 31fdf37.

⛔ Files ignored due to path filters (4)
  • imgs/CVD-Accessibility-Dashboard.png is excluded by !**/*.png
  • imgs/Compare-Transfer-Methods.png is excluded by !**/*.png
  • imgs/dashboard_report.png is excluded by !**/*.png
  • imgs/interface.png is excluded by !**/*.png
📒 Files selected for processing (68)
  • .github/ISSUE_TEMPLATE/bug_report.md
  • .github/ISSUE_TEMPLATE/feature_request.md
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/dependabot.yml
  • .github/workflows/ci.yml
  • .gitignore
  • .zenodo.json
  • CHANGELOG.md
  • CITATION.cff
  • CODE_OF_CONDUCT.md
  • CONTRIBUTING.md
  • MANIFEST.in
  • README.md
  • SECURITY.md
  • colorcast.py
  • colorcast/__init__.py
  • colorcast/__main__.py
  • colorcast/_version.py
  • colorcast/analysis/__init__.py
  • colorcast/analysis/comparison.py
  • colorcast/analysis/daltonization.py
  • colorcast/analysis/dashboard.py
  • colorcast/analysis/error_map.py
  • colorcast/analysis/visualization.py
  • colorcast/gui.py
  • colorcast/processing/__init__.py
  • colorcast/processing/batch.py
  • colorcast/processing/blending.py
  • colorcast/processing/cache.py
  • colorcast/processing/curves.py
  • colorcast/processing/gpu_transfer.py
  • colorcast/processing/image_loader.py
  • colorcast/processing/registry.py
  • colorcast/processing/simulation.py
  • colorcast/processing/transfer_methods.py
  • colorcast/utils/__init__.py
  • colorcast/utils/config.py
  • colorcast/utils/exceptions.py
  • colorcast/utils/validators.py
  • colorcast/utils/validators_enhanced.py
  • docs/conf.py
  • docs/index.rst
  • pyproject.toml
  • requirements-dev.txt
  • requirements.txt
  • scripts/daltonization_efficacy.py
  • tests/__init__.py
  • tests/test_batch.py
  • tests/test_blending.py
  • tests/test_cache.py
  • tests/test_cli.py
  • tests/test_color_blindness.py
  • tests/test_comparison.py
  • tests/test_config.py
  • tests/test_curves.py
  • tests/test_entry_points.py
  • tests/test_gpu_transfer.py
  • tests/test_gui.py
  • tests/test_image_loading.py
  • tests/test_integration.py
  • tests/test_integration_comprehensive.py
  • tests/test_lab_transfer.py
  • tests/test_performance.py
  • tests/test_property_based.py
  • tests/test_registry.py
  • tests/test_transfer_methods.py
  • tests/test_validators_enhanced.py
  • tests/test_visualization.py
💤 Files with no reviewable changes (4)
  • requirements-dev.txt
  • colorcast/utils/validators.py
  • requirements.txt
  • colorcast.py

Comment thread CHANGELOG.md Outdated
Comment thread colorcast/__init__.py Outdated
Comment thread colorcast/analysis/daltonization.py Outdated
Comment on lines +54 to +61
def compute_dashboard(
image_array: np.ndarray,
max_workers: int | None = None,
) -> DashboardResult:
"""Run all three CVD simulations and error maps in parallel.

Args:
image_array: Source image, any numeric dtype, shape (H, W, 3).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Normalize original before storing it in the result.

Line 61 documents that image_array accepts any numeric dtype, and ColorBlindSimulator.transform_color_space normalizes internally. Line 90 stores the raw input. DashboardResult.original is documented at Lines 37-38 as float32 in [0, 1].

Both renderers then treat the field as float in [0, 1]:

  • generate_dashboard_report Line 166 applies np.clip(result.original, 0, 1).
  • colorcast/gui.py Line 52 applies the same clip in _array_to_pixmap.

For a uint8 input every sample above 1 clamps to 1, so the "Original" panel renders as a saturated image while the simulated panels render correctly. Normalize once in compute_dashboard.

🐛 Proposed fix
+from colorcast.processing.image_loader import normalize_to_float32
 from colorcast.processing.simulation import ColorBlindSimulator
     simulator = ColorBlindSimulator()
+    original = normalize_to_float32(image_array)
 
     def _simulate_and_map(deficiency: str) -> tuple[str, np.ndarray, ErrorMap, dict[str, float]]:
-        sim = simulator.transform_color_space(image_array, deficiency)
-        em = get_error_map(image_array, sim, compute_dE00=True)
+        sim = simulator.transform_color_space(original, deficiency)
+        em = get_error_map(original, sim, compute_dE00=True)
     return DashboardResult(
-        original=image_array,
+        original=original,

Also applies to: 89-94

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/analysis/dashboard.py` around lines 54 - 61, Normalize image_array
to float32 in the [0, 1] range inside compute_dashboard before assigning it to
DashboardResult.original, using the same normalization behavior as
ColorBlindSimulator.transform_color_space. Keep the normalized original separate
from the raw input used for simulation if needed, and preserve the existing
result structure and parallel processing.

Comment thread colorcast/analysis/error_map.py Outdated
signed: np.ndarray
absolute: np.ndarray
chroma_error: np.ndarray
chroma_error_dE00: np.ndarray

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find positional ErrorMap construction/unpacking and readers of chroma_error_dE00.
set -euo pipefail

rg -n -C 4 'ErrorMap\(' --type=py
rg -n -C 4 'chroma_error_dE00' --type=py
rg -n -C 3 'get_error_map\(' --type=py

Repository: MichailSemoglou/ColorCast

Length of output: 163


🏁 Script executed:

set -euo pipefail
printf '%s\n' '--- tracked Python files ---'
git ls-files '*.py' | sed -n '1,160p'
printf '%s\n' '--- target file ---'
target=$(git ls-files | awk '$0=="colorcast/analysis/error_map.py"{print; exit}')
if [ -n "$target" ]; then
  nl -ba "$target" | sed -n '1,280p'
else
  printf '%s\n' 'target not found'
fi
printf '%s\n' '--- ErrorMap symbols and consumers ---'
rg -n -C 5 'ErrorMap|chroma_error_dE00|get_error_map' . --glob '*.py' || true

Repository: MichailSemoglou/ColorCast

Length of output: 1605


🏁 Script executed:

set -euo pipefail
printf '%s\n' '--- colorcast/analysis/error_map.py ---'
awk '{printf "%6d\t%s\n", NR, $0}' colorcast/analysis/error_map.py | sed -n '1,280p'
printf '%s\n' '--- symbol and call-site search ---'
rg -n -C 5 'ErrorMap|chroma_error_dE00|get_error_map' colorcast tests scripts --glob '*.py' || true

Repository: MichailSemoglou/ColorCast

Length of output: 50381


🏁 Script executed:

set -euo pipefail
python3 - <<'PY'
import ast
import subprocess
from pathlib import Path

files = [Path(p) for p in subprocess.check_output(
    ["git", "ls-files", "*.py"], text=True
).splitlines()]

def dotted_name(node):
    parts = []
    while isinstance(node, ast.Attribute):
        parts.append(node.attr)
        node = node.value
    if isinstance(node, ast.Name):
        parts.append(node.id)
        return ".".join(reversed(parts))
    return None

print("--- ErrorMap calls with positional arguments ---")
for path in files:
    tree = ast.parse(path.read_text(), filename=str(path))
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            name = dotted_name(node.func) or ""
            if name == "ErrorMap" or name.endswith(".ErrorMap"):
                positional = len(node.args)
                keywords = [kw.arg for kw in node.keywords if kw.arg is not None]
                if positional or not keywords:
                    print(f"{path}:{node.lineno}: positional={positional}, keywords={keywords}")

print("--- get_error_map calls and compute_dE00 values ---")
for path in files:
    tree = ast.parse(path.read_text(), filename=str(path))
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            name = dotted_name(node.func) or ""
            if name == "get_error_map" or name.endswith(".get_error_map"):
                positional = len(node.args)
                kw = {item.arg: ast.unparse(item.value) for item in node.keywords if item.arg}
                print(f"{path}:{node.lineno}: positional={positional}, keywords={kw}")

print("--- direct chroma_error_dE00 readers ---")
for path in files:
    tree = ast.parse(path.read_text(), filename=str(path))
    for node in ast.walk(tree):
        if isinstance(node, ast.Attribute) and node.attr == "chroma_error_dE00":
            print(f"{path}:{node.lineno}: {ast.unparse(node)}")

print("--- tuple unpack assignments fed directly by get_error_map ---")
for path in files:
    tree = ast.parse(path.read_text(), filename=str(path))
    for node in ast.walk(tree):
        if isinstance(node, (ast.Assign, ast.AnnAssign)):
            value = node.value
            name = dotted_name(value.func) if isinstance(value, ast.Call) else None
            if name == "get_error_map" or (name and name.endswith(".get_error_map")):
                targets = node.targets if isinstance(node, ast.Assign) else [node.target]
                for target in targets:
                    if isinstance(target, (ast.Tuple, ast.List)):
                        print(f"{path}:{node.lineno}: target={ast.unparse(target)}")
PY
printf '%s\n' '--- exact field references ---'
rg -n 'chroma_error_dE00' --glob '*.py' colorcast tests scripts || true
printf '%s\n' '--- ErrorMap constructor references ---'
rg -n -C 3 'ErrorMap\(' --glob '*.py' colorcast tests scripts || true

Repository: MichailSemoglou/ColorCast

Length of output: 5391


🏁 Script executed:

set -euo pipefail
printf '%s\n' '--- previous ErrorMap field order ---'
if git rev-parse --verify HEAD^ >/dev/null 2>&1; then
  git show HEAD^:colorcast/analysis/error_map.py 2>/dev/null |
    awk '/class ErrorMap\(NamedTuple\)/,/^$/' |
    grep -E '^[[:space:]]+[A-Za-z_][A-Za-z0-9_]*:'
else
  printf '%s\n' 'no parent commit available'
fi
printf '%s\n' '--- possible indexed ErrorMap access ---'
rg -n -C 2 '(error_map|error_maps|em|ErrorMap)[[:space:]]*\[' colorcast tests scripts --glob '*.py' || true
printf '%s\n' '--- public exports ---'
rg -n -C 2 'ErrorMap' colorcast/__init__.py colorcast/analysis/__init__.py

Repository: MichailSemoglou/ColorCast

Length of output: 201


Preserve the public ErrorMap positional contract.

ErrorMap is an exported NamedTuple. This required field changes positional indexes and breaks the previous positional shape for external callers. No repository code constructs or unpacks ErrorMap positionally. The only production reader passes compute_dE00=True; default callers do not read this field. Keep the optional metric outside the existing positional tuple contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/analysis/error_map.py` at line 93, Update the exported ErrorMap
NamedTuple definition to keep its existing positional fields and indexes
unchanged; do not add chroma_error_dE00 as a required tuple field. Store the
optional dE00 metric outside the positional tuple contract while preserving
compute_dE00=True behavior for the production reader.

Comment thread docs/index.rst
Comment thread README.md Outdated
Comment thread SECURITY.md Outdated
Comment thread tests/test_visualization.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
pyproject.toml (1)

97-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Scope the mypy suppressions.

These settings suppress more than PyQt5 stub errors: warn_return_any = false disables return-Any warnings globally, ignore_missing_imports = true hides unresolved imports globally, and ignore_errors = true skips every diagnostic in colorcast.gui. The new GUI code is therefore outside the type-checking guarantee. Keep colorcast.gui checked and scope suppressions to the Qt modules or specific diagnostics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` around lines 97 - 103, Scope the mypy configuration
suppressions in the main settings and colorcast.gui override: remove the global
warn_return_any and ignore_missing_imports disables, keep colorcast.gui
type-checked by removing ignore_errors, and apply narrowly targeted overrides
only to the relevant PyQt5/Qt modules or specific diagnostics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyproject.toml`:
- Line 96: Align mypy with the project’s minimum supported Python version by
changing the mypy target in pyproject.toml and the corresponding CI job
configuration in .github/workflows/ci.yml from Python 3.12 to Python 3.10, or
configure mypy to run across every supported version.

---

Nitpick comments:
In `@pyproject.toml`:
- Around line 97-103: Scope the mypy configuration suppressions in the main
settings and colorcast.gui override: remove the global warn_return_any and
ignore_missing_imports disables, keep colorcast.gui type-checked by removing
ignore_errors, and apply narrowly targeted overrides only to the relevant
PyQt5/Qt modules or specific diagnostics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a3ea2fb0-2bdb-4858-b6c5-6c7b1f86c2bb

📥 Commits

Reviewing files that changed from the base of the PR and between 31fdf37 and 582c188.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • colorcast/analysis/comparison.py
  • colorcast/analysis/daltonization.py
  • colorcast/analysis/dashboard.py
  • colorcast/processing/image_loader.py
  • colorcast/utils/validators_enhanced.py
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (5)
  • colorcast/processing/image_loader.py
  • colorcast/analysis/daltonization.py
  • colorcast/utils/validators_enhanced.py
  • colorcast/analysis/dashboard.py
  • colorcast/analysis/comparison.py

Comment thread pyproject.toml
[tool.mypy]
python_version = "3.10"
warn_return_any = true
python_version = "3.12"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'requires-python|python_version|python-version' pyproject.toml .github/workflows/ci.yml

Repository: MichailSemoglou/ColorCast

Length of output: 539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- pyproject.toml ---'
sed -n '1,115p' pyproject.toml

printf '%s\n' '--- .github/workflows/ci.yml ---'
sed -n '1,80p' .github/workflows/ci.yml

printf '%s\n' '--- parsed version settings ---'
python3 - <<'PY'
import pathlib, re, tomllib

pyproject = tomllib.loads(pathlib.Path("pyproject.toml").read_text())
print("requires-python:", pyproject["project"].get("requires-python"))
print("tool.mypy.python_version:", pyproject.get("tool", {}).get("mypy", {}).get("python_version"))

workflow = pathlib.Path(".github/workflows/ci.yml").read_text()
for pattern in (
    r'python-version:\s*"([^"]+)"',
    r'python-version:\s*\[(.*?)\]',
):
    print(pattern, re.findall(pattern, workflow))
PY

Repository: MichailSemoglou/ColorCast

Length of output: 5068


Align mypy with the minimum supported Python version.

pyproject.toml declares Python >=3.10, but mypy targets and runs on Python 3.12. Set the mypy target and CI job to Python 3.10, or run mypy for every supported version.

📍 Affects 2 files
  • pyproject.toml#L96-L96 (this comment)
  • .github/workflows/ci.yml#L39-L43
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` at line 96, Align mypy with the project’s minimum supported
Python version by changing the mypy target in pyproject.toml and the
corresponding CI job configuration in .github/workflows/ci.yml from Python 3.12
to Python 3.10, or configure mypy to run across every supported version.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
colorcast/gui.py (4)

28-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider exporting public aliases instead of importing private names.

_DEFICIENCIES and _DEFICIENCY_LABELS are private to colorcast.analysis.dashboard. The GUI depends on them across module boundaries, so renames inside dashboard.py will break the GUI silently. Export public aliases (for example DEFICIENCIES and DEFICIENCY_LABELS) and import those.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/gui.py` around lines 28 - 33, In colorcast.analysis.dashboard,
expose public aliases for the private _DEFICIENCIES and _DEFICIENCY_LABELS
symbols, then update the GUI import to use DEFICIENCIES and DEFICIENCY_LABELS
while preserving existing behavior.

747-758: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Record the failure reason and reuse the transfer cache.

Two points in this loop:

  1. Line 755-756 discards the exception. The log record contains only the method id, so a failing method cannot be diagnosed from the logs. Add exc_info=True.
  2. Line 752 calls instance.transfer(...) directly. The main window uses registry.transfer_cached at line 416. The dialog therefore recomputes transfers that are already cached for the same content and style images. Use registry.transfer_cached to reuse those results.
♻️ Proposed change
     def _start_computation(self) -> None:
         def _run() -> None:
             for method_id in self._label_widgets:
                 try:
-                    instance = registry.get_method(method_id)
-                    self._results[method_id] = instance.transfer(
-                        self._content_image, self._style_image
-                    )
+                    self._results[method_id] = registry.transfer_cached(
+                        method_id, self._content_image, self._style_image
+                    )
                 except Exception:  # noqa: BLE001 — skip unsupported methods
-                    logger.debug("Skipped method %s in comparison", method_id)
+                    logger.debug(
+                        "Skipped method %s in comparison", method_id, exc_info=True
+                    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/gui.py` around lines 747 - 758, Update the _start_computation loop
to call registry.transfer_cached with the method identifier and current
content/style images instead of instance.transfer, preserving result storage in
self._results. Enhance the skipped-method logger.debug call with exc_info=True
so the caught exception details are recorded.

760-776: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the completion count.

self._results only receives successful arrays, so the v is not None filter at line 774 is always true. Use len(self._results).

♻️ Proposed change
-        done = sum(1 for v in self._results.values() if v is not None)
+        done = len(self._results)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/gui.py` around lines 760 - 776, In _on_computation_done, simplify
the completed-method count by using len(self._results) directly instead of
summing values filtered by v is not None; leave the total calculation and status
text unchanged.

311-325: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the shared image-file filter. Define it once and reuse it in both dialogs. ALLOWED_IMAGE_EXTENSIONS is already a dotted tuple with stable order, so sorting is unnecessary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/gui.py` around lines 311 - 325, Extract the repeated image-file
filter expression into a shared value, then reuse it in both file dialogs within
the image-loading methods. Build it directly from ALLOWED_IMAGE_EXTENSIONS in
its existing order without sorting, and preserve the current dialog behavior and
titles.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@colorcast/gui.py`:
- Around line 71-85: Update _run_in_thread and _WorkerSignals usage so the
signals QObject remains strongly referenced until the queued finished callback
has been delivered, rather than only through the worker closure. Release that
retained reference after delivery completes while preserving the existing
on_done callback behavior.

In `@colorcast/utils/config.py`:
- Around line 102-106: Validate that the parsed JSON value in the
configuration-loading flow is an object/mapping before the loop over
raw.items(). Raise TypeError for roots such as arrays or null, while preserving
the existing field filtering behavior for valid object roots.

---

Nitpick comments:
In `@colorcast/gui.py`:
- Around line 28-33: In colorcast.analysis.dashboard, expose public aliases for
the private _DEFICIENCIES and _DEFICIENCY_LABELS symbols, then update the GUI
import to use DEFICIENCIES and DEFICIENCY_LABELS while preserving existing
behavior.
- Around line 747-758: Update the _start_computation loop to call
registry.transfer_cached with the method identifier and current content/style
images instead of instance.transfer, preserving result storage in self._results.
Enhance the skipped-method logger.debug call with exc_info=True so the caught
exception details are recorded.
- Around line 760-776: In _on_computation_done, simplify the completed-method
count by using len(self._results) directly instead of summing values filtered by
v is not None; leave the total calculation and status text unchanged.
- Around line 311-325: Extract the repeated image-file filter expression into a
shared value, then reuse it in both file dialogs within the image-loading
methods. Build it directly from ALLOWED_IMAGE_EXTENSIONS in its existing order
without sorting, and preserve the current dialog behavior and titles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 08165e77-f51c-452f-97b8-71c19fbb4c98

📥 Commits

Reviewing files that changed from the base of the PR and between 7324cd5 and 582c188.

⛔ Files ignored due to path filters (4)
  • imgs/CVD-Accessibility-Dashboard.png is excluded by !**/*.png
  • imgs/Compare-Transfer-Methods.png is excluded by !**/*.png
  • imgs/dashboard_report.png is excluded by !**/*.png
  • imgs/interface.png is excluded by !**/*.png
📒 Files selected for processing (68)
  • .github/ISSUE_TEMPLATE/bug_report.md
  • .github/ISSUE_TEMPLATE/feature_request.md
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/dependabot.yml
  • .github/workflows/ci.yml
  • .gitignore
  • .zenodo.json
  • CHANGELOG.md
  • CITATION.cff
  • CODE_OF_CONDUCT.md
  • CONTRIBUTING.md
  • MANIFEST.in
  • README.md
  • SECURITY.md
  • colorcast.py
  • colorcast/__init__.py
  • colorcast/__main__.py
  • colorcast/_version.py
  • colorcast/analysis/__init__.py
  • colorcast/analysis/comparison.py
  • colorcast/analysis/daltonization.py
  • colorcast/analysis/dashboard.py
  • colorcast/analysis/error_map.py
  • colorcast/analysis/visualization.py
  • colorcast/gui.py
  • colorcast/processing/__init__.py
  • colorcast/processing/batch.py
  • colorcast/processing/blending.py
  • colorcast/processing/cache.py
  • colorcast/processing/curves.py
  • colorcast/processing/gpu_transfer.py
  • colorcast/processing/image_loader.py
  • colorcast/processing/registry.py
  • colorcast/processing/simulation.py
  • colorcast/processing/transfer_methods.py
  • colorcast/utils/__init__.py
  • colorcast/utils/config.py
  • colorcast/utils/exceptions.py
  • colorcast/utils/validators.py
  • colorcast/utils/validators_enhanced.py
  • docs/conf.py
  • docs/index.rst
  • pyproject.toml
  • requirements-dev.txt
  • requirements.txt
  • scripts/daltonization_efficacy.py
  • tests/__init__.py
  • tests/test_batch.py
  • tests/test_blending.py
  • tests/test_cache.py
  • tests/test_cli.py
  • tests/test_color_blindness.py
  • tests/test_comparison.py
  • tests/test_config.py
  • tests/test_curves.py
  • tests/test_entry_points.py
  • tests/test_gpu_transfer.py
  • tests/test_gui.py
  • tests/test_image_loading.py
  • tests/test_integration.py
  • tests/test_integration_comprehensive.py
  • tests/test_lab_transfer.py
  • tests/test_performance.py
  • tests/test_property_based.py
  • tests/test_registry.py
  • tests/test_transfer_methods.py
  • tests/test_validators_enhanced.py
  • tests/test_visualization.py
💤 Files with no reviewable changes (4)
  • requirements.txt
  • colorcast/utils/validators.py
  • colorcast.py
  • requirements-dev.txt
🚧 Files skipped from review as they are similar to previous changes (59)
  • .gitignore
  • tests/init.py
  • CITATION.cff
  • colorcast/processing/curves.py
  • colorcast/processing/blending.py
  • colorcast/processing/init.py
  • tests/test_cli.py
  • .github/ISSUE_TEMPLATE/feature_request.md
  • tests/test_registry.py
  • colorcast/utils/init.py
  • colorcast/analysis/init.py
  • tests/test_blending.py
  • tests/test_integration.py
  • tests/test_curves.py
  • CONTRIBUTING.md
  • scripts/daltonization_efficacy.py
  • colorcast/_version.py
  • .github/ISSUE_TEMPLATE/bug_report.md
  • SECURITY.md
  • pyproject.toml
  • colorcast/processing/transfer_methods.py
  • tests/test_visualization.py
  • docs/conf.py
  • .github/PULL_REQUEST_TEMPLATE.md
  • MANIFEST.in
  • CODE_OF_CONDUCT.md
  • tests/test_lab_transfer.py
  • colorcast/utils/exceptions.py
  • tests/test_validators_enhanced.py
  • .zenodo.json
  • tests/test_color_blindness.py
  • tests/test_gui.py
  • colorcast/processing/image_loader.py
  • tests/test_comparison.py
  • tests/test_integration_comprehensive.py
  • tests/test_image_loading.py
  • tests/test_transfer_methods.py
  • colorcast/init.py
  • .github/dependabot.yml
  • colorcast/processing/simulation.py
  • .github/workflows/ci.yml
  • tests/test_cache.py
  • tests/test_batch.py
  • colorcast/analysis/dashboard.py
  • tests/test_gpu_transfer.py
  • colorcast/analysis/comparison.py
  • tests/test_property_based.py
  • colorcast/utils/validators_enhanced.py
  • colorcast/processing/cache.py
  • colorcast/analysis/daltonization.py
  • colorcast/processing/batch.py
  • tests/test_performance.py
  • colorcast/processing/gpu_transfer.py
  • colorcast/analysis/error_map.py
  • README.md
  • colorcast/analysis/visualization.py
  • colorcast/processing/registry.py
  • docs/index.rst
  • colorcast/main.py

Comment thread colorcast/gui.py
Comment thread colorcast/utils/config.py
Replace eager imports of MethodComparison, daltonize, ErrorMap, and
get_error_map with module-level __getattr__ that resolves them on first
access. Importing colorcast no longer initializes colorcast.analysis or
requires matplotlib.
- ErrorMap.chroma_error_dE00 made optional (defaults to None, not NaN array)
- daltonization docs: clarify L* is preserved from base, not restored from orig
- error_map docs: remove stale claim about apply_daltonization using orig_l_star
- dashboard: normalize image_array to float32 before storing in DashboardResult
- gui: keep QObject reference alive until signal is delivered
- gui: emit finished signal in finally block
- config: raise TypeError on non-object JSON roots
- SECURITY.md: mark 2.5.x as supported
- tests: update dE00 assertions for optional field
- tests: add Figure assertion to test_custom_title
- docs: remove orphaned wiki markdown file

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
colorcast/utils/config.py (3)

53-55: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat empty environment variables as unset.

Use os.environ.get("APPDATA") or ... and os.environ.get("XDG_CONFIG_HOME") or ... so empty values do not resolve to the current directory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/utils/config.py` around lines 53 - 55, Update the base-path
assignments in the configuration path logic to treat empty APPDATA and
XDG_CONFIG_HOME values as unset by falling back to their existing
platform-specific defaults. Preserve the current Path conversion and fallback
locations.

36-36: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not stringify unsupported configuration values.

default=str converts non-JSON values in custom_methods to strings. load() accepts those strings, so the original plugin values cannot be restored. Remove default=str or implement an explicit serializer for supported plugin values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/utils/config.py` at line 36, Update the custom_methods
configuration serialization in the relevant load/save configuration logic to
remove default=str, or replace it with an explicit serializer limited to
supported plugin values. Ensure load() preserves and restores supported non-JSON
plugin values instead of accepting irreversible stringified representations.

111-127: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate semantic configuration values before returning ColorCastConfig.

load() accepts negative dimensions and debounce intervals, default_intensity outside [0.0, 1.0], and unregistered default_method values. StyleTransferApp uses these values directly, and an invalid method causes registry.get_method() to raise ValueError. Add field-specific bounds and validate default_method against registered method IDs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@colorcast/utils/config.py` around lines 111 - 127, Update the configuration
validation in load() before constructing ColorCastConfig: require dimensions and
debounce intervals to be non-negative, constrain default_intensity to [0.0,
1.0], and ensure default_method matches a registered method ID. Preserve the
existing type validation and raise a clear configuration error for each invalid
semantic value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@colorcast/utils/config.py`:
- Around line 53-55: Update the base-path assignments in the configuration path
logic to treat empty APPDATA and XDG_CONFIG_HOME values as unset by falling back
to their existing platform-specific defaults. Preserve the current Path
conversion and fallback locations.
- Line 36: Update the custom_methods configuration serialization in the relevant
load/save configuration logic to remove default=str, or replace it with an
explicit serializer limited to supported plugin values. Ensure load() preserves
and restores supported non-JSON plugin values instead of accepting irreversible
stringified representations.
- Around line 111-127: Update the configuration validation in load() before
constructing ColorCastConfig: require dimensions and debounce intervals to be
non-negative, constrain default_intensity to [0.0, 1.0], and ensure
default_method matches a registered method ID. Preserve the existing type
validation and raise a clear configuration error for each invalid semantic
value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 05169881-2d84-4b90-99a1-235a7ebbbac3

📥 Commits

Reviewing files that changed from the base of the PR and between 213484a and 915e009.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • README.md
  • SECURITY.md
  • colorcast/analysis/daltonization.py
  • colorcast/analysis/dashboard.py
  • colorcast/analysis/error_map.py
  • colorcast/gui.py
  • colorcast/utils/config.py
  • docs/wiki/Color-Vision-Background.md
  • tests/test_color_blindness.py
  • tests/test_visualization.py
💤 Files with no reviewable changes (1)
  • docs/wiki/Color-Vision-Background.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • SECURITY.md
  • tests/test_color_blindness.py
  • colorcast/analysis/daltonization.py
  • CHANGELOG.md
  • README.md
  • colorcast/analysis/error_map.py
  • colorcast/analysis/dashboard.py
  • colorcast/gui.py

@MichailSemoglou
MichailSemoglou merged commit 62dfa5d into main Aug 2, 2026
7 checks passed
@MichailSemoglou
MichailSemoglou deleted the feature/v2.5.0 branch August 2, 2026 09:05
@coderabbitai coderabbitai Bot mentioned this pull request Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant