diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index f49d05c..6ff4f06 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -22,7 +22,7 @@ jobs: cache: true python-version: '3.10' - name: Install dependencies - run: pdm sync -dG doc -dG pyside6 + run: pdm sync -dG doc -dG qt5 - name: Install GraphViz run: sudo apt-get install -y graphviz - name: Sphinx build diff --git a/docs/source/_templates/custom-class-template.rst b/docs/source/_templates/custom-class-template.rst index 999ac41..9c4a67f 100644 --- a/docs/source/_templates/custom-class-template.rst +++ b/docs/source/_templates/custom-class-template.rst @@ -10,3 +10,4 @@ .. autoclass:: {{ objname }} :members: :undoc-members: + :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py index de20a00..bb3b533 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -59,7 +59,7 @@ # -- Settings for automatic API generation ----------------------------------- autodoc_mock_imports = ["PySpin"] -autodoc_class_signature = 'mixed' # 'mixed', 'separated' +autodoc_class_signature = 'separated' # 'mixed', 'separated' autodoc_member_order = 'groupwise' # 'alphabetical', 'groupwise', 'bysource' autodoc_inherit_docstrings = False autodoc_typehints = 'description' # 'description', 'signature', 'none', 'both' @@ -73,7 +73,7 @@ napoleon_numpy_docstring = True napoleon_include_init_with_doc = False napoleon_include_private_with_doc = False -napoleon_include_special_with_doc = False +napoleon_include_special_with_doc = True napoleon_use_admonition_for_examples = True napoleon_use_admonition_for_notes = True napoleon_use_admonition_for_references = True diff --git a/docs/source/dev_guide.rst b/docs/source/dev_guide.rst index 7e37e87..10a1025 100644 --- a/docs/source/dev_guide.rst +++ b/docs/source/dev_guide.rst @@ -39,7 +39,7 @@ Checking and formatting of code pdm run ruff format pdm run ruff check --fix - pdm run mypy + pdm run mypy --package iblqt $(qtpy mypy-args) Building the documentation @@ -55,4 +55,4 @@ Building the package .. code-block:: bash - pdm build \ No newline at end of file + pdm build diff --git a/iblqt/__init__.py b/iblqt/__init__.py index b794fd4..b241dc2 100644 --- a/iblqt/__init__.py +++ b/iblqt/__init__.py @@ -1 +1,3 @@ +"""A collection of extensions and enhancements to the Qt framework.""" + __version__ = '0.1.0' diff --git a/iblqt/core.py b/iblqt/core.py index 5c2694d..82c28e7 100644 --- a/iblqt/core.py +++ b/iblqt/core.py @@ -1,3 +1,5 @@ +"""Non-GUI functionality, including event handling, data types, and data management.""" + import logging from pyqtgraph import ColorMap, colormap # type: ignore @@ -16,6 +18,7 @@ import pandas as pd from pandas import DataFrame import numpy as np +import numpy.typing as npt log = logging.getLogger(__name__) @@ -45,7 +48,18 @@ def __init__( super().__init__(parent, *args, **kwargs) self._dataFrame: DataFrame = DataFrame() if dataFrame is None else dataFrame - def setDataFrame(self, dataFrame: DataFrame) -> None: + def getDataFrame(self) -> DataFrame: + """ + Get the underlying DataFrame. + + Returns + ------- + DataFrame + The DataFrame represented by the model. + """ + return self._dataFrame + + def setDataFrame(self, dataFrame: DataFrame): """ Set a new DataFrame. @@ -58,18 +72,8 @@ def setDataFrame(self, dataFrame: DataFrame) -> None: self._dataFrame = dataFrame.copy() self.endResetModel() - def dataFrame(self) -> DataFrame: - """ - Get the underlying DataFrame. - - Returns - ------- - DataFrame - The DataFrame represented by the model. - """ - return self._dataFrame - - dataFrame = Property(DataFrame, fget=dataFrame, fset=setDataFrame) + dataFrame = Property(DataFrame, fget=getDataFrame, fset=setDataFrame) # type: Property + """The DataFrame containing the models data.""" def headerData( self, @@ -182,11 +186,17 @@ def sort(self, column: int, order: Qt.SortOrder = Qt.SortOrder.AscendingOrder): class ColoredDataFrameTableModel(DataFrameTableModel): - colormapChanged = Signal(str) - alphaChanged = Signal(int) + """Extension of DataFrameTableModel providing color-mapped numerical data.""" + + colormapChanged = Signal(str) # type: Signal + """Emitted when the colormap has been changed.""" + + alphaChanged = Signal(int) # type: Signal + """Emitted when the alpha value has been changed.""" + _normData = DataFrame() - _background: np.ndarray - _foreground: np.ndarray + _background: npt.NDArray[np.int_] + _foreground: npt.NDArray[np.int_] _cmap: ColorMap _alpha: int @@ -218,8 +228,19 @@ def __init__( self.modelReset.connect(self._normalizeData) self.dataChanged.connect(self._normalizeData) self.colormapChanged.connect(self._defineColors) - self.setColormap(colormap) - self.setAlpha(alpha) + self.setProperty('colormap', colormap) + self.setProperty('alpha', alpha) + + def getColormap(self) -> str: + """ + Return the name of the current colormap. + + Returns + ------- + str + The name of the current colormap + """ + return self._cmap.name @Slot(str) def setColormap(self, name: str): @@ -238,11 +259,19 @@ def setColormap(self, name: str): return log.warning(f'No such colormap: "{name}"') - def colormap(self) -> str: - """Return the name of the current colormap.""" - return self._cmap.name + colormap = Property(str, fget=getColormap, fset=setColormap, notify=colormapChanged) # type: Property + """The name of the colormap.""" + + def getAlpha(self) -> int: + """ + Return the alpha value of the colormap. - colormap = Property(str, fget=colormap, fset=setColormap) + Returns + ------- + int + The alpha value of the colormap. + """ + return self._alpha @Slot(int) def setAlpha(self, alpha: int = 255): @@ -258,11 +287,8 @@ def setAlpha(self, alpha: int = 255): self.alphaChanged.emit(self._alpha) self.layoutChanged.emit() - def alpha(self) -> int: - """Return the alpha value of the colormap.""" - return self._alpha - - alpha = Property(int, fget=alpha, fset=setAlpha) + alpha = Property(int, fget=getAlpha, fset=setAlpha, notify=alphaChanged) # type: Property + """The alpha value of the colormap.""" def _normalizeData(self) -> None: """Normalize the Data for mapping to a colormap.""" @@ -334,9 +360,10 @@ def data( row = self._dataFrame.index[index.row()] col = index.column() if role == Qt.BackgroundRole: - val = self._background[row][col] - return QColor.fromRgb(*val, self._alpha) + rgb = self._background[row][col] + return QVariant(QColor.fromRgb(*rgb, self._alpha)) if role == Qt.ForegroundRole: - val = self._foreground[row][col] - return QColor('black' if (val * self._alpha) < 32512 else 'white') + lum: int = self._foreground[row][col] + color = QColor('black' if (lum * self._alpha) < 32512 else 'white') + return QVariant(color) return super().data(index, role) diff --git a/iblqt/widgets.py b/iblqt/widgets.py index c86a557..0820721 100644 --- a/iblqt/widgets.py +++ b/iblqt/widgets.py @@ -1,37 +1,24 @@ -"""A collection of reusable Qt widgets designed for general use in PyQt or PySide applications.""" +"""Graphical user interface components.""" from qtpy.QtWidgets import QPushButton from qtpy.QtCore import Signal, Slot, Property class StatefulButton(QPushButton): - """A QPushButton that maintains an active/inactive state. - - Parameters - ---------- - *args : tuple - Positional arguments passed to QPushButton constructor. - active : bool, optional - Initial state of the button (default is False). - **kwargs : dict - Keyword arguments passed to QPushButton constructor. - - Attributes - ---------- - clickedWhileActive : Signal - Emitted when the button is clicked while it is in the active state. - clickedWhileInactive : Signal - Emitted when the button is clicked while it is in the inactive state. - stateChanged : Signal - Emitted when the button's state has changed. The signal carries the new state. - """ - - clickedWhileActive = Signal(name='clickedWhileActive') - clickedWhileInactive = Signal(name='clickedWhileInactive') - stateChanged = Signal(bool, name='stateChanged') + """A QPushButton that maintains an active/inactive state.""" + + clickedWhileActive = Signal() # type: Signal + """Emitted when the button is clicked while it is in the active state.""" + + clickedWhileInactive = Signal() # type: Signal + """Emitted when the button is clicked while it is in the inactive state.""" + + stateChanged = Signal(bool) # type: Signal + """Emitted when the button's state has changed. The signal carries the new state + (True for active, False for inactive).""" def __init__(self, *args, active: bool = False, **kwargs): - """Initialize the StateButton with the specified active state. + """Initialize the StatefulButton with the specified active state. Parameters ---------- @@ -46,8 +33,7 @@ def __init__(self, *args, active: bool = False, **kwargs): self._isActive = active self.clicked.connect(self._onClick) - @Property(bool) - def active(self) -> bool: + def getActive(self) -> bool: """Get the active state of the button. Returns @@ -58,20 +44,23 @@ def active(self) -> bool: return self._isActive @Slot(bool) - def setActive(self, active: bool): + def setActive(self, value: bool): """Set the active state of the button. Emits `stateChanged` if the state has changed. Parameters ---------- - active : bool + value : bool The new active state of the button. """ - if self._isActive != active: - self._isActive = active + if self._isActive != value: + self._isActive = value self.stateChanged.emit(self._isActive) + active = Property(bool, fget=getActive, fset=setActive, notify=stateChanged) # type: Property + """The active state of the button.""" + @Slot() def _onClick(self): """Handle the button click event. diff --git a/pdm.lock b/pdm.lock index 60d87a2..738b8b2 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,10 +2,10 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "dev", "doc", "lint", "pyside6", "qt5", "qt6", "test", "typing"] +groups = ["default", "dev", "doc", "lint", "qt5", "test", "typing"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:865e1471972a10676124b0fd495fac13c7d679ce0ad104229f989a8585f6b638" +content_hash = "sha256:18f133abf85014323a8c2685eb0f77290ccb6915d3a4c1d7fbf4bd68fbc35e18" [[metadata.targets]] requires_python = "==3.10.*" @@ -480,53 +480,15 @@ files = [ ] [[package]] -name = "pyqt6" -version = "6.7.1" -requires_python = ">=3.8" -summary = "Python bindings for the Qt cross platform application toolkit" -groups = ["qt6"] -marker = "python_version == \"3.10\"" -dependencies = [ - "PyQt6-Qt6<6.8.0,>=6.7.0", - "PyQt6-sip<14,>=13.8", -] -files = [ - {file = "PyQt6-6.7.1-1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7f397f4b38b23b5588eb2c0933510deb953d96b1f0323a916c4839c2a66ccccc"}, - {file = "PyQt6-6.7.1-1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2f202b7941aa74e5c7e1463a6f27d9131dbc1e6cabe85571d7364f5b3de7397"}, - {file = "PyQt6-6.7.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:f053378e3aef6248fa612c8afddda17f942fb63f9fe8a9aeb2a6b6b4cbb0eba9"}, - {file = "PyQt6-6.7.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0adb7914c732ad1dee46d9cec838a98cb2b11bc38cc3b7b36fbd8701ae64bf47"}, - {file = "PyQt6-6.7.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2d771fa0981514cb1ee937633dfa64f14caa902707d9afffab66677f3a73e3da"}, - {file = "PyQt6-6.7.1-cp38-abi3-win_amd64.whl", hash = "sha256:fa3954698233fe286a8afc477b84d8517f0788eb46b74da69d3ccc0170d3714c"}, - {file = "PyQt6-6.7.1.tar.gz", hash = "sha256:3672a82ccd3a62e99ab200a13903421e2928e399fda25ced98d140313ad59cb9"}, -] - -[[package]] -name = "pyqt6-qt6" -version = "6.7.3" -summary = "The subset of a Qt installation needed by PyQt6." -groups = ["qt6"] -marker = "python_version == \"3.10\"" -files = [ - {file = "PyQt6_Qt6-6.7.3-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:f517a93b6b1a814d4aa6587adc312e812ebaf4d70415bb15cfb44268c5ad3f5f"}, - {file = "PyQt6_Qt6-6.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8551732984fb36a5f4f3db51eafc4e8e6caf18617365830285306f2db17a94c2"}, - {file = "PyQt6_Qt6-6.7.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:50c7482bcdcf2bb78af257fb10ed8b582f8daf91d829782393bc50ac5a0a900c"}, - {file = "PyQt6_Qt6-6.7.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:cb525fdd393332de60887953029276a44de480fce1d785251ae639580f5e7246"}, - {file = "PyQt6_Qt6-6.7.3-py3-none-win_amd64.whl", hash = "sha256:36ea0892b8caeb983af3f285f45fb8dfbb93cfd972439f4e01b7efb2868f6230"}, -] - -[[package]] -name = "pyqt6-sip" -version = "13.8.0" -requires_python = ">=3.8" -summary = "The sip module support for PyQt6" -groups = ["qt6"] +name = "pyqt5-stubs" +version = "5.15.6.0" +requires_python = ">= 3.5" +summary = "PEP561 stub files for the PyQt5 framework" +groups = ["qt5"] marker = "python_version == \"3.10\"" files = [ - {file = "PyQt6_sip-13.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cedd554c643e54c4c2e12b5874781a87441a1b405acf3650a4a2e1df42aae231"}, - {file = "PyQt6_sip-13.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f57275b5af774529f9838adcfb58869ba3ebdaf805daea113bb0697a96a3f3cb"}, - {file = "PyQt6_sip-13.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:835ed22eab977f75fd77e60d4ff308a1fa794b1d0c04849311f36d2a080cdf3b"}, - {file = "PyQt6_sip-13.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d8b22a6850917c68ce83fc152a8b606ecb2efaaeed35be53110468885d6cdd9d"}, - {file = "PyQt6_sip-13.8.0.tar.gz", hash = "sha256:2f74cf3d6d9cab5152bd9f49d570b2dfb87553ebb5c4919abfde27f5b9fd69d4"}, + {file = "PyQt5-stubs-5.15.6.0.tar.gz", hash = "sha256:91270ac23ebf38a1dc04cd97aa852cd08af82dc839100e5395af1447e3e99707"}, + {file = "PyQt5_stubs-5.15.6.0-py3-none-any.whl", hash = "sha256:7fb8177c72489a8911f021b7bd7c33f12c87f6dba92dcef3fdcdb5d9400f0f3f"}, ] [[package]] @@ -544,60 +506,6 @@ files = [ {file = "pyqtgraph-0.13.7.tar.gz", hash = "sha256:64f84f1935c6996d0e09b1ee66fe478a7771e3ca6f3aaa05f00f6e068321d9e3"}, ] -[[package]] -name = "pyside6" -version = "6.7.3" -requires_python = "<3.13,>=3.9" -summary = "Python bindings for the Qt cross-platform application and UI framework" -groups = ["pyside6"] -marker = "python_version == \"3.10\"" -dependencies = [ - "PySide6-Addons==6.7.3", - "PySide6-Essentials==6.7.3", - "shiboken6==6.7.3", -] -files = [ - {file = "PySide6-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:1c21c4cf6cdd29bd13bbd7a2514756a19188eab992b92af03e64bf06a9b33d5b"}, - {file = "PySide6-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a21480cc746358f70768975fcc452322f03b3c3622625bfb1743b40ce4e24beb"}, - {file = "PySide6-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:c2a1313296c0088d1c3d231d0a8ccf0eda52b84139d0c4065fded76e4a4378f4"}, - {file = "PySide6-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:3ac8dcb4ca82d276e319f89dd99098b01061f255a2b9104216504aece5e0faf8"}, -] - -[[package]] -name = "pyside6-addons" -version = "6.7.3" -requires_python = "<3.13,>=3.9" -summary = "Python bindings for the Qt cross-platform application and UI framework (Addons)" -groups = ["pyside6"] -marker = "python_version == \"3.10\"" -dependencies = [ - "PySide6-Essentials==6.7.3", - "shiboken6==6.7.3", -] -files = [ - {file = "PySide6_Addons-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:3174cb3a373c09c98740b452e8e8f4945d64cfa18ed8d43964111d570f0dc647"}, - {file = "PySide6_Addons-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:bde1eb03dbffd089b50cd445847aaecaf4056cea84c49ea592d00f84f247251e"}, - {file = "PySide6_Addons-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:5a9e0df31345fe6caea677d916ea48b53ba86f95cc6499c57f89e392447ad6db"}, - {file = "PySide6_Addons-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:d8a19c2b2446407724c81c33ebf3217eaabd092f0f72da8130c17079e04a7813"}, -] - -[[package]] -name = "pyside6-essentials" -version = "6.7.3" -requires_python = "<3.13,>=3.9" -summary = "Python bindings for the Qt cross-platform application and UI framework (Essentials)" -groups = ["pyside6"] -marker = "python_version == \"3.10\"" -dependencies = [ - "shiboken6==6.7.3", -] -files = [ - {file = "PySide6_Essentials-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:f9e08a4e9e7dc7b5ab72fde20abce8c97df7af1b802d9743f098f577dfe1f649"}, - {file = "PySide6_Essentials-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cda6fd26aead48f32e57f044d18aa75dc39265b49d7957f515ce7ac3989e7029"}, - {file = "PySide6_Essentials-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:acdde06b74f26e7d26b4ae1461081b32a6cb17fcaa2a580050b5e0f0f12236c9"}, - {file = "PySide6_Essentials-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:f0950fcdcbcd4f2443336dc6a5fe692172adc225f876839583503ded0ab2f2a7"}, -] - [[package]] name = "pytest" version = "8.3.3" @@ -741,20 +649,6 @@ files = [ {file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"}, ] -[[package]] -name = "shiboken6" -version = "6.7.3" -requires_python = "<3.13,>=3.9" -summary = "Python/C++ bindings helper module" -groups = ["pyside6"] -marker = "python_version == \"3.10\"" -files = [ - {file = "shiboken6-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:285fe3cf79be3135fe1ad1e2b9ff6db3a48698887425af6aa6ed7a05a9abc3d6"}, - {file = "shiboken6-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f0852e5781de78be5b13c140ec4c7fb9734e2aaf2986eb2d6a224363e03efccc"}, - {file = "shiboken6-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:f0dd635178e64a45be2f84c9f33dd79ac30328da87f834f21a0baf69ae210e6e"}, - {file = "shiboken6-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:5f29325dfa86fde0274240f1f38e421303749d3174ce3ada178715b5f4719db9"}, -] - [[package]] name = "six" version = "1.16.0" diff --git a/pyproject.toml b/pyproject.toml index bf5f47b..4bd9e97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,12 +46,7 @@ typing = [ ] qt5 = [ "PyQt5>=5.15.11", -] -qt6 = [ - "PyQt6>=6.7.1", -] -pyside6 = [ - "PySide6>=6.7.3", + "PyQt5-stubs>=5.15.6.0", ] [tool.ruff] @@ -64,7 +59,7 @@ quote-style = "single" extend-select = ["D"] [tool.ruff.lint.per-file-ignores] -"src/*.py" = ["D"] +"tests/**/*.py" = ["D"] # pydocstyle [tool.ruff.lint.pydocstyle] convention = "numpy"