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