diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b2375e71..b0403549 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -124,9 +124,7 @@ python scripts/run_with_env.py python -m ruff format python scripts/run_with_env.py python -m ruff check --fix # Pylint -python scripts/run_with_env.py python -m pylint sigima \ - --disable=duplicate-code,fixme,too-many-arguments, \ - too-many-branches,too-many-instance-attributes +python scripts/run_with_env.py python -m pylint sigima ``` ### Translations diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml index 77ff75d5..f9f4be2e 100644 --- a/.github/workflows/build_deploy.yml +++ b/.github/workflows/build_deploy.yml @@ -17,9 +17,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: '3.x' - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0ae6745c..c8ba9d25 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,7 @@ on: options: - 'all' - 'build' + - 'quality' - 'build_latest' default: 'all' schedule: @@ -28,7 +29,7 @@ on: jobs: build: - if: ${{ (github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'release') || (github.event_name == 'workflow_dispatch' && (github.event.inputs.job_to_run == 'all' || github.event.inputs.job_to_run == 'build')) }} + if: ${{ (github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'release' || github.event_name == 'workflow_call') || (github.event_name == 'workflow_dispatch' && (github.event.inputs.job_to_run == 'all' || github.event.inputs.job_to_run == 'build')) }} env: DISPLAY: ':99.0' @@ -40,9 +41,9 @@ jobs: python-version: ["3.9", "3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -87,6 +88,37 @@ jobs: - name: Test with pytest run: pytest -v --tb=long + quality: + name: Quality (visualization and Pylint) + if: ${{ (github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'release' || github.event_name == 'workflow_call') || (github.event_name == 'workflow_dispatch' && (github.event.inputs.job_to_run == 'all' || github.event.inputs.job_to_run == 'quality')) }} + env: + DISPLAY: ':99.0' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + - name: Set up Python 3.13 + uses: actions/setup-python@v7 + with: + python-version: "3.13" + - name: Install quality dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \ + libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils + /sbin/start-stop-daemon --start --quiet \ + --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background \ + --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX + python -m pip install --upgrade pip + python -m pip install -e ".[dev,test,qt]" matplotlib + - name: Verify PlotPy backend + run: python -c "import plotpy; print(f'PlotPy loaded from {plotpy.__file__}')" + - name: Test visualization backends + run: python -m pytest -o addopts="--import-mode=importlib" sigima/tests/viz -vv + - name: Lint with Pylint + run: python -m pylint sigima + build_latest: if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && (github.event.inputs.job_to_run == 'all' || github.event.inputs.job_to_run == 'build_latest')) }} env: @@ -94,10 +126,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python 3.13 - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.13" diff --git a/.pylintrc b/.pylintrc index 5ad83e90..0cea9264 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,10 +4,23 @@ max-line-length=88 [TYPECHECK] -ignored-modules=qtpy.QtWidgets,qtpy.QtCore,qtpy.QtGui,cv2,plotpy._scaler,skimage.restoration,skimage.feature +# NumPy's floating-point limits are populated dynamically. +ignored-classes=finfo +ignored-modules=qtpy.QtWidgets,qtpy.QtCore,qtpy.QtGui,cv2,plotpy._scaler,scipy.special,skimage.draw,skimage.restoration,skimage.feature [MESSAGES CONTROL] -disable=wrong-import-order +# Ruff owns import ordering. The remaining exclusions reflect accepted complexity in +# numerical algorithms, compatibility layers, and parallel object APIs. +disable=duplicate-code, + fixme, + too-many-arguments, + too-many-branches, + too-many-instance-attributes, + too-many-lines, + too-many-locals, + too-many-public-methods, + too-many-statements, + wrong-import-order [DESIGN] max-args=10 # default: 5 diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7e0e651d..cb9ff6cd 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -94,15 +94,6 @@ "-m", "pylint", "sigima", - "--disable=duplicate-code", - "--disable=fixme", - "--disable=too-many-arguments", - "--disable=too-many-branches", - "--disable=too-many-instance-attributes", - "--disable=too-many-lines", - "--disable=too-many-locals", - "--disable=too-many-public-methods", - "--disable=too-many-statements", ], "options": { "cwd": "${workspaceFolder}", diff --git a/doc/contributing/gitworkflow.rst b/doc/contributing/gitworkflow.rst index 1f146dcf..bf87348c 100644 --- a/doc/contributing/gitworkflow.rst +++ b/doc/contributing/gitworkflow.rst @@ -87,6 +87,36 @@ This naming convention improves clarity by clearly separating documentation efforts from code-related development (features, fixes, etc.). +Commit Message Convention +------------------------- + +New commits and pull request titles should follow the `Conventional Commits +`_ format:: + + [optional scope]: + +Use the type that best describes the change: + +- ``feat`` for a new capability; +- ``fix`` for a bug fix; +- ``test`` for test-only changes; +- ``docs`` for documentation; +- ``ci`` for continuous-integration configuration; +- ``refactor`` for a behavior-preserving code change; +- ``chore`` for maintenance work not covered above. + +For example:: + + fix(roi): preserve inverse ROI rendering + test(viz): cover PlotPy ROI coordinates + ci: add Pylint quality gate + +Merge commits and annotated release tags are exceptions: keep their explicit Git +messages, such as ``Merge branch 'feature/feature_name'`` and +``Release version 1.2.0``. Do not rewrite published history solely to change old +messages. + + Workflow for New Features ------------------------- @@ -95,7 +125,7 @@ Workflow for New Features .. code-block:: sh git checkout develop - git checkout -b develop/feature_name + git checkout -b feature/feature_name 2. Develop the feature and commit changes. @@ -104,13 +134,13 @@ Workflow for New Features .. code-block:: sh git checkout develop - git merge --no-ff develop/feature_name + git merge --no-ff feature/feature_name 4. Delete the feature branch: .. code-block:: sh - git branch -d develop/feature_name + git branch -d feature/feature_name .. warning:: @@ -170,13 +200,13 @@ For current maintenance release (target: ``release``): .. warning:: - Do not create a ``fix/xxx`` branch from a ``develop/feature_name`` branch. + Do not create a ``fix/xxx`` branch from a ``feature/feature_name`` branch. Always branch from ``develop`` or ``release`` to ensure fixes are correctly propagated. .. code-block:: sh # Incorrect: - git checkout develop/feature_name + git checkout feature/feature_name git checkout -b fix/wrong_branch .. code-block:: sh @@ -264,14 +294,27 @@ When ready to release a new minor or major version (e.g., 1.1.0, 2.0.0): git tag -a v1.1.0 -m "Release version 1.1.0" git push origin main --tags -3. Delete the old ``release`` branch (if exists): +3. Fast-forward ``develop`` to the released ``main`` commit: + + .. code-block:: sh + + git checkout develop + git merge --ff-only main + git push origin develop + + This makes the release merge commit, its conflict resolutions, and the tag + reachable from ``develop`` without creating a redundant merge commit. If the + fast-forward fails, stop and reconcile ``main`` and ``develop`` through a + reviewed merge; never rebase or force-push either shared branch. + +4. Delete the old ``release`` branch (if exists): .. code-block:: sh git branch -d release git push origin --delete release -4. Create a new ``release`` branch from ``main`` when the first patch for 1.1.1 is needed: +5. Create a new ``release`` branch from ``main`` when the first patch for 1.1.1 is needed: .. code-block:: sh @@ -286,8 +329,8 @@ Best Practices .. code-block:: sh - git checkout develop/feature_name - git rebase develop + git checkout feature/feature_name + git rebase develop - Avoid long-lived branches to minimize merge conflicts. diff --git a/doc/contributing/guidelines.rst b/doc/contributing/guidelines.rst index b5d09640..7a3de966 100644 --- a/doc/contributing/guidelines.rst +++ b/doc/contributing/guidelines.rst @@ -10,7 +10,7 @@ We follow the `PEP 8 `_ coding style. In particular, we are especially strict about the following guidelines: -- Limit all lines to a maximum of 79 characters. +- Limit all lines to a maximum of 88 characters. - Respect the naming conventions (classes, functions, variables, etc.). - Use specific exceptions instead of the generic :class:`Exception`. @@ -23,26 +23,31 @@ ruff ^^^^ If you are using `Visual Studio Code `_, -the project settings will automatically format your code with `ruff` on save -(you may also run the task "Run Ruff" to run `ruff` on the project). +the project settings will automatically format your code with `ruff` on save. +You may also run the Ruff tasks on the project. -To run `ruff`, run the following command:: +To format and check the code with `ruff`, run the following commands from the +repository root:: - ruff check + python scripts/run_with_env.py python -m ruff format + python scripts/run_with_env.py python -m ruff check pylint ^^^^^^ -To run `pylint`, run the following command:: +To run `pylint`, run the following command from the repository root:: - pylint sigima + python scripts/run_with_env.py python -m pylint sigima If you are using `Visual Studio Code `_ -on Windows, you may run the task "Run Pylint" to run `pylint` on the project. +on Windows, you may run the "Pylint" task on the project. The enabled checks +and accepted exclusions are defined in the repository's ``.pylintrc`` file. .. note:: - A `pylint` rating greater than 9/10 is required to merge a pull request. + The Ruff and Pylint checks must both exit successfully before a pull request + may be merged. Pylint's numeric rating is informative only: the merge gate is + based on the absence of diagnostics from the enabled checks. Specific coding guidelines -------------------------- diff --git a/doc/locale/fr/LC_MESSAGES/contributing/gitworkflow.po b/doc/locale/fr/LC_MESSAGES/contributing/gitworkflow.po index 9bd732f3..a116e17f 100644 --- a/doc/locale/fr/LC_MESSAGES/contributing/gitworkflow.po +++ b/doc/locale/fr/LC_MESSAGES/contributing/gitworkflow.po @@ -114,6 +114,42 @@ msgstr "``doc/user-guide``" msgid "This naming convention improves clarity by clearly separating documentation efforts from code-related development (features, fixes, etc.)." msgstr "Cette convention de nommage améliore la clarté en séparant clairement les efforts de documentation du développement lié au code (fonctionnalités, corrections, etc.)." +msgid "Commit Message Convention" +msgstr "Convention des messages de commit" + +msgid "New commits and pull request titles should follow the `Conventional Commits `_ format::" +msgstr "Les nouveaux commits et les titres de demandes d'extraction doivent suivre le format `Conventional Commits `_::" + +msgid "Use the type that best describes the change:" +msgstr "Utiliser le type qui décrit le mieux la modification :" + +msgid "``feat`` for a new capability;" +msgstr "``feat`` pour une nouvelle fonctionnalité ;" + +msgid "``fix`` for a bug fix;" +msgstr "``fix`` pour une correction d'anomalie ;" + +msgid "``test`` for test-only changes;" +msgstr "``test`` pour des modifications concernant uniquement les tests ;" + +msgid "``docs`` for documentation;" +msgstr "``docs`` pour la documentation ;" + +msgid "``ci`` for continuous-integration configuration;" +msgstr "``ci`` pour la configuration de l'intégration continue ;" + +msgid "``refactor`` for a behavior-preserving code change;" +msgstr "``refactor`` pour une modification de code qui préserve le comportement ;" + +msgid "``chore`` for maintenance work not covered above." +msgstr "``chore`` pour les travaux de maintenance non couverts ci-dessus." + +msgid "For example::" +msgstr "Par exemple::" + +msgid "Merge commits and annotated release tags are exceptions: keep their explicit Git messages, such as ``Merge branch 'feature/feature_name'`` and ``Release version 1.2.0``. Do not rewrite published history solely to change old messages." +msgstr "Les commits de fusion et les tags de version annotés font exception : conserver leurs messages Git explicites, tels que ``Merge branch 'feature/feature_name'`` et ``Release version 1.2.0``. Ne pas réécrire l'historique publié uniquement pour modifier d'anciens messages." + msgid "Workflow for New Features" msgstr "Workflow pour les nouvelles fonctionnalités" @@ -159,8 +195,8 @@ msgstr "Créer une branche de correction d'anomalie à partir de ``release`` :" msgid "Merge the fix branch back into ``release``:" msgstr "Fusionner la branche de correction de nouveau dans ``release`` :" -msgid "Do not create a ``fix/xxx`` branch from a ``develop/feature_name`` branch. Always branch from ``develop`` or ``release`` to ensure fixes are correctly propagated." -msgstr "Ne pas créer une branche ``fix/xxx`` à partir d'une branche ``develop/feature_name``. Toujours créer une branche à partir de ``develop`` ou ``release`` pour garantir que les corrections sont correctement propagées." +msgid "Do not create a ``fix/xxx`` branch from a ``feature/feature_name`` branch. Always branch from ``develop`` or ``release`` to ensure fixes are correctly propagated." +msgstr "Ne pas créer une branche ``fix/xxx`` à partir d'une branche ``feature/feature_name``. Toujours créer une branche à partir de ``develop`` ou ``release`` pour garantir que les corrections sont correctement propagées." msgid "Workflow for Critical Hotfixes" msgstr "Workflow pour les correctifs urgents" @@ -207,6 +243,12 @@ msgstr "Lorsque vous êtes prêt à publier une nouvelle version mineure ou maje msgid "Merge ``develop`` into ``main``:" msgstr "Fusionner ``develop`` dans ``main`` :" +msgid "Fast-forward ``develop`` to the released ``main`` commit:" +msgstr "Avancer ``develop`` en fast-forward jusqu'au commit publié de ``main`` :" + +msgid "This makes the release merge commit, its conflict resolutions, and the tag reachable from ``develop`` without creating a redundant merge commit. If the fast-forward fails, stop and reconcile ``main`` and ``develop`` through a reviewed merge; never rebase or force-push either shared branch." +msgstr "Cette opération rend le commit de fusion de la version, ses résolutions de conflits et le tag accessibles depuis ``develop`` sans créer de commit de fusion redondant. Si le fast-forward échoue, arrêter l'opération et réconcilier ``main`` et ``develop`` au moyen d'une fusion revue ; ne jamais rebaser ni forcer la publication de l'une de ces branches partagées." + msgid "Delete the old ``release`` branch (if exists):" msgstr "Supprimer l'ancienne branche ``release`` (si elle existe) :" diff --git a/doc/locale/fr/LC_MESSAGES/contributing/guidelines.po b/doc/locale/fr/LC_MESSAGES/contributing/guidelines.po index f43b662d..7bf13126 100644 --- a/doc/locale/fr/LC_MESSAGES/contributing/guidelines.po +++ b/doc/locale/fr/LC_MESSAGES/contributing/guidelines.po @@ -22,8 +22,8 @@ msgstr "Nous suivons le style de codage `PEP 8 `_ pour l'analyse statique du msgid "ruff" msgstr "ruff" -msgid "If you are using `Visual Studio Code `_, the project settings will automatically format your code with `ruff` on save (you may also run the task \"Run Ruff\" to run `ruff` on the project)." -msgstr "Si vous utilisez `Visual Studio Code `_, les paramètres du projet formateront automatiquement votre code avec `ruff` à l'enregistrement (vous pouvez également exécuter la tâche \"Run Ruff\" pour exécuter `ruff` sur le projet)." +msgid "If you are using `Visual Studio Code `_, the project settings will automatically format your code with `ruff` on save. You may also run the Ruff tasks on the project." +msgstr "Si vous utilisez `Visual Studio Code `_, les paramètres du projet formateront automatiquement votre code avec `ruff` à l'enregistrement. Vous pouvez également exécuter les tâches Ruff sur le projet." -msgid "To run `ruff`, run the following command::" -msgstr "Pour exécuter `ruff`, exécutez la commande suivante::" +msgid "To format and check the code with `ruff`, run the following commands from the repository root::" +msgstr "Pour formater et vérifier le code avec `ruff`, exécutez les commandes suivantes depuis la racine du dépôt::" msgid "pylint" msgstr "pylint" -msgid "To run `pylint`, run the following command::" -msgstr "Pour exécuter `pylint`, exécutez la commande suivante::" +msgid "To run `pylint`, run the following command from the repository root::" +msgstr "Pour exécuter `pylint`, lancez la commande suivante depuis la racine du dépôt::" -msgid "If you are using `Visual Studio Code `_ on Windows, you may run the task \"Run Pylint\" to run `pylint` on the project." -msgstr "Si vous utilisez `Visual Studio Code `_ sur Windows, vous pouvez exécuter la tâche \"Run Pylint\" pour exécuter `pylint` sur le projet." +msgid "If you are using `Visual Studio Code `_ on Windows, you may run the \"Pylint\" task on the project. The enabled checks and accepted exclusions are defined in the repository's ``.pylintrc`` file." +msgstr "Si vous utilisez `Visual Studio Code `_ sous Windows, vous pouvez exécuter la tâche \"Pylint\" sur le projet. Les vérifications activées et les exclusions acceptées sont définies dans le fichier ``.pylintrc`` du dépôt." -msgid "A `pylint` rating greater than 9/10 is required to merge a pull request." -msgstr "Une note `pylint` supérieure à 9/10 est requise pour fusionner une demande d'extraction." +msgid "The Ruff and Pylint checks must both exit successfully before a pull request may be merged. Pylint's numeric rating is informative only: the merge gate is based on the absence of diagnostics from the enabled checks." +msgstr "Les vérifications Ruff et Pylint doivent toutes deux se terminer avec succès avant qu'une demande d'extraction puisse être fusionnée. La note numérique de Pylint est uniquement informative : l'autorisation de fusion repose sur l'absence de diagnostics issus des vérifications activées." msgid "Specific coding guidelines" msgstr "Règles de codage spécifiques" diff --git a/sigima/tests/signal/stability_unit_test.py b/sigima/tests/signal/stability_unit_test.py index 68746b69..4b307483 100644 --- a/sigima/tests/signal/stability_unit_test.py +++ b/sigima/tests/signal/stability_unit_test.py @@ -45,8 +45,8 @@ import sigima.objects import sigima.params import sigima.proc.signal -import sigima.tools.signal.stability as stability from sigima.tests.helpers import check_array_result +from sigima.tools.signal import stability #: Number of samples used by the exact-expectation references. Kept small on purpose: #: the trace is accumulated by looping over the canonical basis, so the cost is O(N²). diff --git a/sigima/tests/viz/roi_items_unit_test.py b/sigima/tests/viz/roi_items_unit_test.py new file mode 100644 index 00000000..5e192a2a --- /dev/null +++ b/sigima/tests/viz/roi_items_unit_test.py @@ -0,0 +1,73 @@ +# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. + +""" +Test creation of PlotPy ROI items from an :class:`sigima.objects.ImageObj`. + +Regression test for the inverse rectangular ROI rendering bug: the PlotPy ROI +item must be built from the rectangle's own shape, not from the extraction +bounding box (which, for an inverse ROI, covers the whole image). +""" + +# pylint: disable=import-outside-toplevel + +from __future__ import annotations + +import importlib.util + +import numpy as np +import pytest + +import sigima.objects +from sigima.objects import ImageObj + + +def _make_image() -> ImageObj: + """Return a 100x100 float64 image with a unit coordinate system.""" + obj = ImageObj(title="Test") + obj.data = np.zeros((100, 100), dtype=np.float64) + obj.set_uniform_coords(1.0, 1.0, 0.0, 0.0) + return obj + + +class _RecordingBuilder: + """Minimal ``make`` stub recording the rectangle coordinates.""" + + def __init__(self) -> None: + self.rect_coords: tuple[float, float, float, float] | None = None + + def annotated_rectangle( + self, + x0: float, + y0: float, + x1: float, + y1: float, + _title: str = "", + ) -> object: + """Record the rectangle coordinates and return a sentinel item.""" + self.rect_coords = (x0, y0, x1, y1) + return object() + + +@pytest.mark.skipif( + importlib.util.find_spec("plotpy") is None, reason="PlotPy not installed" +) +@pytest.mark.parametrize("inverse", [False, True]) +def test_rectangular_roi_item_uses_shape_not_extraction_box( + monkeypatch: pytest.MonkeyPatch, inverse: bool +) -> None: + """Render a rectangular ROI as its own shape, even when inverse.""" + from sigima.viz import viz_plotpy + + builder = _RecordingBuilder() + monkeypatch.setattr(viz_plotpy, "make", builder) + + obj = _make_image() + obj.roi = sigima.objects.create_image_roi( + "rectangle", [20.0, 30.0, 40.0, 25.0], inverse=inverse + ) + + create_items = getattr(viz_plotpy, "__create_image_roi_items") + items = create_items(obj) + + assert len(items) == 1 + assert builder.rect_coords == (20.0, 30.0, 60.0, 55.0) diff --git a/sigima/tools/signal/replace_values.py b/sigima/tools/signal/replace_values.py index dd7035db..a80db6a6 100644 --- a/sigima/tools/signal/replace_values.py +++ b/sigima/tools/signal/replace_values.py @@ -227,8 +227,8 @@ def interpolate_masked( if interp_method == Interpolation1DMethod.LINEAR: y[mask] = np.interp(x_masked, x_valid, y_valid) elif interp_method == Interpolation1DMethod.SPLINE: - knots, coeffs, degree = scipy.interpolate.splrep(x_valid, y_valid, s=0) - y[mask] = scipy.interpolate.splev(x_masked, (knots, coeffs, degree), der=0) + spline = scipy.interpolate.splrep(x_valid, y_valid, s=0) + y[mask] = scipy.interpolate.splev(x_masked, spline, der=0) elif interp_method == Interpolation1DMethod.QUADRATIC: coeffs = np.polyfit(x_valid, y_valid, min(2, len(x_valid) - 1)) y[mask] = np.polyval(coeffs, x_masked) diff --git a/sigima/viz/__init__.py b/sigima/viz/__init__.py index df484576..d8856122 100644 --- a/sigima/viz/__init__.py +++ b/sigima/viz/__init__.py @@ -215,11 +215,6 @@ def create_marker(x: float, y: float, title: str | None = None) -> Any: return object() -# Determine which backend to use -_BACKEND_NAME: str | None = None -_BACKEND_SOURCE: str = "auto" - - def _select_backend() -> tuple[str, str]: """Select visualization backend based on configuration and availability. @@ -300,8 +295,8 @@ def _select_backend() -> tuple[str, str]: # Lazy backend initialization - deferred until first attribute access _BACKEND_MODULE = None -_BACKEND_NAME = None -_BACKEND_SOURCE = None +_BACKEND_NAME: str | None = None +_BACKEND_SOURCE: str | None = None _INITIALIZING = False # Flag to prevent recursion # Public API: Set default values for BACKEND_NAME and BACKEND_SOURCE