diff --git a/.isort.cfg b/.isort.cfg
deleted file mode 100644
index 5ff59aac..00000000
--- a/.isort.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[settings]
-known_first_party=plotpy
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b364d843..180b05d7 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -18,25 +18,12 @@
"files.trimTrailingWhitespace": true,
"python.defaultInterpreterPath": "${env:PPSTACK_PYTHONEXE}",
"editor.formatOnSave": true,
- "isort.args": [
- "--profile",
- "black"
- ],
- "[python]": {
- "editor.codeActionsOnSave": {
- "source.organizeImports": "explicit"
- },
- "editor.defaultFormatter": "ms-python.black-formatter"
- },
+ "python.analysis.autoFormatStrings": true,
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.testing.pytestPath": "pytest",
- "python.testing.pytestArgs": [
- "-v",
- "--cov",
- "--cov-report=html",
- "plotpy"
- ],
- "python.analysis.autoFormatStrings": true,
- "python.analysis.typeCheckingMode": "off",
+ "python.testing.pytestArgs": [],
+ "[python]": {
+ "editor.defaultFormatter": "charliermarsh.ruff"
+ },
}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 2f01c6c2..8677de6c 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -149,6 +149,34 @@
"clear": true
}
},
+ {
+ "label": "Run Ruff",
+ "type": "shell",
+ "command": "cmd",
+ "args": [
+ "/c",
+ "run_ruff.bat",
+ ],
+ "options": {
+ "cwd": "scripts",
+ "env": {
+ "PYTHON": "${env:PPSTACK_PYTHONEXE}",
+ "UNATTENDED": "1"
+ }
+ },
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "presentation": {
+ "echo": true,
+ "reveal": "always",
+ "focus": false,
+ "panel": "dedicated",
+ "showReuseMessage": true,
+ "clear": true
+ }
+ },
{
"label": "Clean Up",
"type": "shell",
diff --git a/doc/dev/build.rst b/doc/dev/build.rst
index 7997c97a..42a595e8 100644
--- a/doc/dev/build.rst
+++ b/doc/dev/build.rst
@@ -39,8 +39,7 @@ To run test with coverage support, use the following command::
Code formatting
^^^^^^^^^^^^^^^
-The code is formatted with `black `_
-and `isort `_.
+The code is formatted with `ruff `_.
If you are using `Visual Studio Code `_,
the formatting is done automatically when you save a file, thanks to the
diff --git a/doc/intro/motivation.rst b/doc/intro/motivation.rst
index a3eb0ce2..df1948a7 100644
--- a/doc/intro/motivation.rst
+++ b/doc/intro/motivation.rst
@@ -9,7 +9,7 @@ What are PlotPy V2 advantages over PlotPy V1?
From a developer point of view, PlotPy V2 is a major overhaul of PlotPy V1:
* Architecture has been redesigned to be more modular and extensible, and more simple.
-* Code quality has been improved introducing `black`, `isort` and typing annotations
+* Code quality has been improved introducing `ruff` and typing annotations
all over the codebase
.. note::
diff --git a/plotpy/external/sliders/_labeled.py b/plotpy/external/sliders/_labeled.py
index 84bf0b4f..8aeb480b 100644
--- a/plotpy/external/sliders/_labeled.py
+++ b/plotpy/external/sliders/_labeled.py
@@ -22,7 +22,6 @@
)
from ._misc import signals_blocked
-
from ._sliders import QDoubleRangeSlider, QDoubleSlider, QRangeSlider
@@ -133,14 +132,12 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
_slider: QSlider
@overload
- def __init__(self, parent: QWidget | None = ...) -> None:
- ...
+ def __init__(self, parent: QWidget | None = ...) -> None: ...
@overload
def __init__(
self, orientation: Qt.Orientation, parent: QWidget | None = ...
- ) -> None:
- ...
+ ) -> None: ...
def __init__(self, *args: Any, **kwargs: Any) -> None:
parent, orientation = _handle_overloaded_slider_sig(args, kwargs)
@@ -263,14 +260,12 @@ class QLabeledDoubleSlider(QLabeledSlider):
_frangeChanged = Signal(float, float)
@overload
- def __init__(self, parent: QWidget | None = ...) -> None:
- ...
+ def __init__(self, parent: QWidget | None = ...) -> None: ...
@overload
def __init__(
self, orientation: Qt.Orientation, parent: QWidget | None = ...
- ) -> None:
- ...
+ ) -> None: ...
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
@@ -300,14 +295,12 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
_slider: QRangeSlider
@overload
- def __init__(self, parent: QWidget | None = ...) -> None:
- ...
+ def __init__(self, parent: QWidget | None = ...) -> None: ...
@overload
def __init__(
self, orientation: Qt.Orientation, parent: QWidget | None = ...
- ) -> None:
- ...
+ ) -> None: ...
def __init__(self, *args: Any, **kwargs: Any) -> None:
parent, orientation = _handle_overloaded_slider_sig(args, kwargs)
@@ -545,14 +538,12 @@ class QLabeledDoubleRangeSlider(QLabeledRangeSlider):
_frangeChanged = Signal(float, float)
@overload
- def __init__(self, parent: QWidget | None = ...) -> None:
- ...
+ def __init__(self, parent: QWidget | None = ...) -> None: ...
@overload
def __init__(
self, orientation: Qt.Orientation, parent: QWidget | None = ...
- ) -> None:
- ...
+ ) -> None: ...
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
diff --git a/plotpy/io.py b/plotpy/io.py
index fcf58d5c..195e5cce 100644
--- a/plotpy/io.py
+++ b/plotpy/io.py
@@ -374,7 +374,7 @@ def _import_dcm():
# is to check if pydicom is installed:
# pylint: disable=import-outside-toplevel
# pylint: disable=import-error
- from pydicom import dicomio # type:ignore
+ from pydicom import dicomio # type:ignore # noqa: F401
logger.setLevel(logging.WARNING)
diff --git a/plotpy/items/label.py b/plotpy/items/label.py
index 112daa38..2db16dbb 100644
--- a/plotpy/items/label.py
+++ b/plotpy/items/label.py
@@ -706,7 +706,7 @@ def get_legend_size(
"""
width = 0
height = 0
- for text, _, _, _ in legenditems:
+ for text, _, _, _ in legenditems: # noqa
sz = text.size()
if sz.width() > width:
width = sz.width()
diff --git a/plotpy/tests/__init__.py b/plotpy/tests/__init__.py
index 1f82c562..8847f00c 100644
--- a/plotpy/tests/__init__.py
+++ b/plotpy/tests/__init__.py
@@ -12,7 +12,7 @@
from guidata.configtools import get_module_data_path
-import plotpy
+import plotpy # noqa: F401
TESTDATAPATH = get_module_data_path("plotpy", osp.join("tests", "data"))
diff --git a/plotpy/tests/features/test_autoscale_shapes.py b/plotpy/tests/features/test_autoscale_shapes.py
index 434d43d7..144a3341 100644
--- a/plotpy/tests/features/test_autoscale_shapes.py
+++ b/plotpy/tests/features/test_autoscale_shapes.py
@@ -11,9 +11,7 @@
from guidata.qthelpers import qt_app_context
from plotpy.builder import make
-from plotpy.items import PolygonShape
from plotpy.plot import PlotDialog
-from plotpy.styles import ShapeParam
from plotpy.tools import (
AnnotatedCircleTool,
AnnotatedEllipseTool,
diff --git a/plotpy/tests/features/test_colormap_editor.py b/plotpy/tests/features/test_colormap_editor.py
index 8138688f..83cb1ae9 100644
--- a/plotpy/tests/features/test_colormap_editor.py
+++ b/plotpy/tests/features/test_colormap_editor.py
@@ -59,8 +59,8 @@ def test_colormap_editor() -> None:
cmap_tuples = tuple((int(val * 255 + 1), color) for val, color in cmap_tuples)
print(
"Initialization of a new default colormap editor, "
- "modified post-initialization with the previous colormap with stops scaled by "
- "255 + 1: ",
+ "modified post-initialization with the previous colormap "
+ "with stops scaled by 255 + 1: ",
cmap_tuples,
)
new_cmap = EditableColormap.from_iterable(cmap_tuples)
@@ -70,8 +70,8 @@ def test_colormap_editor() -> None:
print(
"Initialization of a new default colormap editor, "
- "modified post-initialization with the previous colormap where the red stop is "
- "replaced with a green stop: ",
+ "modified post-initialization with the previous colormap "
+ "where the red stop is replaced with a green stop: ",
cmap_tuples,
)
diff --git a/plotpy/tests/features/test_imagefilter.py b/plotpy/tests/features/test_imagefilter.py
index 977e84b5..08f43164 100644
--- a/plotpy/tests/features/test_imagefilter.py
+++ b/plotpy/tests/features/test_imagefilter.py
@@ -32,7 +32,11 @@ def imshow(x, y, data, filter_area, yreverse=True):
plot = win.manager.get_plot()
plot.add_item(image)
xmin, xmax, ymin, ymax = filter_area
- ifilter = lambda x, y, data: gaussian_filter(data, 5)
+
+ def ifilter(x, y, data):
+ """Image filter function"""
+ return gaussian_filter(data, 5)
+
flt = make.imagefilter(xmin, xmax, ymin, ymax, image, filter=ifilter)
plot.add_item(flt, z=1)
plot.replot()
diff --git a/plotpy/tests/items/test_svgshapes.py b/plotpy/tests/items/test_svgshapes.py
index 59c6db40..60d33003 100644
--- a/plotpy/tests/items/test_svgshapes.py
+++ b/plotpy/tests/items/test_svgshapes.py
@@ -7,8 +7,6 @@
# guitest: show
-import os.path as osp
-
from guidata.qthelpers import qt_app_context
from plotpy.builder import make
diff --git a/plotpy/tests/tools/test_get_segment.py b/plotpy/tests/tools/test_get_segment.py
index c7ff5405..55fcc758 100644
--- a/plotpy/tests/tools/test_get_segment.py
+++ b/plotpy/tests/tools/test_get_segment.py
@@ -17,7 +17,6 @@
from guidata.qthelpers import qt_app_context
from plotpy.builder import make
-from plotpy.config import _
from plotpy.coords import axes_to_canvas
from plotpy.tools import AnnotatedSegmentTool
from plotpy.widgets.selectdialog import SelectDialog, select_with_shape_tool
diff --git a/plotpy/tests/unit/test_aspect_ratio_tool.py b/plotpy/tests/unit/test_aspect_ratio_tool.py
index 9ce7b363..e9f4e4d9 100644
--- a/plotpy/tests/unit/test_aspect_ratio_tool.py
+++ b/plotpy/tests/unit/test_aspect_ratio_tool.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import qtpy.QtCore as QC
from guidata.qthelpers import exec_dialog, qt_app_context
from plotpy.interfaces.items import IImageItemType
diff --git a/plotpy/tests/unit/test_baseplot.py b/plotpy/tests/unit/test_baseplot.py
index e9e591eb..6cac877c 100644
--- a/plotpy/tests/unit/test_baseplot.py
+++ b/plotpy/tests/unit/test_baseplot.py
@@ -15,7 +15,7 @@
from plotpy.tests import vistools as ptv
from plotpy.tests.features.test_auto_curve_image import make_curve_image_legend
-from plotpy.tools.curve import DownSamplingTool, EditPointTool, SelectPointsTool
+from plotpy.tools.curve import EditPointTool, SelectPointsTool
def test_baseplot_api():
diff --git a/plotpy/tests/unit/test_builder_shape.py b/plotpy/tests/unit/test_builder_shape.py
index 3c74a393..b9810f38 100644
--- a/plotpy/tests/unit/test_builder_shape.py
+++ b/plotpy/tests/unit/test_builder_shape.py
@@ -56,8 +56,6 @@ def test_builder_svgshape():
svg_path = get_path("svg_target.svg")
with open(svg_path, "rb") as f:
svg_data = f.read()
- x = np.linspace(0, 1, 10)
- y = x**2
for shape_str in ("circle", "rectangle", "square"):
for data_or_path in (svg_data, svg_path):
items.append(
diff --git a/plotpy/tests/widgets/test_dotarraydemo.py b/plotpy/tests/widgets/test_dotarraydemo.py
index 7a27ea6a..eb715e0d 100644
--- a/plotpy/tests/widgets/test_dotarraydemo.py
+++ b/plotpy/tests/widgets/test_dotarraydemo.py
@@ -29,8 +29,7 @@
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
-import plotpy.config # Loading icons
-import plotpy.widgets
+import plotpy.config # Loading icons # noqa: F401
from plotpy.interfaces import IImageItemType
from plotpy.items import RawImageItem
from plotpy.items.curve.errorbar import vmap
diff --git a/plotpy/tests/widgets/test_filtertest1.py b/plotpy/tests/widgets/test_filtertest1.py
index e8020b4b..6bd04096 100644
--- a/plotpy/tests/widgets/test_filtertest1.py
+++ b/plotpy/tests/widgets/test_filtertest1.py
@@ -14,7 +14,7 @@
from guidata.qthelpers import qt_app_context
from qtpy import QtWidgets as QW
-import plotpy.config # Loading icons
+import plotpy.config # Loading icons # noqa: F401
from plotpy.builder import make
diff --git a/plotpy/tests/widgets/test_filtertest2.py b/plotpy/tests/widgets/test_filtertest2.py
index aaa73922..755791e3 100644
--- a/plotpy/tests/widgets/test_filtertest2.py
+++ b/plotpy/tests/widgets/test_filtertest2.py
@@ -15,7 +15,7 @@
from guidata.qthelpers import qt_app_context, win32_fix_title_bar_background
from qtpy import QtWidgets as QW
-import plotpy.config # Loading icons
+import plotpy.config # Loading icons # noqa: F401
from plotpy.builder import make
from plotpy.plot import BasePlot, BasePlotOptions
from plotpy.plot.manager import PlotManager
diff --git a/plotpy/tests/widgets/test_resize_dialog.py b/plotpy/tests/widgets/test_resize_dialog.py
index 35da5a64..58e1474e 100644
--- a/plotpy/tests/widgets/test_resize_dialog.py
+++ b/plotpy/tests/widgets/test_resize_dialog.py
@@ -8,7 +8,6 @@
import pytest
from guidata.qthelpers import exec_dialog, qt_app_context
from qtpy import QtCore as QC
-from qtpy.QtCore import Qt
from plotpy.widgets.resizedialog import ResizeDialog
diff --git a/plotpy/tests/widgets/test_syncplot.py b/plotpy/tests/widgets/test_syncplot.py
index 06e29f76..a2474d8e 100644
--- a/plotpy/tests/widgets/test_syncplot.py
+++ b/plotpy/tests/widgets/test_syncplot.py
@@ -12,7 +12,6 @@
from qtpy import QtGui as QG
from plotpy.builder import make
-from plotpy.config import _
from plotpy.plot import BasePlot, PlotOptions
from plotpy.plot.plotwidget import SyncPlotWindow
from plotpy.tests.data import gen_2d_gaussian
diff --git a/plotpy/tools/misc.py b/plotpy/tools/misc.py
index cd8eb97b..dd97569c 100644
--- a/plotpy/tools/misc.py
+++ b/plotpy/tools/misc.py
@@ -236,8 +236,10 @@ def activate_command(self, plot, checked):
"""Keyboard/mouse shortcuts:
- single left-click: item (curve, image, ...) selection
- single right-click: context-menu relative to selected item
- - shift: on-active-curve (or image) cursor (+ control to maintain cursor visible)
- - shift + control: on-active-curve cursor (+ control to maintain cursor visible)
+ - shift: on-active-curve (or image) cursor (+ control to maintain
+cursor visible)
+ - shift + control: on-active-curve cursor (+ control to maintain
+cursor visible)
- alt: free cursor
- left-click + mouse move: move item (when available)
- middle-click + mouse move: pan
diff --git a/plotpy/widgets/fit.py b/plotpy/widgets/fit.py
index f29ee8f1..404638bb 100644
--- a/plotpy/widgets/fit.py
+++ b/plotpy/widgets/fit.py
@@ -63,7 +63,9 @@
update_dataset,
)
from guidata.qthelpers import create_groupbox, exec_dialog
-from numpy import inf # Do not remove this import (used by optimization funcs)
+
+# Do not remove this import (used by optimization funcs)
+from numpy import inf # noqa: F401
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy.QtWidgets import QWidget # only to help intersphinx find QWidget
diff --git a/pyproject.toml b/pyproject.toml
index 68673452..e458bc10 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -55,7 +55,7 @@ Documentation = "https://plotpy.readthedocs.io/en/latest/"
plotpy-tests = "plotpy.tests:run"
[project.optional-dependencies]
-dev = ["black", "isort", "pylint", "Coverage", "Cython"]
+dev = ["ruff", "pylint", "Coverage", "Cython"]
doc = [
"PyQt5",
"sphinx",
@@ -74,3 +74,25 @@ include = ["plotpy*"]
[tool.setuptools.dynamic]
version = { attr = "plotpy.__version__" }
+
+[tool.ruff]
+exclude = [".git", ".vscode", "build", "dist"]
+line-length = 88 # Same as Black.
+indent-width = 4 # Same as Black.
+target-version = "py38" # Assume Python 3.8
+
+[tool.ruff.lint]
+# all rules can be found here: https://beta.ruff.rs/docs/rules/
+select = ["E", "F", "W", "I"]
+ignore = [
+ "E203", # space before : (needed for how black formats slicing)
+]
+
+[tool.ruff.format]
+quote-style = "double" # Like Black, use double quotes for strings.
+indent-style = "space" # Like Black, indent with spaces, rather than tabs.
+skip-magic-trailing-comma = false # Like Black, respect magic trailing commas.
+line-ending = "auto" # Like Black, automatically detect the appropriate line ending.
+
+[tool.ruff.lint.per-file-ignores]
+"doc/*" = ["E402"]
diff --git a/requirements.txt b/requirements.txt
index 06602580..4c31ff1e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,10 +6,9 @@ tifffile
PyQt5
PythonQwt>=0.12.1
SciPy>=1.3
-black
guidata>=3.4
-isort
pylint
+ruff
pytest
pytest-xvfb
python-docs-theme
diff --git a/scripts/run_ruff.bat b/scripts/run_ruff.bat
new file mode 100644
index 00000000..0ee389bf
--- /dev/null
+++ b/scripts/run_ruff.bat
@@ -0,0 +1,16 @@
+@echo off
+REM This script was derived from PythonQwt project
+REM ======================================================
+REM Run Ruff analysis
+REM ======================================================
+REM Licensed under the terms of the MIT License
+REM Copyright (c) 2020 Pierre Raybaut
+REM (see PythonQwt LICENSE file for more details)
+REM ======================================================
+setlocal
+call %~dp0utils GetScriptPath SCRIPTPATH
+call %FUNC% GetModName MODNAME
+call %FUNC% SetPythonPath
+call %FUNC% UsePython
+ruff check
+call %FUNC% EndOfScript
\ No newline at end of file