diff --git a/arpes/_typing.py b/arpes/_typing.py index ce752055..ff6bcee5 100644 --- a/arpes/_typing.py +++ b/arpes/_typing.py @@ -49,6 +49,12 @@ MarkEveryType, ) from numpy.typing import ArrayLike, NDArray + from PySide6.QtCore.Qt import Orientation, WindowType + from PySide6.QtGui import QIcon, QPixmap + from PySide6.QtWidgets import ( + QWidget, + ) + __all__ = [ "DataType", @@ -298,6 +304,25 @@ class ARPESAttrs(SPECTROMETER, LIGHTSOURCEINFO, SAMPLEINFO, total=False): ] +# TypedDict for Qt + + +class QSliderARGS(TypedDict, total=False): + orientation: Orientation + parent: QWidget | None + + +class QWidgetARGS(TypedDict, total=False): + parent: QWidget | None + f: WindowType + + +class QPushButtonARGS(TypedDict, total=False): + icon: QIcon | QPixmap + text: str + parent: QWidget | None + + # # TypedDict for plotting # diff --git a/arpes/plotting/bz_tool/__init__.py b/arpes/plotting/bz_tool/__init__.py index 61c39a65..59c213a2 100644 --- a/arpes/plotting/bz_tool/__init__.py +++ b/arpes/plotting/bz_tool/__init__.py @@ -10,7 +10,7 @@ import numpy as np import xarray as xr from matplotlib.axes import Axes -from matplotlib.backends.backend_qt import FigureCanvas +from matplotlib.backends.backend_qt import FigureCanvasQT from matplotlib.figure import Figure from PySide6 import QtWidgets @@ -26,7 +26,9 @@ if TYPE_CHECKING: from _typeshed import Incomplete - from PySide6.QtWidgets import QLayout, QWidget + from PySide6.QtWidgets import QGridLayout, QWidget + + from arpes.utilities.qt.data_array_image_view import DataArrayImageView __all__ = ["bz_tool"] @@ -52,20 +54,20 @@ class BZTool: def __init__(self) -> None: self.settings = None - self.context = {} + self.context: dict[str, Incomplete] = {} - self.content_layout: QLayout - self.main_layout: QLayout - self.views = {} + self.content_layout: QGridLayout + self.main_layout: QGridLayout + self.views: dict[str, DataArrayImageView] = {} self.reactive_views = [] self.current_material: MaterialParams2D self.cut_line = None - self.canvas = None + self.canvas: FigureCanvasQT self.ax: Axes def configure_main_widget(self) -> None: - self.canvas = FigureCanvas(Figure(figsize=(8, 8))) + self.canvas = FigureCanvasQT(Figure(figsize=(8, 8))) self.ax = self.canvas.figure.subplots() assert isinstance(self.ax, Axes) self.content_layout.addWidget(self.canvas, 0, 0) diff --git a/arpes/plotting/qt_tool/AxisInfoWidget.py b/arpes/plotting/qt_tool/AxisInfoWidget.py index 05ad13f1..949a5d91 100644 --- a/arpes/plotting/qt_tool/AxisInfoWidget.py +++ b/arpes/plotting/qt_tool/AxisInfoWidget.py @@ -1,4 +1,5 @@ """A widget providing rudimentary information about an axis on a DataArray.""" + # pylint: disable=import-error from __future__ import annotations @@ -8,6 +9,8 @@ from PySide6 import QtWidgets if TYPE_CHECKING: + from PySide6.QtWidgets import QGridLayout, QLabel, QPushButton + from . import QtTool __all__ = ("AxisInfoWidget",) @@ -25,10 +28,10 @@ def __init__( """Configure inner widgets for axis info, and transpose to front button.""" super().__init__(title=str(axis_index), parent=parent) - self.layout = QtWidgets.QGridLayout(self) + self.layout: QGridLayout = QtWidgets.QGridLayout(self) - self.label = QtWidgets.QLabel("Cursor: ") - self.transpose_button = QtWidgets.QPushButton("To Front") + self.label: QLabel = QtWidgets.QLabel("Cursor: ") + self.transpose_button: QPushButton = QtWidgets.QPushButton("To Front") self.transpose_button.clicked.connect(self.on_transpose) self.layout.addWidget(self.label) @@ -47,7 +50,7 @@ def root(self) -> QtTool: def recompute(self) -> None: """Force a recomputation of dependent UI state: here, the title and text.""" - self.setTitle(self.root.data.dims[self.axis_index]) + self.setTitle(str(self.root.data.dims[self.axis_index])) try: cursor_index = self.root.context["cursor"][self.axis_index] cursor_value = self.root.context["value_cursor"][self.axis_index] diff --git a/arpes/plotting/qt_tool/__init__.py b/arpes/plotting/qt_tool/__init__.py index e597e012..8cf8a639 100644 --- a/arpes/plotting/qt_tool/__init__.py +++ b/arpes/plotting/qt_tool/__init__.py @@ -6,7 +6,6 @@ import contextlib import warnings import weakref -from collections.abc import Sequence from logging import DEBUG, INFO, Formatter, StreamHandler, getLogger from typing import TYPE_CHECKING, reveal_type @@ -14,7 +13,8 @@ import matplotlib as mpl import numpy as np import pyqtgraph as pg -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets +from PySide6.QtWidgets import QGridLayout from arpes.utilities import normalize_to_spectrum from arpes.utilities.qt import ( @@ -36,6 +36,7 @@ import xarray as xr from _typeshed import Incomplete from PySide6.QtCore import QEvent + from PySide6.QtGui import QKeyEvent from PySide6.QtWidgets import QWidget from arpes._typing import DataType @@ -130,7 +131,7 @@ def transpose_swap(self, event: QEvent) -> None: self.app().transpose_to_front(1) @staticmethod - def _update_scroll_delta(delta: tuple[float, ...], event: QtGui.QKeyEvent) -> tuple: + def _update_scroll_delta(delta: tuple[float, float], event: QKeyEvent) -> tuple[float, float]: logger.debug(f"method: _update_scroll_delta {event!s}") if event.nativeModifiers() & 1: # shift key delta = (delta[0], delta[1] * 5) @@ -140,11 +141,11 @@ def _update_scroll_delta(delta: tuple[float, ...], event: QtGui.QKeyEvent) -> tu return delta - def reset_intensity(self, event: QtGui.QKeyEvent) -> None: + def reset_intensity(self, event: QKeyEvent) -> None: logger.debug(f"method: reset_intensity {event!s}") self.app().reset_intensity() - def scroll_z(self, event: QtGui.QKeyEvent) -> None: + def scroll_z(self, event: QKeyEvent) -> None: key_map = { QtCore.Qt.Key.Key_N: (2, -1), QtCore.Qt.Key.Key_M: (2, 1), @@ -156,7 +157,7 @@ def scroll_z(self, event: QtGui.QKeyEvent) -> None: if delta is not None and self.app() is not None: self.app().scroll(delta) - def scroll(self, event: QtGui.QKeyEvent) -> None: + def scroll(self, event: QKeyEvent) -> None: """[TODO:summary]. Args: @@ -171,10 +172,7 @@ def scroll(self, event: QtGui.QKeyEvent) -> None: QtCore.Qt.Key.Key_Down: (1, -1), QtCore.Qt.Key.Key_Up: (1, 1), } - logger.debug(f"method: scroll {event!s}") - logger.debug(f"app {reveal_type(self.app)}") - logger.debug(f"app() {reveal_type(self.app())}") delta = self._update_scroll_delta(key_map.get(event.key()), event) if delta is not None and self.app() is not None: self.app().scroll(delta) @@ -492,10 +490,10 @@ def add_contextual_widgets(self) -> None: self.main_layout.addLayout(self.content_layout, 0, 0) self.main_layout.addWidget(self.tabs, 1, 0) - def layout(self) -> QtWidgets.QGridLayout: + def layout(self) -> QGridLayout: """Initialize the layout components.""" - self.main_layout = QtWidgets.QGridLayout() - self.content_layout = QtWidgets.QGridLayout() + self.main_layout: QGridLayout = QGridLayout() + self.content_layout: QGridLayout = QGridLayout() return self.main_layout def before_show(self) -> None: diff --git a/arpes/provenance.py b/arpes/provenance.py index a3af150d..56af7048 100644 --- a/arpes/provenance.py +++ b/arpes/provenance.py @@ -28,7 +28,7 @@ import warnings from datetime import UTC from pathlib import Path -from typing import TYPE_CHECKING, TypedDict +from typing import TYPE_CHECKING, Any, TypedDict import xarray as xr @@ -138,7 +138,7 @@ def update_provenance( A decorator which can be applied to a function. """ - def update_provenance_decorator(fn: Callable) -> Callable[..., xr.DataArray | xr.Dataset]: + def update_provenance_decorator(fn: Callable) -> Callable[[Any], xr.DataArray | xr.Dataset]: """[TODO:summary]. Args: diff --git a/arpes/utilities/qt/__init__.py b/arpes/utilities/qt/__init__.py index fad4b0b0..2243c655 100644 --- a/arpes/utilities/qt/__init__.py +++ b/arpes/utilities/qt/__init__.py @@ -53,7 +53,7 @@ def run_tool_in_daemon_process(tool_handler: Callable) -> Callable: - """Starts a Qt based tool as a daemon process. + """Start a Qt based tool as a daemon process. This is exceptionally useful because it let's you have multiple tool windows open simultaneously and does not block the main "analysis" process. @@ -89,7 +89,7 @@ def wrapped_handler( def remove_dangling_viewboxes() -> None: - """Removes ViewBoxes that don't get garbage collected on app close. + """Remove ViewBoxes that don't get garbage collected on app close. If you construct a view hierarchy which has circular references then it can happen that Python will retain the references to Qt @@ -168,7 +168,7 @@ def inches_to_px( return tuple(int(x * self.screen_dpi) for x in arg) def setup_pyqtgraph(self) -> None: - """Does any patching required on PyQtGraph and configures options.""" + """Do any patching required on PyQtGraph and configures options.""" if self._pg_patched: return diff --git a/arpes/utilities/qt/app.py b/arpes/utilities/qt/app.py index cf1899f7..dc06d009 100644 --- a/arpes/utilities/qt/app.py +++ b/arpes/utilities/qt/app.py @@ -70,7 +70,7 @@ def __init__(self) -> None: self.settings = arpes.config.SETTINGS.copy() def copy_to_clipboard(self, value: object) -> None: - """Attempts to copy the value to the clipboard.""" + """Attempt to copy the value to the clipboard.""" try: import pprint @@ -113,12 +113,12 @@ def ninety_eight_percentile(self) -> float: return self._ninety_eight_percentile def print(self, *args: Incomplete, **kwargs: Incomplete) -> None: - """Forwards printing to the application so it ends up in Jupyter.""" + """Forward printing to the application so it ends up in Jupyter.""" self.window.window_print(*args, **kwargs) @staticmethod def build_pg_cmap(colormap: Colormap) -> pg.ColorMap: - """Converts a matplotlib colormap to one suitable for pyqtgraph. + """Convert a matplotlib colormap to one suitable for pyqtgraph. pyqtgraph uses its own colormap format but for consistency and aesthetic reasons we want to use the ones from matplotlib. This will sample the colors @@ -134,7 +134,7 @@ def build_pg_cmap(colormap: Colormap) -> pg.ColorMap: return pg.ColorMap(pos=np.linspace(0, 1, len(sampled_colormap)), color=sampled_colormap) def set_colormap(self, colormap: Colormap | str) -> None: - """Finds all `DataArrayImageView` instances and sets their color palette.""" + """Find all `DataArrayImageView` instances and sets their color palette.""" if isinstance(colormap, str): colormap = mpl.colormaps.get_cmap(colormap) @@ -154,7 +154,7 @@ def generate_marginal_for( cursors: bool = False, layout: QGridLayout | None = None, ) -> DataArrayImageView | DataArrayPlot: - """Generates a marginal plot for the applications's data after selecting along `dimensions`. + """Generate a marginal plot for the applications's data after selecting along `dimensions`. This is used to generate the many different views of a volume in the browsable tools. """ @@ -252,12 +252,12 @@ def layout(self) -> QGridLayout: @property def window(self) -> SimpleWindow: - """Gets the window instance on the current application.""" + """Get the window instance on the current application.""" assert self._window is not None return self._window def start(self, *, no_exec: bool = False, app: QtWidgets.QApplication | None = None) -> None: - """Starts the Qt application, configures the window, and begins Qt execution.""" + """Start the Qt application, configures the window, and begins Qt execution.""" # When running in nbconvert, don't actually open tools. import arpes.config diff --git a/arpes/utilities/qt/data_array_image_view.py b/arpes/utilities/qt/data_array_image_view.py index 56f57471..56c066c6 100644 --- a/arpes/utilities/qt/data_array_image_view.py +++ b/arpes/utilities/qt/data_array_image_view.py @@ -70,7 +70,7 @@ def plot( *args: Incomplete, **kwargs: Incomplete, ) -> pg.PlotDataItem: - """Updates the UI with new data. + """Update the UI with new data. Data also needs to be forwarded to the coordinate axis in case of transpose or changed range of data. @@ -126,7 +126,7 @@ def setImage( keep_levels: bool = False, **kwargs: Incomplete, ) -> None: - """Accepts an xarray.DataArray instead of a numpy array.""" + """Accept an xarray.DataArray instead of a numpy array.""" assert isinstance(img, xr.DataArray) if keep_levels: levels = self.getLevels() @@ -140,4 +140,4 @@ def setImage( self.setLevels(*levels) def recompute(self) -> None: - """A hook to recompute UI state, not used by this widget.""" + """Recompute UI state, not used by this widget.""" diff --git a/arpes/utilities/qt/help_dialogs.py b/arpes/utilities/qt/help_dialogs.py index be2880b3..8fe7b5cd 100644 --- a/arpes/utilities/qt/help_dialogs.py +++ b/arpes/utilities/qt/help_dialogs.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from PySide6.QtGui import QKeyEvent + from PySide6.QtWidgets import QGridLayout, QGroupBox, QLabel, QVBoxLayout from arpes.utilities.ui import KeyBinding __all__ = ("BasicHelpDialog",) @@ -26,13 +27,15 @@ def __init__(self, shortcuts: list[KeyBinding] | None = None) -> None: if shortcuts is None: shortcuts = [] - self.layout = QtWidgets.QVBoxLayout() + self.layout: QVBoxLayout = QtWidgets.QVBoxLayout() - keyboard_shortcuts_info = QtWidgets.QGroupBox(title="Keyboard Shortcuts") - keyboard_shortcuts_layout = QtWidgets.QGridLayout() + keyboard_shortcuts_info: QGroupBox = QtWidgets.QGroupBox(title="Keyboard Shortcuts") + keyboard_shortcuts_layout: QGridLayout = QtWidgets.QGridLayout() for i, shortcut in enumerate(shortcuts): + the_label: QLabel = label(", ".join(PRETTY_KEYS[k] for k in shortcut.chord)) + the_label.setWordWrap(on=True) keyboard_shortcuts_layout.addWidget( - label(", ".join(PRETTY_KEYS[k] for k in shortcut.chord), wordWrap=True), + the_label, i, 0, ) @@ -41,18 +44,17 @@ def __init__(self, shortcuts: list[KeyBinding] | None = None) -> None: keyboard_shortcuts_info.setLayout(keyboard_shortcuts_layout) aboutInfo: QtWidgets.QGroupBox = QtWidgets.QGroupBox(title="About") - vertical( - label( - "QtTool is the work of Conrad Stansbury, with much inspiration " - "and thanks to the authors of ImageTool. QtTool is distributed " - "as part of the PyARPES data analysis framework.", - wordWrap=True, - ), - label( - "Complaints and feature requests should be directed to chstan@berkeley.edu.", - wordWrap=True, - ), + the_label1 = label( + "QtTool is the work of Conrad Stansbury, with much inspiration " + "and thanks to the authors of ImageTool. QtTool is distributed " + "as part of the PyARPES data analysis framework.", ) + the_label1.setWordWrap(on=True) + the_label2 = label( + "Complaints and feature requests should be directed to chstan@berkeley.edu.", + ) + the_label2.setWordWrap(on=True) + vertical(the_label1, the_label2) from . import qt_info diff --git a/arpes/utilities/qt/utils.py b/arpes/utilities/qt/utils.py index d41bf1b3..0574da81 100644 --- a/arpes/utilities/qt/utils.py +++ b/arpes/utilities/qt/utils.py @@ -1,4 +1,5 @@ """Contains utility classes for Qt in PyARPES.""" + from __future__ import annotations import enum @@ -20,7 +21,7 @@ class PlotOrientation(str, enum.Enum): @dataclass class ReactivePlotRecord: - """This contains metadata related to a reactive plot or marginal on a DataArary. + """Metadata related to a reactive plot or marginal on a DataArary. This is used to know how to update and mount corresponding widgets on a main tool view. """ diff --git a/arpes/utilities/qt/windows.py b/arpes/utilities/qt/windows.py index 0ac77281..e17a9d61 100644 --- a/arpes/utilities/qt/windows.py +++ b/arpes/utilities/qt/windows.py @@ -13,7 +13,6 @@ from arpes.utilities.ui import KeyBinding if TYPE_CHECKING: - from _typeshed import Incomplete from PySide6.QtCore import QObject from PySide6.QtGui import QCloseEvent, QKeyEvent @@ -50,7 +49,7 @@ class SimpleWindow(QtWidgets.QMainWindow, QtCore.QObject): HELP_DIALOG_CLS: type[BasicHelpDialog] | None = None def __init__(self) -> None: - """Configures the window. + """Configure the window. In order to start the window, we @@ -89,7 +88,7 @@ def closeEvent(self, event: QCloseEvent) -> None: self.do_close(event) def do_close(self, event: QCloseEvent) -> None: - """Handler for closing accepting an unused event arg.""" + """Handle closing accepting an unused event arg.""" logger.debug(f"unused {event!s} is detected") self.close() @@ -98,7 +97,7 @@ def close(self) -> bool: """If we need to close, give the application a chance to clean up first.""" sys.excepthook = self._old_excepthook self.app().close() - super().close() + return super().close() def eventFilter(self, source: QObject, event: QKeyEvent) -> bool: # type:ignore[override] """Neglect Qt events which do not relate to key presses for now.""" @@ -121,7 +120,7 @@ def eventFilter(self, source: QObject, event: QKeyEvent) -> bool: # type:ignore return super().eventFilter(source, event) def handleKeyPressEvent(self, event: QKeyEvent) -> None: - """Listener for key events supporting single key chords.""" + """Listen key events supporting single key chords.""" handled = False for binding in self._keyBindings: for combination in binding.chord: @@ -149,5 +148,5 @@ def toggle_help(self, event: QKeyEvent) -> None: self._help_dialog = None def window_print(self, *args: Incomplete, **kwargs: Incomplete) -> None: - """Forwards prints to the application instance so they end up in Jupyter.""" + """Forward prints to the application instance so they end up in Jupyter.""" print(*args, **kwargs) diff --git a/arpes/utilities/ui.py b/arpes/utilities/ui.py index f2bd36c0..ad33d61f 100644 --- a/arpes/utilities/ui.py +++ b/arpes/utilities/ui.py @@ -44,7 +44,7 @@ import functools from enum import Enum from logging import DEBUG, INFO, Formatter, StreamHandler, getLogger -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, NamedTuple, Unpack import pyqtgraph as pg import rx @@ -76,10 +76,14 @@ if TYPE_CHECKING: from collections.abc import Callable, Sequence + from dataclasses import dataclass from _typeshed import Incomplete + from PySide6.QtCore.Qt import WindowType from PySide6.QtGui import QKeyEvent + from arpes._typing import QWidgetARGS + __all__ = ( "CollectUI", "CursorRegion", @@ -148,7 +152,7 @@ class CursorMode(NamedTuple): supported_dimensions: Incomplete -PRETTY_KEYS = {} +PRETTY_KEYS: str[int, str] = {} for key, value in vars(QtCore.Qt.Key).items(): if isinstance(value, QtCore.Qt.Key): PRETTY_KEYS[value] = key.partition("_")[2] @@ -164,7 +168,7 @@ def pretty_key_event(event: QKeyEvent) -> list[str]: The key sequence as a human readable string. """ key_sequence = [] - + # note: event.key() returns int, and event.text() returns str key_name = PRETTY_KEYS.get(event.key(), event.text()) if key_name not in key_sequence: key_sequence.append(key_name) @@ -226,7 +230,7 @@ def __enter__(self) -> dict: """Pass my UI tree to the caller so they can write to it.""" return self.ui - def __exit__(self, exc_type, exc_val, exc_tb) -> None: + def __exit__(self, exc_type, exc_val, exc_tb) -> None: # noqa: ANN001 """Reset the active UI.""" global ACTIVE_UI # noqa: PLW0603 ACTIVE_UI = None @@ -309,13 +313,13 @@ def group( @ui_builder -def label(text: str, *args: Incomplete, **kwargs: Incomplete) -> QLabel: +def label(text: str, *args: QWidget | WindowType, **kwargs: Unpack[QWidgetARGS]) -> QLabel: """A convenience method for making a text label.""" return QLabel(text, *args, **kwargs) @ui_builder -def tabs(*children: list[str | QWidget] | tuple[str, QWidget]) -> QTabWidget: +def tabs(*children: tuple[str, QWidget]) -> QTabWidget: """A convenience method for making a tabs control.""" widget = QTabWidget() for name, child in children: @@ -331,9 +335,9 @@ def button(text: str, *args: QWidget) -> QWidget: @ui_builder -def check_box(text: str, *args: Incomplete) -> QWidget: +def check_box(*args: QWidget) -> QWidget: """A convenience method for making a checkbox.""" - return SubjectiveCheckBox(text, *args) + return SubjectiveCheckBox(*args) @ui_builder @@ -359,15 +363,15 @@ def file_dialog(*args: Incomplete) -> QWidget: @ui_builder -def line_edit(*args: Incomplete) -> QWidget: +def line_edit(*args: str | QWidget) -> QWidget: """A convenience method for making a single line text input.""" return SubjectiveLineEdit(*args) @ui_builder -def radio_button(text: str, *args: Incomplete) -> QWidget: +def radio_button(*args: QWidget) -> QWidget: """A convenience method for making a RadioButton.""" - return SubjectiveRadioButton(text, *args) + return SubjectiveRadioButton(*args) @ui_builder @@ -401,7 +405,7 @@ def spin_box( adaptive: bool = True, ) -> QWidget: """A convenience method for making a SpinBox.""" - widget = SubjectiveSpinBox() + widget: SubjectiveSpinBox = SubjectiveSpinBox() widget.setRange(minimum, maximum) @@ -449,7 +453,7 @@ def numeric_input( if validator_settings is None: validator_settings = default_settings.get(input_type) assert isinstance(validator_settings, dict) - widget = SubjectiveLineEdit(str(value), *args) + widget: SubjectiveLineEdit = SubjectiveLineEdit(str(value), *args) widget.setValidator(validators.get(input_type, QtGui.QIntValidator)(**validator_settings)) return widget @@ -528,7 +532,7 @@ def _layout_dataclass_field(dataclass_cls: Incomplete, field_name: str, prefix: field_input = check_box(field_name, id=id_for_field) else: msg = f"Could not render field: {field}" - raise Exception(msg) + raise RuntimeError(msg) return group( field_name, @@ -536,14 +540,14 @@ def _layout_dataclass_field(dataclass_cls: Incomplete, field_name: str, prefix: ) -def layout_dataclass(dataclass_cls: Incomplete, prefix: str = "") -> QWidget: +def layout_dataclass(dataclass_cls: type[dataclass], prefix: str = "") -> QWidget: """Renders a dataclass instance to QtWidgets. See also `bind_dataclass` below to get one way data binding to the instance. Args: - dataclass_cls - prefix + dataclass_cls (type[dataclass]): class type of dataclass + prefix (str): prefix text Returns: The widget containing the layout for the dataclass. @@ -625,6 +629,7 @@ def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: super().__init__(*args, **kwargs) self._region_width = 1.0 self.lines[1].setMovable(m=False) + self.blockLineSignal: bool def set_width(self, value: float) -> None: """Adjusts the region by moving the right boundary to a distance `value` from the left.""" diff --git a/arpes/utilities/widgets.py b/arpes/utilities/widgets.py index f6380e00..a3a67f64 100644 --- a/arpes/utilities/widgets.py +++ b/arpes/utilities/widgets.py @@ -4,7 +4,7 @@ from logging import DEBUG, INFO, Formatter, StreamHandler, getLogger from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Unpack from PySide6.QtWidgets import ( QCheckBox, @@ -22,8 +22,10 @@ from rx.subject import BehaviorSubject, Subject if TYPE_CHECKING: - from _typeshed import Incomplete - from PySide6.QtCore.Qt import CheckState + from PySide6.QtCore.Qt import CheckState, Orientation, WindowType + from PySide6.QtGui import QIcon, QPixmap + + from arpes._typing import QPushButtonARGS, QSliderARGS __all__ = ( "SubjectivePushButton", @@ -53,7 +55,7 @@ class SubjectiveComboBox(QComboBox): """A QComboBox using rx instead of signals.""" - def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: + def __init__(self, *args: QWidget, **kwargs: QWidget) -> None: """Wrap signals in ``rx.BehaviorSubject``s.""" super().__init__(*args, **kwargs) self.subject = BehaviorSubject(self.currentData()) @@ -63,7 +65,7 @@ def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: class SubjectiveSpinBox(QSpinBox): """A QSpinBox using rx instead of signals.""" - def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: + def __init__(self, *args: QWidget, **kwargs: QWidget) -> None: """Wrap signals in ``rx.BehaviorSubject``s.""" super().__init__(*args, **kwargs) self.subject = BehaviorSubject(self.value()) @@ -78,7 +80,7 @@ def update_ui(self, value: int) -> None: class SubjectiveTextEdit(QTextEdit): """A QTextEdit using rx instead of signals.""" - def __init__(self, *args: Incomplete) -> None: + def __init__(self, *args: QWidget) -> None: """Wrap signals in ``rx.BehaviorSubject``s.""" super().__init__(*args) self.subject = BehaviorSubject(self.toPlainText()) @@ -94,7 +96,11 @@ def update_ui(self, value: str) -> None: class SubjectiveSlider(QSlider): """A QSlider using rx instead of signals.""" - def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: + def __init__( + self, + *args: Orientation | QWidget | None, + **kwargs: Unpack[QSliderARGS], + ) -> None: """Wrap signals in ``rx.BehaviorSubject``s.""" super().__init__(*args, **kwargs) self.subject = BehaviorSubject(self.value()) @@ -109,7 +115,7 @@ def update_ui(self, value: int) -> None: class SubjectiveLineEdit(QLineEdit): """A QLineEdit using rx instead of signals.""" - def __init__(self, *args: Incomplete) -> None: + def __init__(self, *args: str | QWidget) -> None: """Wrap signals in ``rx.BehaviorSubject``s.""" super().__init__(*args) self.subject = BehaviorSubject(self.text()) @@ -125,7 +131,7 @@ def update_ui(self, value: str) -> None: class SubjectiveRadioButton(QRadioButton): """A QRadioButton using rx instead of signals.""" - def __init__(self, *args: Incomplete) -> None: + def __init__(self, *args: QWidget | None) -> None: """Wrap signals in ``rx.BehaviorSubject``s.""" super().__init__(*args) self.subject = BehaviorSubject(self.isChecked()) @@ -142,7 +148,7 @@ class SubjectiveFileDialog(QWidget): def __init__( self, - *args: Incomplete, + *args: QWidget | WindowType | None, single: bool = True, dialog_root: Path | None = None, ) -> None: @@ -190,9 +196,14 @@ def get_files(self) -> None: class SubjectivePushButton(QPushButton): """A QCheckBox using rx instead of signals.""" - def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: + def __init__( + self, + *args: QIcon | QPixmap | str | QWidget, + **kwargs: Unpack[QPushButtonARGS], + ) -> None: """Wrap signals in ``rx.BehaviorSubject``s.""" - super().__init__(*args) + super().__init__(*args, **kwargs) + self.subject = Subject() self.clicked.connect(lambda: self.subject.on_next(value=True)) @@ -200,12 +211,9 @@ def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: class SubjectiveCheckBox(QCheckBox): """A QCheckBox using rx instead of signals.""" - def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: + def __init__(self, *args: QWidget, **kwargs: QWidget) -> None: """Wrap signals in ``rx.BehaviorSubject``s.""" - if kwargs: - for k, v in kwargs.items(): - logger.debug(f"unused kwargs: key: {k}, value{v}") - super().__init__(*args) + super().__init__(*args, **kwargs) self.subject = BehaviorSubject(self.checkState()) self.stateChanged.connect(self.subject.on_next) self.subject.subscribe(self.update_ui) diff --git a/arpes/widgets.py b/arpes/widgets.py index 261d3baf..f2708c3d 100644 --- a/arpes/widgets.py +++ b/arpes/widgets.py @@ -35,7 +35,7 @@ from collections.abc import Sequence from functools import wraps from logging import DEBUG, INFO, Formatter, StreamHandler, getLogger -from typing import TYPE_CHECKING, Any, TypeAlias +from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar import matplotlib as mpl import matplotlib.pyplot as plt @@ -170,7 +170,10 @@ def disconnect(self) -> None: self.canvas.draw_idle() -def popout(plotting_function: Callable) -> Callable: +R = TypeVar("R") + + +def popout(plotting_function: Callable[..., R]) -> Callable[..., R]: """A decorator which applies the "%matplotlib qt" magic so that interactive plots are enabled. Sets and subsequently unsets the matplotlib backend for one function call, to allow use of @@ -184,7 +187,7 @@ def popout(plotting_function: Callable) -> Callable: """ @wraps(plotting_function) - def wrapped(*args: Incomplete, **kwargs: Incomplete): + def wrapped(*args: Incomplete, **kwargs: Incomplete) -> R: """[TODO:summary]. [TODO:description] @@ -481,9 +484,7 @@ def autoscale(self) -> None: @popout -def fit_initializer( - data: DataType, -) -> dict[str, Incomplete]: +def fit_initializer(data: DataType) -> dict[str, Incomplete]: """A tool for initializing lineshape fitting. [TODO:description] @@ -504,9 +505,9 @@ def fit_initializer( invisible_axes(ax_other) prefixes = "abcdefghijklmnopqrstuvwxyz" - model_settings = [] + model_settings: list[dict[str, dict[str, float]]] = [] model_defs = [] - for_fit = data.expand_dims("fit_dim") + for_fit: DataType = data.expand_dims("fit_dim") for_fit.coords["fit_dim"] = np.array([0]) data_view = DataArrayView(ax_initial) @@ -539,7 +540,7 @@ def on_add_new_peak(selection) -> None: Returns: [TODO:description] """ - amplitude = data.sel(**selection).mean().item() + amplitude = data.sel(selection).mean().item() selection = selection[data.dims[0]] center = (selection.start + selection.stop) / 2 @@ -555,8 +556,13 @@ def on_add_new_peak(selection) -> None: model_defs.append(LorentzianModel) if model_defs: - results = broadcast_model(model_defs, for_fit, "fit_dim", params=compute_parameters()) - result = results.results[0].item() + results: xr.Dataset = broadcast_model( + model_defs, + for_fit, + "fit_dim", + params=compute_parameters(), + ) + result: xr.DataArray = results.results[0].item() if result is not None: # residual @@ -602,7 +608,7 @@ def on_copy_settings(event: MouseEvent) -> None: @popout def pca_explorer( pca: DataType, - data: DataType, + data: xr.DataArray, # values is used component_dim: str = "components", initial_values: list[float] | None = None, *, @@ -612,7 +618,7 @@ def pca_explorer( Args: pca: The decomposition of the data, the output of an sklearn PCA decomp. - data: The original data. + data (xr.DataArray): The original data. component_dim: The variable name or identifier associated to the PCA component projection in the input data. Defaults to "components" which is what is produced by `pca_along`. initial_values: Which of the PCA components to use for the 2D embedding. Defaults to None. @@ -647,12 +653,10 @@ def compute_for_scatter() -> tuple[xr.DataArray | xr.Dataset, int]: Returns: (tuple[xr.DataArray | xr.Dataset, int] [TODO:description] """ - for_scatter = pca.copy(deep=True).isel( - **dict([[component_dim, context["selected_components"]]]), - ) + for_scatter = pca.copy(deep=True).isel({component_dim: context["selected_components"]}) for_scatter = for_scatter.S.transpose_to_back(component_dim) - size = data.mean(other_dims).stack(pca_dims=pca_dims).values + size: NDArray[np.float_] = data.mean(other_dims).stack(pca_dims=pca_dims).values norm = np.expand_dims(np.linalg.norm(pca.values, axis=(0,)), axis=-1) return (for_scatter / norm).stack(pca_dims=pca_dims), 5 * size / np.mean(size) @@ -960,8 +964,6 @@ def apply_offsets(event: MouseEvent) -> None: def pick_rectangles(data: DataType, **kwargs: Incomplete) -> list[list[float]]: """A utility allowing for selection of rectangular regions. - [TODO:description] - Args: data: [TODO:description] kwargs: [TODO:description] diff --git a/pyproject.toml b/pyproject.toml index 61be0700..f3da7e61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,12 +54,6 @@ dev-dependencies = [ [tool.hatch.metadata] allow-direct-references = true -[tool.pydocstyle] -convention = "google" - -[tool.ruff.pydocstyle] -convention = "google" - [tool.coverge.run] include = ["arpes"] omit = [ @@ -79,7 +73,7 @@ pythonVersion = "3.11" pythonPlatform = "All" [tool.ruff] -ignore = [ +lint.ignore = [ "PD", # pandas-vet "ANN101", # missing-type-self (ANN101) "N802", # invalid-function-name (N802) @@ -96,13 +90,16 @@ ignore = [ "NPY201", # Numpy 2.0, ] target-version = "py310" -select = ["ALL"] +lint.select = ["ALL"] line-length = 100 exclude = ["scripts", "docs", "conda"] +[tool.ruff.lint.pydocstyle] +convention = "google" + -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] # unused-import "arpes/__init__.py" = ["T201"] # print used "arpes/experiment/__init__.py" = ["ALL"]