Skip to content

chore: Drop python 3.9 support#1404

Merged
Czaki merged 14 commits into
developfrom
drop_python_3_9
Jun 6, 2026
Merged

chore: Drop python 3.9 support#1404
Czaki merged 14 commits into
developfrom
drop_python_3_9

Conversation

@Czaki
Copy link
Copy Markdown
Collaborator

@Czaki Czaki commented Jun 4, 2026

Summary by CodeRabbit

  • Breaking Changes

    • Minimum Python requirement raised to 3.10.
  • Improvements

    • CI and packaging updated to target newer Python and Napari versions.
    • Stricter runtime checks for mismatched sequence lengths to surface issues earlier.
  • Refactor

    • Modernized type annotations across the codebase.
  • Chores

    • Removed legacy autogenerated dependency constraint files.

@Czaki Czaki added this to the 0.17.0 milestone Jun 4, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Sorry @Czaki, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 4, 2026

Too much diff to scan? Review this PR in Change Stack to start with the highest-impact changes.

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Bumps CI and packaging to Python >=3.10, replaces Optional/Union with PEP 604 | unions across the codebase, moves many Callable imports to collections.abc, and enables selective zip(..., strict=...) checks; tests and notebooks updated.

Changes

Python 3.10+ Migration and Typing Modernization

Layer / File(s) Summary
CI matrices and packaging targets
.github/workflows/*, pyproject.toml, tox.ini
Workflows and tox matrices drop Python 3.9 and add newer minors; requires-python raised to >=3.10; tooling/config pins updated.
PEP 604 typing sweep
package/**, package/PartSegCore/**, package/PartSegImage/**
Widespread replacement of typing.Optional/typing.Union with `X
zip() strictness and unpacking
multiple modules and tests
Selected zip call sites changed to strict=True or strict=False, altering behavior on mismatched-length iterables (raise vs truncate/silent).
I/O, readers, writers, and save/load APIs
package/PartSegCore/io_utils.py, package/PartSegCore/mask/io_functions.py, package/PartSegImage/*
Signature modernizations to use PEP 604 unions for path/stream types and optional parameters across loaders/savers/readers/writers.
GUI and plugin signatures
package/PartSeg/common_gui/*, package/PartSeg/plugins/*
Widget/dialog constructors and related method signatures updated to `
Tests and notebooks
package/tests/*, tutorials/*
Test annotations and some assertions updated; notebooks adjusted to stricter zip/unpack semantics.

Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels
skip check PR title

Poem

A rabbit hops through pipes and types,
Rewrites Optionals into tidy pipes.
CI climbs higher, zips now strict,
Tests and notebooks click-click-click.
Hooray — a modern, typed delight 🐰✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch drop_python_3_9

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (4)
package/PartSeg/plugins/napari_io/loader.py (1)

25-37: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix adjust_color return contract (generator vs tuple + wrong tuple[float])
In package/PartSeg/plugins/napari_io/loader.py, the list[int] overload promises list[float], but the implementation returns a generator expression; additionally the implementation annotation tuple[float] denotes a 1-element tuple, not an RGB 3-float tuple.

Proposed fix
 `@typing.overload`
 def adjust_color(color: str) -> str: ...
 
 
 `@typing.overload`
-def adjust_color(color: list[int]) -> list[float]: ...
+def adjust_color(color: list[int]) -> tuple[float, float, float]: ...
 
 
-def adjust_color(color: str | list[int]) -> str | tuple[float]:
+def adjust_color(color: str | list[int]) -> str | tuple[float, float, float]:
@@
-    elif isinstance(color, list):
-        return (color[i] / 255 for i in range(3))
+    elif isinstance(color, list):
+        return tuple(color[i] / 255 for i in range(3))
🤖 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 `@package/PartSeg/plugins/napari_io/loader.py` around lines 25 - 37,
adjust_color currently promises to return tuple[float] but returns a generator
for list inputs and a wrongly-typed annotation; update the function signature to
return str | tuple[float, float, float], and replace the generator expression in
the list branch with an actual 3-tuple of floats (e.g. compute the three color
components divided by 255 and return as a tuple) so callers receive a concrete
(r,g,b) tuple rather than a generator and the type annotation matches the
runtime shape.
package/PartSegCore/analysis/measurement_calculation.py (1)

297-299: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the right subtree when deriving right-side component/area metadata.

Line 298 currently evaluates tree.left twice, so the right branch is ignored and combined metadata can be incorrect.

Suggested fix
         left_par, left_area = self._get_par_component_and_area_type(tree.left)
-        right_par, right_area = self._get_par_component_and_area_type(tree.left)
+        right_par, right_area = self._get_par_component_and_area_type(tree.right)
🤖 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 `@package/PartSegCore/analysis/measurement_calculation.py` around lines 297 -
299, The code calls self._get_par_component_and_area_type(tree.left) for both
left and right, ignoring the right subtree; update the second call to use
tree.right so right_par and right_area are derived from the right subtree (i.e.,
call self._get_par_component_and_area_type(tree.right) when setting right_par,
right_area) so the subsequent check using PerComponent.Yes on [left_par,
right_par] is correct.
package/PartSeg/_roi_mask/stack_settings.py (1)

322-335: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align get_mask return type with actual None return path.

When segmentation is None and mask is None, this function returns None, but the signature declares -> np.ndarray. Please update the annotation to reflect the real contract.

Suggested patch
-def get_mask(segmentation: np.ndarray | None, mask: np.ndarray | None, selected: list[int]) -> np.ndarray:
+def get_mask(segmentation: np.ndarray | None, mask: np.ndarray | None, selected: list[int]) -> np.ndarray | 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 `@package/PartSeg/_roi_mask/stack_settings.py` around lines 322 - 335, The
function get_mask can return None when segmentation or mask is None, but its
return type is declared as np.ndarray; change the signature to return
typing.Optional[np.ndarray] (or Optional[np.ndarray]) so the annotation matches
actual behavior, update any import if needed, and adjust the docstring
return/rtype lines to reflect Optional[np.ndarray]; locate get_mask in
stack_settings.py to apply this change.
package/PartSeg/common_gui/colormap_creator.py (1)

663-693: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix declared BytesIO support for colormap JSON save/load

save_location / load_locations are typed to accept BytesIO, but the implementation uses open(save_location, ...) and open(load_locations[0]), which raises TypeError: expected str, bytes or os.PathLike object, not BytesIO at package/PartSeg/common_gui/colormap_creator.py lines 669 and 692. Handle BytesIO explicitly (write/read from the buffer) instead of calling open(...) on it.

Proposed fix
@@
-    def save(
+    def save(
         cls,
         save_location: str | BytesIO | Path,
         project_info,
         parameters: dict | None = None,
         range_changed=None,
         step_changed=None,
     ):
-        with open(save_location, "w") as f:
-            json.dump(project_info, f, cls=local_migrator.Encoder, indent=4)
+        if isinstance(save_location, BytesIO):
+            payload = json.dumps(project_info, cls=local_migrator.Encoder, indent=4).encode("utf-8")
+            save_location.write(payload)
+            return
+        with open(save_location, "w", encoding="utf-8") as f:
+            json.dump(project_info, f, cls=local_migrator.Encoder, indent=4)
@@
-    def load(
+    def load(
         cls,
         load_locations: list[str | BytesIO | Path],
         range_changed: typing.Callable[[int, int], typing.Any] | None = None,
         step_changed: typing.Callable[[int], typing.Any] | None = None,
         metadata: dict | None = None,
     ) -> Colormap:
-        with open(load_locations[0]) as f:
-            return json.load(f, object_hook=local_migrator.object_hook)
+        source = load_locations[0]
+        if isinstance(source, BytesIO):
+            source.seek(0)
+            return json.loads(source.read().decode("utf-8"), object_hook=local_migrator.object_hook)
+        with open(source, encoding="utf-8") as f:
+            return json.load(f, object_hook=local_migrator.object_hook)
🤖 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 `@package/PartSeg/common_gui/colormap_creator.py` around lines 663 - 693, The
save implementation that accepts a save_location parameter and the
ColormapLoad.load method currently call open(...) directly which fails for
BytesIO; update both to handle BytesIO explicitly: in the save routine (the
function with parameter save_location) detect if isinstance(save_location,
BytesIO) and write the JSON text to it (e.g., json_str =
json.dumps(project_info, cls=local_migrator.Encoder, indent=4) then write
json_str.encode() or use a TextIOWrapper), otherwise use open(save_location,
"w") as before; in ColormapLoad.load detect if isinstance(load_locations[0],
BytesIO) and read from the buffer (decode bytes or use TextIOWrapper) then
json.loads(..., object_hook=local_migrator.object_hook), otherwise open the path
with open(...) as currently implemented.
🧹 Nitpick comments (1)
package/PartSeg/_roi_analysis/advanced_window.py (1)

369-369: ⚡ Quick win

Fix chosen_element_area annotation to match actual runtime type.

chosen_element_area is used as a measurement node object (accessed via .area / .per_component and passed as Node.left), so tuple[AreaType, float] | None is inconsistent and weakens type safety.

Suggested patch
-        self.chosen_element_area: tuple[AreaType, float] | None = None
+        self.chosen_element_area: Leaf | Node | None = 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 `@package/PartSeg/_roi_analysis/advanced_window.py` at line 369,
chosen_element_area is annotated as tuple[AreaType, float] | None but at runtime
it holds a measurement node object (the object you call .area and .per_component
on and pass to Node.left); update its annotation to the actual measurement node
class (e.g., AreaMeasurementNode or the class name used for area nodes in your
codebase) and import that class, then declare chosen_element_area:
Optional[ThatMeasurementNode]; if the concrete class is unclear, use typing.Any
(Optional[Any]) as a temporary fallback and replace it with the real type.
🤖 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 @.github/workflows/tests.yml:
- Around line 148-149: The CI matrix entry for the test_minimal job sets
python_version: "3.9" but uses tox_args: "-e py310-PyQt5-minimal", causing a
mismatch; update the python_version to "3.10" (or otherwise match the py310 tox
environment) so the Python version aligns with the py310 tox environment and the
project's Python 3.9 drop goal—change the python_version field in the same
matrix entry that contains tox_args and any job name or identifier referencing
test_minimal/py310-PyQt5-minimal.

In `@package/PartSeg/common_gui/custom_load_dialog.py`:
- Line 131: Update the typing annotations that use the runtime expression
type(LoadBase) to the static form type[LoadBase] so type checkers recognize
"subclass of LoadBase"; specifically change the load_register parameter in
PLoadDialog.__init__ and any occurrences in IORegister (and related annotations
referencing LoadBase) from type(LoadBase) to type[LoadBase], and adjust
union/collection annotations (e.g., dict[str, type[LoadBase]] | type[LoadBase])
accordingly.

In `@package/PartSeg/common_gui/label_create.py`:
- Around line 332-336: The type hint wrongly advertises BytesIO support for the
load_locations parameter even though the implementation calls open(...) on each
entry; remove BytesIO from the annotated union (change list[str | BytesIO |
Path] to list[str | Path]) in the function signature(s) that define
load_locations and the other two signatures mentioned, and update any
docstrings/comments to match the tightened contract so callers aren't misled
into passing BytesIO objects that will cause a runtime error.

In `@package/PartSeg/common_gui/napari_image_view.py`:
- Around line 732-733: The loop generating grid positions uses zip(...,
itertools.product(...), strict=True) which raises ValueError when product yields
more positions than images; replace this by limiting the generated positions to
the exact number of images and remove strict: use
itertools.islice(itertools.product(range(n_row), repeat=2),
len(self.image_info)) and zip(self.image_info.values(), that_islice) so
translate_2d = np.multiply(scene_size[-2:], pos) still applies; update imports
to include itertools.islice and reference the existing symbols self.image_info,
translate_2d, and n_row in the change.

In `@package/PartSeg/plugins/itk_snap_save/__init__.py`:
- Line 26: The SaveITKSnap.save() signature incorrectly includes BytesIO for
save_location but forwards it to SimpleITK.WriteImage(mask, save_location),
which accepts only filesystem paths; update SaveITKSnap.save() to accept only
path types (e.g., change save_location annotation to str | Path) and remove
BytesIO from the type OR implement conversion of a BytesIO by writing its
contents to a temporary file and passing that temp path to SimpleITK.WriteImage;
update any related docstring and callers to match the new API.

In `@package/PartSeg/plugins/modeling_save/save_modeling_data.py`:
- Line 45: The save_location parameter is annotated to allow BytesIO but the
save() implementation only works with filesystem paths; update the type hints to
restrict save_location to path-like types (e.g., str | Path | os.PathLike) and
remove BytesIO from the union in the save() function signature (and the other
related signatures around the same block), ensuring all internal uses expect a
directory path; alternatively, if you want to keep file-like support, implement
explicit handling in save() to detect a BytesIO and raise a clear error or
implement a different code path for writing multiple files into an archive, but
the minimal fix is to change the annotations to path-like only in
save_modeling_data.py for the save() and related functions.

In `@package/PartSeg/plugins/old_partseg/old_partseg.py`:
- Around line 67-70: The load() signature declares load_locations: list[str |
BytesIO | Path] but the control flow only handles str, TarFile and IOBase so
Path objects fall through to ValueError; update the load() implementation to
detect pathlib.Path (or use os.fspath) and convert Path instances to strings (or
file-like objects as appropriate) before the existing branching, and apply the
same fix to the analogous handling in the other block referenced (lines ~73-83).
Ensure you reference the parameter name load_locations and the load() function
(and the local variable that iterates locations, e.g., location) when making the
change.

In `@package/PartSegCore/analysis/calculation_plan.py`:
- Line 238: name_dict is declared as possibly None but parse_map writes into it
(e.g., item assignments in parse_map), causing a NoneType error on first parse;
update the code that defines/uses name_dict (the variable named name_dict and
the function parse_map) to ensure name_dict is initialized to an empty dict
before any writes (for example, set name_dict = {} if it is None at the start of
parse_map or where name_dict is declared) and apply the same initialization
logic for the other parse paths around the parse_map-related code (the code
block covering the parse_map interactions currently around lines that reference
name_dict).

In `@package/PartSegImage/image.py`:
- Around line 417-420: The list comprehension that builds merged channel arrays
is using the right image's channel data twice (y and reordered y) which drops
this instance's channels; in the comprehension inside the merge routine replace
the first operand with the left image channel variable (x) so it concatenates x
with the reordered y when axis is not 'C'. Locate the comprehension that
iterates over self._channel_arrays and image._channel_arrays (the variables x,
y) and change the first concatenation argument from y to x while keeping the
call to self.reorder_axes(y, image.array_axis_order) and the axis=index
unchanged.

---

Outside diff comments:
In `@package/PartSeg/_roi_mask/stack_settings.py`:
- Around line 322-335: The function get_mask can return None when segmentation
or mask is None, but its return type is declared as np.ndarray; change the
signature to return typing.Optional[np.ndarray] (or Optional[np.ndarray]) so the
annotation matches actual behavior, update any import if needed, and adjust the
docstring return/rtype lines to reflect Optional[np.ndarray]; locate get_mask in
stack_settings.py to apply this change.

In `@package/PartSeg/common_gui/colormap_creator.py`:
- Around line 663-693: The save implementation that accepts a save_location
parameter and the ColormapLoad.load method currently call open(...) directly
which fails for BytesIO; update both to handle BytesIO explicitly: in the save
routine (the function with parameter save_location) detect if
isinstance(save_location, BytesIO) and write the JSON text to it (e.g., json_str
= json.dumps(project_info, cls=local_migrator.Encoder, indent=4) then write
json_str.encode() or use a TextIOWrapper), otherwise use open(save_location,
"w") as before; in ColormapLoad.load detect if isinstance(load_locations[0],
BytesIO) and read from the buffer (decode bytes or use TextIOWrapper) then
json.loads(..., object_hook=local_migrator.object_hook), otherwise open the path
with open(...) as currently implemented.

In `@package/PartSeg/plugins/napari_io/loader.py`:
- Around line 25-37: adjust_color currently promises to return tuple[float] but
returns a generator for list inputs and a wrongly-typed annotation; update the
function signature to return str | tuple[float, float, float], and replace the
generator expression in the list branch with an actual 3-tuple of floats (e.g.
compute the three color components divided by 255 and return as a tuple) so
callers receive a concrete (r,g,b) tuple rather than a generator and the type
annotation matches the runtime shape.

In `@package/PartSegCore/analysis/measurement_calculation.py`:
- Around line 297-299: The code calls
self._get_par_component_and_area_type(tree.left) for both left and right,
ignoring the right subtree; update the second call to use tree.right so
right_par and right_area are derived from the right subtree (i.e., call
self._get_par_component_and_area_type(tree.right) when setting right_par,
right_area) so the subsequent check using PerComponent.Yes on [left_par,
right_par] is correct.

---

Nitpick comments:
In `@package/PartSeg/_roi_analysis/advanced_window.py`:
- Line 369: chosen_element_area is annotated as tuple[AreaType, float] | None
but at runtime it holds a measurement node object (the object you call .area and
.per_component on and pass to Node.left); update its annotation to the actual
measurement node class (e.g., AreaMeasurementNode or the class name used for
area nodes in your codebase) and import that class, then declare
chosen_element_area: Optional[ThatMeasurementNode]; if the concrete class is
unclear, use typing.Any (Optional[Any]) as a temporary fallback and replace it
with the real type.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: abcbfd90-5b00-4197-84dc-99c7fc1e96b0

📥 Commits

Reviewing files that changed from the base of the PR and between 6010bde and 9fa1994.

📒 Files selected for processing (91)
  • .github/workflows/test_napari_widgets.yml
  • .github/workflows/tests.yml
  • package/PartSeg/_roi_analysis/advanced_window.py
  • package/PartSeg/_roi_analysis/batch_window.py
  • package/PartSeg/_roi_analysis/calculation_pipeline_thread.py
  • package/PartSeg/_roi_analysis/export_batch.py
  • package/PartSeg/_roi_analysis/image_view.py
  • package/PartSeg/_roi_analysis/partseg_settings.py
  • package/PartSeg/_roi_analysis/prepare_plan_widget.py
  • package/PartSeg/_roi_analysis/profile_export.py
  • package/PartSeg/_roi_mask/batch_proceed.py
  • package/PartSeg/_roi_mask/main_window.py
  • package/PartSeg/_roi_mask/segmentation_info_dialog.py
  • package/PartSeg/_roi_mask/stack_settings.py
  • package/PartSeg/common_backend/base_argparser.py
  • package/PartSeg/common_backend/base_settings.py
  • package/PartSeg/common_backend/partially_const_dict.py
  • package/PartSeg/common_backend/progress_thread.py
  • package/PartSeg/common_gui/advanced_tabs.py
  • package/PartSeg/common_gui/algorithms_description.py
  • package/PartSeg/common_gui/channel_control.py
  • package/PartSeg/common_gui/collapse_checkbox.py
  • package/PartSeg/common_gui/colormap_creator.py
  • package/PartSeg/common_gui/custom_load_dialog.py
  • package/PartSeg/common_gui/custom_save_dialog.py
  • package/PartSeg/common_gui/error_report.py
  • package/PartSeg/common_gui/image_adjustment.py
  • package/PartSeg/common_gui/label_create.py
  • package/PartSeg/common_gui/main_window.py
  • package/PartSeg/common_gui/mask_widget.py
  • package/PartSeg/common_gui/napari_image_view.py
  • package/PartSeg/common_gui/napari_viewer_wrap.py
  • package/PartSeg/common_gui/stack_image_view.py
  • package/PartSeg/common_gui/universal_gui_part.py
  • package/PartSeg/plugins/itk_snap_save/__init__.py
  • package/PartSeg/plugins/modeling_save/save_modeling_data.py
  • package/PartSeg/plugins/napari_io/loader.py
  • package/PartSeg/plugins/napari_io/save_mask_roi.py
  • package/PartSeg/plugins/napari_io/save_tiff_layer.py
  • package/PartSeg/plugins/napari_widgets/algorithm_widgets.py
  • package/PartSeg/plugins/napari_widgets/colormap_control.py
  • package/PartSeg/plugins/napari_widgets/simple_measurement_widget.py
  • package/PartSeg/plugins/napari_widgets/utils.py
  • package/PartSeg/plugins/old_partseg/old_partseg.py
  • package/PartSegCore/_old_json_hooks.py
  • package/PartSegCore/algorithm_describe_base.py
  • package/PartSegCore/analysis/batch_processing/batch_backend.py
  • package/PartSegCore/analysis/batch_processing/parallel_backend.py
  • package/PartSegCore/analysis/calculate_pipeline.py
  • package/PartSegCore/analysis/calculation_plan.py
  • package/PartSegCore/analysis/io_utils.py
  • package/PartSegCore/analysis/load_functions.py
  • package/PartSegCore/analysis/measurement_base.py
  • package/PartSegCore/analysis/measurement_calculation.py
  • package/PartSegCore/analysis/save_functions.py
  • package/PartSegCore/class_generator.py
  • package/PartSegCore/convex_fill.py
  • package/PartSegCore/image_operations.py
  • package/PartSegCore/image_transforming/combine_channels.py
  • package/PartSegCore/image_transforming/image_projection.py
  • package/PartSegCore/image_transforming/interpolate_image.py
  • package/PartSegCore/image_transforming/swap_time_stack.py
  • package/PartSegCore/image_transforming/transform_base.py
  • package/PartSegCore/io_utils.py
  • package/PartSegCore/mask/io_functions.py
  • package/PartSegCore/mask_create.py
  • package/PartSegCore/mask_partition_utils.py
  • package/PartSegCore/plugins/__init__.py
  • package/PartSegCore/project_info.py
  • package/PartSegCore/roi_info.py
  • package/PartSegCore/segmentation/algorithm_base.py
  • package/PartSegCore/segmentation/restartable_segmentation_algorithms.py
  • package/PartSegCore/segmentation/segmentation_algorithm.py
  • package/PartSegCore/segmentation/threshold.py
  • package/PartSegCore/segmentation/watershed.py
  • package/PartSegCore/utils.py
  • package/PartSegImage/channel_class.py
  • package/PartSegImage/image.py
  • package/PartSegImage/image_reader.py
  • package/PartSegImage/image_writer.py
  • package/tests/test_PartSeg/test_colormap_create.py
  • package/tests/test_PartSeg/test_common_backend.py
  • package/tests/test_PartSeg/test_common_gui.py
  • package/tests/test_PartSegCore/test_algorithm_describe_base.py
  • package/tests/test_PartSegCore/test_analysis_batch.py
  • package/tests/test_PartSegCore/test_class_generator.py
  • package/tests/test_PartSegCore/test_segmentation.py
  • package/tests/test_PartSegImage/test_image.py
  • pyproject.toml
  • requirements/constraints_py3.9.txt
  • requirements/constraints_py3.9_pydantic_1.txt
💤 Files with no reviewable changes (3)
  • requirements/constraints_py3.9.txt
  • requirements/constraints_py3.9_pydantic_1.txt
  • package/PartSegCore/plugins/init.py

Comment thread .github/workflows/tests.yml Outdated
Comment thread package/PartSeg/common_gui/custom_load_dialog.py Outdated
Comment thread package/PartSeg/common_gui/label_create.py
Comment thread package/PartSeg/common_gui/napari_image_view.py Outdated
Comment thread package/PartSeg/plugins/itk_snap_save/__init__.py
Comment thread package/PartSeg/plugins/modeling_save/save_modeling_data.py
Comment thread package/PartSeg/plugins/old_partseg/old_partseg.py
Comment thread package/PartSegCore/analysis/calculation_plan.py
Comment thread package/PartSegImage/image.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 (1)
.github/workflows/tests.yml (1)

107-119: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Contradictory matrix include/exclude for Python 3.11 + PySide2.

The include section adds python_version: "3.11" with qt_backend: "PySide2" on ubuntu-22.04 (lines 107–109), but the exclude section immediately removes all python_version: "3.11" with qt_backend: "PySide2" combinations (lines 118–119). This results in the included entry being excluded, making it unreachable in the CI matrix.

Recommended fix

If Python 3.11 + PySide2 testing is intended, remove the conflicting exclude rule:

         exclude:
-          - python_version: "3.11"
-            qt_backend: "PySide2"
           - python_version: "3.12"
             qt_backend: "PySide2"

If Python 3.11 + PySide2 testing is not intended, remove the conflicting include entry:

           - python_version: "3.12"
             qt_backend: "PyQt5"
             os: "ubuntu-22.04"
-          - python_version: "3.11"
-            os: "ubuntu-22.04"
-            qt_backend: "PySide2"
           - python_version: "3.11"
             os: "ubuntu-22.04"
🤖 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/tests.yml around lines 107 - 119, The matrix currently
both includes and excludes the combination python_version: "3.11" with
qt_backend: "PySide2" (ubuntu-22.04) via the include and exclude blocks; decide
intent and make the matrix consistent by either removing the exclude entry that
matches python_version: "3.11" / qt_backend: "PySide2" so the included case
runs, or by removing the include entry for python_version: "3.11" / qt_backend:
"PySide2" if that combination should not be tested; update only the
include/exclude entries (matrix include/exclude) to eliminate the contradictory
rule.
♻️ Duplicate comments (1)
package/PartSeg/plugins/modeling_save/save_modeling_data.py (1)

45-45: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

save_location type allows BytesIO, but implementation requires filesystem paths.

The save_location parameter annotation includes BytesIO, but lines 51–54 immediately call os.path.exists() and os.path.isdir() on it, which require path-like objects and will fail at runtime if a BytesIO is passed.

Recommended fix
-    def save(
-        cls,
-        save_location: str | BytesIO | Path,
+    def save(
+        cls,
+        save_location: str | Path,
         project_info: ProjectTuple,
🤖 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 `@package/PartSeg/plugins/modeling_save/save_modeling_data.py` at line 45, The
annotation for save_location currently allows BytesIO but the implementation
unconditionally calls os.path.exists() and os.path.isdir() on save_location;
either remove BytesIO from the annotation or add branching to handle both cases:
in the function that receives save_location (save_modeling_data.py, parameter
save_location) check isinstance(save_location, (str, Path, os.PathLike)) before
calling os.path.exists()/os.path.isdir() and treat that branch as the filesystem
path flow, and add an elif for isinstance(save_location, BytesIO) to skip
filesystem checks and write to the buffer using file-like semantics; update the
type hint to reflect both supported types if you keep BytesIO (e.g., str | Path
| BytesIO) and ensure any file-opening/writing code uses open(...) only in the
path branch and buffer.write(...) in the BytesIO branch.
🤖 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 @.github/workflows/tests.yml:
- Around line 107-119: The matrix currently both includes and excludes the
combination python_version: "3.11" with qt_backend: "PySide2" (ubuntu-22.04) via
the include and exclude blocks; decide intent and make the matrix consistent by
either removing the exclude entry that matches python_version: "3.11" /
qt_backend: "PySide2" so the included case runs, or by removing the include
entry for python_version: "3.11" / qt_backend: "PySide2" if that combination
should not be tested; update only the include/exclude entries (matrix
include/exclude) to eliminate the contradictory rule.

---

Duplicate comments:
In `@package/PartSeg/plugins/modeling_save/save_modeling_data.py`:
- Line 45: The annotation for save_location currently allows BytesIO but the
implementation unconditionally calls os.path.exists() and os.path.isdir() on
save_location; either remove BytesIO from the annotation or add branching to
handle both cases: in the function that receives save_location
(save_modeling_data.py, parameter save_location) check isinstance(save_location,
(str, Path, os.PathLike)) before calling os.path.exists()/os.path.isdir() and
treat that branch as the filesystem path flow, and add an elif for
isinstance(save_location, BytesIO) to skip filesystem checks and write to the
buffer using file-like semantics; update the type hint to reflect both supported
types if you keep BytesIO (e.g., str | Path | BytesIO) and ensure any
file-opening/writing code uses open(...) only in the path branch and
buffer.write(...) in the BytesIO branch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9165a7b2-f497-4e19-9b0b-396778b89464

📥 Commits

Reviewing files that changed from the base of the PR and between 9fa1994 and eea771c.

📒 Files selected for processing (19)
  • .github/workflows/tests.yml
  • package/PartSeg/_roi_analysis/advanced_window.py
  • package/PartSeg/_roi_analysis/measurement_widget.py
  • package/PartSeg/_roi_mask/stack_settings.py
  • package/PartSeg/common_gui/algorithms_description.py
  • package/PartSeg/common_gui/custom_load_dialog.py
  • package/PartSeg/plugins/modeling_save/save_modeling_data.py
  • package/PartSeg/plugins/napari_widgets/mask_create_widget.py
  • package/PartSeg/plugins/napari_widgets/measurement_widget.py
  • package/PartSeg/plugins/napari_widgets/simple_measurement_widget.py
  • package/PartSeg/plugins/napari_widgets/utils.py
  • package/PartSegCore/algorithm_describe_base.py
  • package/PartSegCore/analysis/measurement_base.py
  • package/PartSegCore/analysis/measurement_calculation.py
  • package/PartSegCore/color_image/base_colors.py
  • package/PartSegImage/image.py
  • pyproject.toml
  • tox.ini
  • tutorials/tutorial_neuron_types/Neuron_types_example.ipynb
🚧 Files skipped from review as they are similar to previous changes (6)
  • package/PartSeg/_roi_mask/stack_settings.py
  • package/PartSeg/plugins/napari_widgets/simple_measurement_widget.py
  • package/PartSeg/common_gui/custom_load_dialog.py
  • package/PartSeg/common_gui/algorithms_description.py
  • package/PartSegImage/image.py
  • package/PartSegCore/algorithm_describe_base.py

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 4, 2026

Codecov Report

❌ Patch coverage is 95.57823% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.72%. Comparing base (4eb9c39) to head (05674b4).

Files with missing lines Patch % Lines
package/PartSeg/common_gui/napari_image_view.py 90.00% 2 Missing ⚠️
package/PartSeg/plugins/itk_snap_save/__init__.py 0.00% 2 Missing ⚠️
...artSeg/plugins/modeling_save/save_modeling_data.py 33.33% 2 Missing ⚠️
package/PartSeg/plugins/old_partseg/old_partseg.py 0.00% 2 Missing ⚠️
package/PartSegCore/analysis/calculation_plan.py 71.42% 2 Missing ⚠️
package/PartSeg/_roi_mask/main_window.py 66.66% 1 Missing ⚠️
package/PartSeg/common_gui/universal_gui_part.py 75.00% 1 Missing ⚠️
package/PartSegCore/algorithm_describe_base.py 90.90% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1404      +/-   ##
===========================================
- Coverage    93.12%   92.72%   -0.40%     
===========================================
  Files          211      211              
  Lines        33285    33273      -12     
===========================================
- Hits         30995    30851     -144     
- Misses        2290     2422     +132     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
tox.ini (2)

12-13: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the obsolete Python 3.9 mapping.

The gh-actions section still contains 3.9: py39, which is inconsistent with the PR objective of dropping Python 3.9 support. This mapping should be removed.

🧹 Proposed fix
 [gh-actions]
 python =
-    3.9: py39
     3.10: py310
     3.11: py311
🤖 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 `@tox.ini` around lines 12 - 13, Remove the obsolete Python 3.9 mapping from
the gh-actions table in tox.ini by deleting the line "3.9: py39" so the
gh-actions mapping matches the PR's intent to drop Python 3.9 support; ensure
only the remaining mappings (e.g., "3.10: py310") remain and that the gh-actions
section syntax remains valid after removal.

79-84: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Update the testenv header to include napari_70.

Line 84 adds support for napari_70: napari==0.7.0, but the testenv header on line 79 only lists napari_{419,54}. The header should include napari_70 to match the dependency definitions.

🔧 Proposed fix
-[testenv:py{310,311,312,313,314,315}-{PyQt5,PySide2,PyQt6,PySide6}-napari_{419,54}]
+[testenv:py{310,311,312,313,314,315}-{PyQt5,PySide2,PyQt6,PySide6}-napari_{419,54,70}]
 deps =
     {[testenv]deps}
     napari_419: napari==0.4.19.post1
     napari_54: napari==0.5.4
     napari_70: napari==0.7.0
🤖 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 `@tox.ini` around lines 79 - 84, The testenv header named
"py{310,311,312,313,314,315}-{PyQt5,PySide2,PyQt6,PySide6}-napari_{419,54}" is
missing the napari_70 variant; update the testenv header to include napari_70 so
it matches the deps block (which defines napari_419, napari_54, and napari_70).
Locate the testenv header string (the one containing
"py{310,311,312,313,314,315}-{PyQt5,PySide2,PyQt6,PySide6}-napari_{419,54}") and
add ",70" or ",_70" to the napari brace group to produce "napari_{419,54,70}"
(or equivalent consistent naming) so the header aligns with the napari_70
dependency entry.
🤖 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 `@tox.ini`:
- Line 7: The envlist in tox.ini has a typo `py{310,311,312,313.314}` causing a
syntax error; edit the envlist entry (the envlist variable in tox.ini) to
replace the period with a comma so it reads `py{310,311,312,313,314}` (ensure
all other grouped env braces follow the same comma-separated format).

---

Outside diff comments:
In `@tox.ini`:
- Around line 12-13: Remove the obsolete Python 3.9 mapping from the gh-actions
table in tox.ini by deleting the line "3.9: py39" so the gh-actions mapping
matches the PR's intent to drop Python 3.9 support; ensure only the remaining
mappings (e.g., "3.10: py310") remain and that the gh-actions section syntax
remains valid after removal.
- Around line 79-84: The testenv header named
"py{310,311,312,313,314,315}-{PyQt5,PySide2,PyQt6,PySide6}-napari_{419,54}" is
missing the napari_70 variant; update the testenv header to include napari_70 so
it matches the deps block (which defines napari_419, napari_54, and napari_70).
Locate the testenv header string (the one containing
"py{310,311,312,313,314,315}-{PyQt5,PySide2,PyQt6,PySide6}-napari_{419,54}") and
add ",70" or ",_70" to the napari brace group to produce "napari_{419,54,70}"
(or equivalent consistent naming) so the header aligns with the napari_70
dependency entry.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 25f1a2a4-1495-4189-955a-c85eaafae628

📥 Commits

Reviewing files that changed from the base of the PR and between 17762dd and 2620db5.

📒 Files selected for processing (3)
  • .github/workflows/test_napari_widgets.yml
  • package/tests/test_PartSegImage/test_image.py
  • tox.ini
🚧 Files skipped from review as they are similar to previous changes (1)
  • package/tests/test_PartSegImage/test_image.py

Comment thread tox.ini Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 (1)
tox.ini (1)

7-18: ⚠️ Potential issue | 🟠 Major

Synchronize gh-actions Python mapping with envlist

tox.ini maps 3.15 -> py315 in [gh-actions], but envlist only expands py{310,311,312,313,314} (while fail_on_no_env = True is set). This can cause the Python 3.15 job to fail to select any matching tox envs even though testenv templates for py315 exist.

Suggested fix
-envlist = py{310,311,312,313,314}-{PyQt5,PySide2,PyQt6,PySide6}-all, py{310,311,312,313,314}-{PyQt5,PyQt6}-napari_repo,py{310,311,312,313,314}-{PyQt5,PyQt6,PySide2}-napari_{419,54,70}, py{312,313,314}-PySide6-napari_{419,54,70,repo}
+envlist = py{310,311,312,313,314,315}-{PyQt5,PySide2,PyQt6,PySide6}-all, py{310,311,312,313,314,315}-{PyQt5,PyQt6}-napari_repo,py{310,311,312,313,314,315}-{PyQt5,PyQt6,PySide2}-napari_{419,54,70}, py{312,313,314,315}-PySide6-napari_{419,54,70,repo}
🤖 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 `@tox.ini` around lines 7 - 18, The GH Actions Python mapping includes 3.15 ->
py315 but envlist does not expand py315 (envlist uses py{310,311,312,313,314}),
causing no matching tox envs when fail_on_no_env=True; update either the envlist
to include py315 (add py{315} into the py{...} expansions or separate py315
entries) or remove the 3.15 -> py315 mapping from the [gh-actions] section so
the mapping matches envlist; reference the envlist, the [gh-actions] block, and
the py315 identifier when making the change.
🤖 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 `@tox.ini`:
- Around line 7-18: The GH Actions Python mapping includes 3.15 -> py315 but
envlist does not expand py315 (envlist uses py{310,311,312,313,314}), causing no
matching tox envs when fail_on_no_env=True; update either the envlist to include
py315 (add py{315} into the py{...} expansions or separate py315 entries) or
remove the 3.15 -> py315 mapping from the [gh-actions] section so the mapping
matches envlist; reference the envlist, the [gh-actions] block, and the py315
identifier when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9480c3df-a5f1-4b56-8642-2fba7ee1d45e

📥 Commits

Reviewing files that changed from the base of the PR and between 2620db5 and e67f818.

📒 Files selected for processing (10)
  • .github/workflows/tests.yml
  • package/PartSeg/common_gui/label_create.py
  • package/PartSeg/common_gui/napari_image_view.py
  • package/PartSeg/plugins/itk_snap_save/__init__.py
  • package/PartSeg/plugins/modeling_save/save_modeling_data.py
  • package/PartSeg/plugins/old_partseg/old_partseg.py
  • package/PartSegCore/analysis/calculation_plan.py
  • package/PartSegImage/image.py
  • package/tests/test_PartSeg/test_common_gui.py
  • tox.ini
✅ Files skipped from review due to trivial changes (1)
  • package/PartSeg/plugins/old_partseg/old_partseg.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • package/PartSeg/plugins/itk_snap_save/init.py
  • package/PartSeg/plugins/modeling_save/save_modeling_data.py
  • package/PartSegImage/image.py
  • package/PartSegCore/analysis/calculation_plan.py
  • .github/workflows/tests.yml
  • package/PartSeg/common_gui/label_create.py
  • package/PartSeg/common_gui/napari_image_view.py

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 6, 2026

@Czaki Czaki merged commit acb30dd into develop Jun 6, 2026
63 of 65 checks passed
@Czaki Czaki deleted the drop_python_3_9 branch June 6, 2026 15:46
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