Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/project_dict.pws
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
personal_ws-1.1 en 31
personal_ws-1.1 en 32
napari
autoupdate
aspell
Expand Down Expand Up @@ -30,3 +30,4 @@ ImageJ
QtViewer
ubuntu
numpy
pydantic
22 changes: 9 additions & 13 deletions .github/workflows/base_test_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ on:
required: false
type: number
default: 60
pydantic:
required: false
type: string
default: ""
artifact_suffix:
required: false
type: string
Expand All @@ -50,7 +46,7 @@ on:

jobs:
test:
name: ${{ inputs.os }} py ${{ inputs.python_version }} ${{ inputs.napari }} ${{ inputs.qt_backend }} ${{ inputs.pydantic }}
name: ${{ inputs.os }} py ${{ inputs.python_version }} ${{ inputs.napari }} ${{ inputs.qt_backend }}
runs-on: ${{ inputs.os }}
env:
NAPARI: ${{ inputs.napari }}
Expand Down Expand Up @@ -92,8 +88,8 @@ jobs:
# shellcheck disable=SC2086
python -m tox $TOX_ARGS -- package/tests/test_PartSegImage $PYTEST_ARGS
env:
PIP_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}{1}.txt', inputs.python_version, inputs.pydantic ) || 'requirements/version_denylist.txt' }}
UV_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}{1}.txt', inputs.python_version, inputs.pydantic ) || 'requirements/version_denylist.txt' }}
PIP_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}.txt', inputs.python_version) || 'requirements/version_denylist.txt' }}
UV_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}.txt', inputs.python_version) || 'requirements/version_denylist.txt' }}

- name: Test with tox PartSegCore
if: ${{ inputs.napari == 'latest' }}
Expand All @@ -102,8 +98,8 @@ jobs:
# shellcheck disable=SC2086
python -m tox $TOX_ARGS -- package/tests/test_PartSegCore $PYTEST_ARGS
env:
PIP_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}{1}.txt', inputs.python_version, inputs.pydantic ) || 'requirements/version_denylist.txt' }}
UV_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}{1}.txt', inputs.python_version, inputs.pydantic ) || 'requirements/version_denylist.txt' }}
PIP_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}.txt', inputs.python_version) || 'requirements/version_denylist.txt' }}
UV_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}.txt', inputs.python_version ) || 'requirements/version_denylist.txt' }}

- name: Test with tox PartSeg
if: ${{ inputs.napari == 'latest' }}
Expand All @@ -113,8 +109,8 @@ jobs:
# shellcheck disable=SC2086
python -m tox $TOX_ARGS -- package/tests/test_PartSeg $PYTEST_ARGS
env:
PIP_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}{1}.txt', inputs.python_version, inputs.pydantic ) || 'requirements/version_denylist.txt' }}
UV_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}{1}.txt', inputs.python_version, inputs.pydantic ) || 'requirements/version_denylist.txt' }}
PIP_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}.txt', inputs.python_version ) || 'requirements/version_denylist.txt' }}
UV_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}.txt', inputs.python_version ) || 'requirements/version_denylist.txt' }}

- name: Test with tox all
if: ${{ inputs.napari != 'latest' }}
Expand All @@ -124,8 +120,8 @@ jobs:
# shellcheck disable=SC2086
python -m tox $TOX_ARGS ${PYTEST_ARGS:+-- $PYTEST_ARGS}
env:
PIP_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}{1}.txt', inputs.python_version, inputs.pydantic ) || 'requirements/version_denylist.txt' }}
UV_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}{1}.txt', inputs.python_version, inputs.pydantic ) || 'requirements/version_denylist.txt' }}
PIP_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}.txt', inputs.python_version ) || 'requirements/version_denylist.txt' }}
UV_CONSTRAINT: ${{ inputs.napari == 'latest' && format('requirements/constraints_py{0}.txt', inputs.python_version ) || 'requirements/version_denylist.txt' }}


- uses: actions/upload-artifact@v7
Expand Down
10 changes: 3 additions & 7 deletions .github/workflows/test_napari_widgets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,13 @@ jobs:
qt_backend: ${{ matrix.qt_backend }}
timeout: 10

test-pyqt5:
test-qt5:
name: PyQt5 ${{ matrix.napari }}
strategy:
fail-fast: false
matrix:
napari: ["napari419", "napari54"]
qt_backend: ["PyQt5"]
include:
- napari: "napari54"
qt_backend: "PySide2"
if: github.event_name == 'push'
napari: ["napari62"]
qt_backend: ["PyQt5", "PySide2"]
uses: ./.github/workflows/base_test_workflow.yml
with:
python_version: "3.10"
Expand Down
10 changes: 0 additions & 10 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,12 @@ jobs:
- python_version: "3.12"
os: "ubuntu-22.04"
qt_backend: "PyQt6"
- python_version: "3.10"
os: "ubuntu-22.04"
qt_backend: "PyQt5"
pydantic: "_pydantic_1"
with:
test_data: True
python_version: ${{ matrix.python_version }}
os: ${{ matrix.os }}
qt_backend: ${{ matrix.qt_backend }}
tox_args: ${{ matrix.tox_args }}
pydantic: ${{ matrix.pydantic }}

base-test-main:
name: Base py${{ matrix.python_version }}
Expand All @@ -110,10 +105,6 @@ jobs:
- python_version: "3.11"
os: "ubuntu-22.04"
qt_backend: "PyQt6"
- python_version: "3.11"
os: "ubuntu-22.04"
qt_backend: "PyQt5"
pydantic: "_pydantic_1"
exclude:
- python_version: "3.11"
qt_backend: "PySide2"
Expand All @@ -126,7 +117,6 @@ jobs:
python_version: ${{ matrix.python_version }}
os: ${{ matrix.os }}
qt_backend: ${{ matrix.qt_backend }}
pydantic: ${{ matrix.pydantic }}
artifact_suffix: "-main"
pytest_args: "-v"

Expand Down
1 change: 0 additions & 1 deletion .github/workflows/upgrade-dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ jobs:

for pyv in 3.10 3.11 3.12 3.13 3.14; do
uv pip compile --python-version ${pyv} --upgrade --output-file requirements/constraints_py${pyv}.txt pyproject.toml requirements/version_denylist.txt "${flags[@]}"
uv pip compile --python-version ${pyv} --upgrade --output-file requirements/constraints_py${pyv}_pydantic_1.txt pyproject.toml requirements/version_denylist.txt "${flags[@]}" --constraint requirements/pydantic_1.txt
done
uv pip compile --python-version 3.12 --upgrade --output-file requirements/constraints_py3.12_docs.txt pyproject.toml --extra docs --extra pyqt6
# END PYTHON DEPENDENCIES
Expand Down
14 changes: 5 additions & 9 deletions package/PartSeg/common_backend/load_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def import_config():
if os.path.exists(state_store.save_folder):
return
version = packaging.version.parse(parsed_version.base_version)
base_folder = os.path.dirname(state_store.save_folder)
base_folder: str = os.path.dirname(state_store.save_folder)
possible_folders = glob(os.path.join(base_folder, "*"))
versions = sorted(
(
Expand All @@ -46,15 +46,11 @@ def import_config():
"Import from old version",
"There is no configuration folder for this version of PartSeg\n"
"Would you like to import it from " + before_name + " version of PartSeg",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.Yes,
)
if resp == QMessageBox.Yes:
if resp == QMessageBox.StandardButton.Yes:
shutil.copytree(os.path.join(base_folder, before_name), state_store.save_folder)
if os.path.exists(os.path.join(state_store.save_folder, IGNORE_FILE)):
os.remove(os.path.join(state_store.save_folder, IGNORE_FILE))
napari_settings = napari_get_settings(state_store.save_folder)
if hasattr(napari_settings, "load") and napari_settings.load is not None:
napari_settings.load()
elif getattr(napari_settings, "_load", None) is not None:
napari_settings._load() # pylint: disable=protected-access
napari_get_settings(state_store.save_folder)
1 change: 1 addition & 0 deletions package/PartSeg/common_gui/algorithms_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ def _any():

class FormWidget(QWidget):
value_changed = Signal()
_model_class: type[BaseModel] | None = None

def __init__(
self,
Expand Down
7 changes: 1 addition & 6 deletions package/PartSeg/common_gui/error_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import sentry_sdk
from napari.settings import get_settings
from napari.utils.theme import get_theme
from packaging.version import parse as parse_version
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import (
QApplication,
Expand Down Expand Up @@ -54,7 +53,6 @@
_FEEDBACK_URL = "https://sentry.io/api/0/projects/{organization_slug}/{project_slug}/user-feedback/".format(
organization_slug="cent", project_slug="partseg"
)
_napari_ge_5 = parse_version(version("napari")) >= parse_version("0.5.0a1")


def _print_traceback(exception, file_):
Expand Down Expand Up @@ -84,10 +82,7 @@ def __init__(self, exception: Exception, description: str, additional_notes: str
self.create_issue_btn = QPushButton("Create issue")
self.cancel_btn = QPushButton("Cancel")
self.error_description = QTextEdit()
if _napari_ge_5:
theme = get_theme(get_settings().appearance.theme)
else:
theme = get_theme(get_settings().appearance.theme, as_dict=False)
theme = get_theme(get_settings().appearance.theme)
self._highlight = Pylighter(self.error_description.document(), "python", theme.syntax_style)
self.traceback_summary = additional_info
if additional_info is None:
Expand Down
20 changes: 4 additions & 16 deletions package/PartSeg/common_gui/napari_image_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,7 @@ def setup_ui(self):
layout.addWidget(self.viewer_widget)

self.setLayout(layout)

if hasattr(self.viewer_widget.canvas, "background_color_override"):
self.viewer_widget.canvas.background_color_override = "black"
self.viewer.scale_bar.color = "white"
self.viewer.scale_bar.colored = True
self.viewer_widget.canvas.background_color_override = "black"

def _connect_to_settings(self):
self.settings.mask_changed.connect(self.set_mask)
Expand Down Expand Up @@ -327,21 +323,13 @@ def update_spacing_info(self, image: Image | None = None) -> None:
image_info.mask.scale = image.normalized_scaling()

def _active_layer(self):
if hasattr(self.viewer.layers, "selection"):
return self.viewer.layers.selection.active
return self.viewer.active_layer
return self.viewer.layers.selection.active

def _coordinates(self):
def _coordinates(self) -> list[int] | None:
active_layer = self._active_layer()
if active_layer is None:
return None
if (
hasattr(self.viewer, "cursor")
and hasattr(self.viewer.cursor, "position")
and hasattr(active_layer, "world_to_data")
):
return [int(x) for x in active_layer.world_to_data(self.viewer.cursor.position)]
return [int(x) for x in active_layer.coordinates]
return [int(x) for x in active_layer.world_to_data(self.viewer.cursor.position)]

def print_info(self, event=None):
cords = self._coordinates()
Expand Down
19 changes: 4 additions & 15 deletions package/PartSeg/plugins/napari_io/loader.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import os
import typing
from importlib.metadata import version

import numpy as np
from packaging.version import parse as parse_version

from PartSeg.plugins.napari_widgets._settings import get_settings
from PartSegCore import UNIT_SCALE
Expand Down Expand Up @@ -40,19 +38,10 @@ def adjust_color(color: str | list[int]) -> str | tuple[float]:
return color


if parse_version(version("napari")) >= parse_version("0.4.19a1"):

def add_color(image: Image, idx: int) -> dict:
return {
"colormap": adjust_color(image.get_colors()[idx]),
}

else:

def add_color(image: Image, idx: int) -> dict: # noqa: ARG001
# Do nothing, as napari is not able to pass hex color to image
# the image and idx are present to keep the same signature
return {}
def add_color(image: Image, idx: int) -> dict:
return {
"colormap": adjust_color(image.get_colors()[idx]),
}


def _image_to_layers(project_info, scale, translate):
Expand Down
7 changes: 4 additions & 3 deletions package/PartSeg/plugins/napari_widgets/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from magicgui.widgets import Container, create_widget
from napari.layers import Layer
from platformdirs import user_config_dir

from PartSeg._roi_analysis.partseg_settings import PartSettings
from PartSeg.common_backend import napari_get_settings
Expand Down Expand Up @@ -43,10 +44,10 @@ def get_settings() -> PartSegNapariSettings:
global _SETTINGS # noqa: PLW0603 # pylint: disable=global-statement
if _SETTINGS is None:
napari_settings = napari_get_settings()
if hasattr(napari_settings, "path"):
save_path = napari_settings.path
else:
if napari_settings.config_path is not None:
save_path = os.path.dirname(napari_settings.config_path)
else:
save_path = user_config_dir("napari", False) # pragma: no cover
_SETTINGS = PartSegNapariSettings(os.path.join(save_path, "PartSeg_napari_plugins"))
_SETTINGS.load()
return _SETTINGS
Expand Down
4 changes: 0 additions & 4 deletions package/PartSeg/plugins/napari_widgets/lables_control.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
from collections.abc import Sequence
from importlib.metadata import version

from napari import Viewer
from napari.layers import Labels
from napari.utils.colormaps import DirectLabelColormap
from packaging.version import parse as parse_version
from qtpy.QtWidgets import QHBoxLayout, QPushButton, QTabWidget

from PartSeg.common_backend.base_settings import BaseSettings
from PartSeg.common_gui.label_create import LabelChoose, LabelEditor, LabelShow
from PartSeg.plugins.napari_widgets._settings import get_settings

NAPARI_GE_5_0 = parse_version(version("napari")) >= parse_version("0.5.0a1")


class NapariLabelShow(LabelShow):
def __init__(self, viewer: Viewer, name: str, label: list[Sequence[float]], removable, parent=None):
Expand Down
2 changes: 1 addition & 1 deletion package/PartSegCore/analysis/measurement_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ def replace(self, **kwargs) -> Leaf:
if getattr(self, key) is not None and (key != "parameters" or dict(self.parameters)):
del kwargs[key]

return self.copy(update=kwargs)
return self.model_copy(update=kwargs)


Leaf.replace_ = replace
Expand Down
2 changes: 1 addition & 1 deletion package/PartSegCore/mask_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def _fill_holes(mask_description: MaskProperty, mask: np.ndarray) -> np.ndarray:
if mask_description.save_components:
border = 1
res_slice = tuple(slice(border, -border) for _ in range(mask.ndim))
mask_description_copy = mask_description.copy(update={"save_components": False})
mask_description_copy = mask_description.model_copy(update={"save_components": False})
mask_prohibited = mask > 0
for component, slice_arr, cmp_num in _cut_components(mask, mask, border):
mask_prohibited_component = mask_prohibited[slice_arr]
Expand Down
10 changes: 8 additions & 2 deletions package/PartSegCore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,12 +444,18 @@ class Config:
extra = "forbid"

def __getitem__(self, item):
if item in self.__fields__:
if item in self.__class__.model_fields:
warnings.warn("Access to attribute by [] is deprecated. Use . instead", FutureWarning, stacklevel=2)
return getattr(self, item)
raise KeyError(f"{item} not found in {self.__class__.__name__}")

def copy(self: PydanticBaseModel, *, validate: bool = True, **kwargs: typing.Any) -> PydanticBaseModel:
def model_copy(self: typing.Self, *, validate: bool = True, **kwargs: typing.Any) -> typing.Self:
copy_res = super().model_copy(**kwargs)
if validate:
return self.__class__(**{name: getattr(copy_res, name) for name in copy_res.model_fields_set})
return copy_res

def copy(self: typing.Self, *, validate: bool = True, **kwargs: typing.Any) -> typing.Self:
copy_res = super().copy(**kwargs)
if validate:
return self.validate(
Expand Down
Loading
Loading