mirror of
https://github.com/pyapp-kit/superqt.git
synced 2025-07-21 12:11:07 +02:00
build: use hatch for build backend, and use ruff for linting (#139)
* style: ruff fixes * style: no implicit optional * keep mypy manual * build: add fake setup.py * build: use hatch * update ruff * update ruff settings * chore: merge * smaller sdist * fix: fix qfont typing * fix types again * add toc permalink * ignore setup.py from sdist
This commit is contained in:
@@ -1,59 +1,38 @@
|
||||
ci:
|
||||
autoupdate_schedule: monthly
|
||||
autofix_commit_msg: "style: [pre-commit.ci] auto fixes [...]"
|
||||
autoupdate_commit_msg: "ci: [pre-commit.ci] autoupdate"
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-docstring-first
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v1.7.7
|
||||
hooks:
|
||||
- id: autoflake
|
||||
args: ["--in-place", "--remove-all-unused-imports"]
|
||||
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.2.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus, --keep-runtime-typing]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.149
|
||||
hooks:
|
||||
- id: flake8
|
||||
exclude: examples
|
||||
additional_dependencies:
|
||||
- flake8-pyprojecttoml @ git+https://github.com/tlambert03/flake8-pyprojecttoml.git@main
|
||||
- flake8-pyprojecttoml
|
||||
- flake8-bugbear
|
||||
- flake8-typing-imports
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.2.2
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.10.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus, --keep-runtime-typing]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
- id: validate-pyproject
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.991
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: tests|examples
|
||||
additional_dependencies:
|
||||
- types-Pygments
|
||||
stages:
|
||||
- manual
|
||||
|
@@ -85,12 +85,10 @@ class DrawSignalsWidget(QWidget):
|
||||
self.update()
|
||||
|
||||
def scrollAndCut(self, v: Deque[int], cutoff: int):
|
||||
x = 0
|
||||
L = len(v)
|
||||
for p in range(L):
|
||||
v[p] += 1
|
||||
if v[p] > cutoff:
|
||||
x = p
|
||||
break
|
||||
|
||||
# TODO: fix this... delete old ones
|
||||
|
@@ -36,6 +36,9 @@ markdown_extensions:
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:materialx.emoji.twemoji
|
||||
emoji_generator: !!python/name:materialx.emoji.to_svg
|
||||
- toc:
|
||||
permalink: "#"
|
||||
|
||||
|
||||
plugins:
|
||||
- search
|
||||
|
100
pyproject.toml
100
pyproject.toml
@@ -1,7 +1,7 @@
|
||||
# https://peps.python.org/pep-0517/
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools-scm[toml]>=6.2"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling", "hatch-vcs"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
# https://peps.python.org/pep-0621/
|
||||
[project]
|
||||
@@ -11,7 +11,15 @@ readme = "README.md"
|
||||
requires-python = ">=3.7"
|
||||
license = { text = "BSD 3-Clause License" }
|
||||
authors = [{ email = "talley.lambert@gmail.com" }, { name = "Talley Lambert" }]
|
||||
keywords = ["qt", "pyqt", "pyside", "widgets", "range slider", "components", "gui"]
|
||||
keywords = [
|
||||
"qt",
|
||||
"pyqt",
|
||||
"pyside",
|
||||
"widgets",
|
||||
"range slider",
|
||||
"components",
|
||||
"gui",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: X11 Applications :: Qt",
|
||||
@@ -43,11 +51,6 @@ dependencies = [
|
||||
test = ["pint", "pytest", "pytest-cov", "pytest-qt", "tox", "tox-conda"]
|
||||
dev = [
|
||||
"black",
|
||||
"flake8-bugbear",
|
||||
"flake8-docstrings",
|
||||
"flake8-pyprojecttoml",
|
||||
"flake8-typing-imports",
|
||||
"flake8",
|
||||
"ipython",
|
||||
"isort",
|
||||
"jedi<0.18.0",
|
||||
@@ -62,6 +65,7 @@ dev = [
|
||||
"rich",
|
||||
"tox-conda",
|
||||
"tox",
|
||||
"types-Pygments",
|
||||
]
|
||||
docs = ["mkdocs-macros-plugin", "mkdocs-material", "mkdocstrings[python]"]
|
||||
quantity = ["pint"]
|
||||
@@ -78,49 +82,52 @@ Source = "https://github.com/pyapp-kit/superqt"
|
||||
Tracker = "https://github.com/pyapp-kit/superqt/issues"
|
||||
Changelog = "https://github.com/pyapp-kit/superqt/blob/main/CHANGELOG.md"
|
||||
|
||||
[tool.hatch.version]
|
||||
source = "vcs"
|
||||
|
||||
# https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
|
||||
[tool.setuptools]
|
||||
zip-safe = false
|
||||
include-package-data = true
|
||||
packages = { find = { where = ["src"], exclude = [] } }
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"*" = ["py.typed", "*.pyi"]
|
||||
|
||||
|
||||
# https://github.com/pypa/setuptools_scm/#pyprojecttoml-usage
|
||||
[tool.setuptools_scm]
|
||||
write_to = "src/superqt/_version.py"
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["src", "tests", "CHANGELOG.md"]
|
||||
|
||||
# https://pycqa.github.io/isort/docs/configuration/options.html
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
src_paths = ["src/superqt", "tests"]
|
||||
|
||||
# https://flake8.pycqa.org/en/latest/user/options.html
|
||||
# https://gitlab.com/durko/flake8-pyprojecttoml
|
||||
[tool.flake8]
|
||||
exclude = "docs,.eggs,examples,_version.py"
|
||||
max-line-length = 88
|
||||
min-python-version = "3.8.0"
|
||||
docstring-convention = "all" # use numpy convention, while allowing D417
|
||||
extend-ignore = """
|
||||
E203 # whitespace before ':'
|
||||
D107,D203,D212,D213,D402,D413,D415,D416 # numpy
|
||||
D100 # missing docstring in public module
|
||||
D401 # imperative mood
|
||||
W503 # line break before binary operator
|
||||
E302,E704 # black will handle these when we want them
|
||||
"""
|
||||
per-file-ignores = ["tests/*:D"]
|
||||
# https://github.com/charliermarsh/ruff
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py37"
|
||||
src = ["src","tests"]
|
||||
extend-select = [
|
||||
"E", # style errors
|
||||
"F", # flakes
|
||||
# "D", # pydocstyle
|
||||
"I001", # isort
|
||||
"U", # pyupgrade
|
||||
# "N", # pep8-naming
|
||||
# "S", # bandit
|
||||
"C", # flake8-comprehensions
|
||||
"B", # flake8-bugbear
|
||||
"A001", # flake8-builtins
|
||||
"RUF", # ruff-specific rules
|
||||
"M001", # Unused noqa directive
|
||||
]
|
||||
extend-ignore = [
|
||||
"D100", # Missing docstring in public module
|
||||
"D107", # Missing docstring in __init__
|
||||
"D203", # 1 blank line required before class docstring
|
||||
"D212", # Multi-line docstring summary should start at the first line
|
||||
"D213", # Multi-line docstring summary should start at the second line
|
||||
"D413", # Missing blank line after last section
|
||||
"D416", # Section name should end with a colon
|
||||
"C901", # Function is too complex
|
||||
]
|
||||
|
||||
# http://www.pydocstyle.org/en/stable/usage.html
|
||||
[tool.pydocstyle]
|
||||
match_dir = "src/superqt"
|
||||
convention = "numpy"
|
||||
add_select = "D402,D415,D417"
|
||||
ignore = "D100,D213,D401,D413,D107"
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"tests/*.py" = ["D"]
|
||||
"examples/demo_widget.py" = ["E501"]
|
||||
"examples/*.py" = ["B"]
|
||||
|
||||
# https://docs.pytest.org/en/6.2.x/customize.html
|
||||
[tool.pytest.ini_options]
|
||||
@@ -135,14 +142,17 @@ filterwarnings = [
|
||||
|
||||
# https://mypy.readthedocs.io/en/stable/config_file.html
|
||||
[tool.mypy]
|
||||
files = "src/**/"
|
||||
files = "src/**/*.py"
|
||||
strict = true
|
||||
disallow_untyped_defs = false
|
||||
disallow_untyped_calls = false
|
||||
disallow_any_generics = false
|
||||
disallow_subclassing_any = false
|
||||
show_error_codes = true
|
||||
pretty = true
|
||||
exclude = ['tests/**/*']
|
||||
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["superqt.qtcompat.*"]
|
||||
ignore_missing_imports = true
|
||||
@@ -172,4 +182,6 @@ ignore = [
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.md",
|
||||
"codecov.yml",
|
||||
".ruff_cache/**/*",
|
||||
"setup.py"
|
||||
]
|
||||
|
29
setup.py
Normal file
29
setup.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import sys
|
||||
|
||||
sys.stderr.write(
|
||||
"""
|
||||
===============================
|
||||
Unsupported installation method
|
||||
===============================
|
||||
superqt does not support installation with `python setup.py install`.
|
||||
Please use `python -m pip install .` instead.
|
||||
"""
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# The below code will never execute, however GitHub is particularly
|
||||
# picky about where it finds Python packaging metadata.
|
||||
# See: https://github.com/github/feedback/discussions/6456
|
||||
#
|
||||
# To be removed once GitHub catches up.
|
||||
|
||||
setup( # noqa: F821
|
||||
name="superqt",
|
||||
install_requires=[
|
||||
"packaging",
|
||||
"pygments>=2.4.0",
|
||||
"qtpy>=1.1.0",
|
||||
"typing-extensions",
|
||||
],
|
||||
)
|
@@ -1,5 +1,5 @@
|
||||
"""superqt is a collection of Qt components for python."""
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
try:
|
||||
from ._version import version as __version__
|
||||
@@ -46,7 +46,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name == "QQuantity":
|
||||
from .spinbox._quantity import QQuantity
|
||||
|
||||
|
@@ -56,7 +56,7 @@ class QElidingLabel(QLabel):
|
||||
# Reimplemented QT methods
|
||||
|
||||
def text(self) -> str:
|
||||
"""This property holds the label's text.
|
||||
"""Return the label's text.
|
||||
|
||||
If no text has been set this will return an empty string.
|
||||
"""
|
||||
@@ -90,7 +90,7 @@ class QElidingLabel(QLabel):
|
||||
# private implementation methods
|
||||
|
||||
def _elidedText(self) -> str:
|
||||
"""Return `self._text` elided to `width`"""
|
||||
"""Return `self._text` elided to `width`."""
|
||||
fm = QFontMetrics(self.font())
|
||||
# the 2 is a magic number that prevents the ellipses from going missing
|
||||
# in certain cases (?)
|
||||
|
@@ -1,4 +1,4 @@
|
||||
"""A collapsible widget to hide and unhide child widgets"""
|
||||
"""A collapsible widget to hide and unhide child widgets."""
|
||||
from typing import Optional, Union
|
||||
|
||||
from qtpy.QtCore import (
|
||||
@@ -150,7 +150,7 @@ class QCollapsible(QFrame):
|
||||
self._expand_collapse(QPropertyAnimation.Direction.Backward, animate)
|
||||
|
||||
def isExpanded(self) -> bool:
|
||||
"""Return whether the collapsible section is visible"""
|
||||
"""Return whether the collapsible section is visible."""
|
||||
return self._toggle_btn.isChecked()
|
||||
|
||||
def setLocked(self, locked: bool = True) -> None:
|
||||
@@ -159,7 +159,7 @@ class QCollapsible(QFrame):
|
||||
self._toggle_btn.setCheckable(not locked)
|
||||
|
||||
def locked(self) -> bool:
|
||||
"""Return True if collapse/expand is disabled"""
|
||||
"""Return True if collapse/expand is disabled."""
|
||||
return self._locked
|
||||
|
||||
def _expand_collapse(
|
||||
|
@@ -11,7 +11,7 @@ NONE_STRING = "----"
|
||||
|
||||
|
||||
def _get_name(enum_value: Enum):
|
||||
"""Create human readable name if user does not implement __str__"""
|
||||
"""Create human readable name if user does not implement `__str__`."""
|
||||
if (
|
||||
enum_value.__str__.__module__ != "enum"
|
||||
and not enum_value.__str__.__module__.startswith("shibokensupport")
|
||||
@@ -24,8 +24,7 @@ def _get_name(enum_value: Enum):
|
||||
|
||||
|
||||
class QEnumComboBox(QComboBox):
|
||||
"""
|
||||
ComboBox presenting options from a python Enum.
|
||||
"""ComboBox presenting options from a python Enum.
|
||||
|
||||
If the Enum class does not implement `__str__` then a human readable name
|
||||
is created from the name of the enum member, replacing underscores with spaces.
|
||||
@@ -44,9 +43,7 @@ class QEnumComboBox(QComboBox):
|
||||
self.currentIndexChanged.connect(self._emit_signal)
|
||||
|
||||
def setEnumClass(self, enum: Optional[EnumMeta], allow_none=False):
|
||||
"""
|
||||
Set enum class from which members value should be selected
|
||||
"""
|
||||
"""Set enum class from which members value should be selected."""
|
||||
self.clear()
|
||||
self._enum_class = enum
|
||||
self._allow_none = allow_none and enum is not None
|
||||
@@ -55,11 +52,11 @@ class QEnumComboBox(QComboBox):
|
||||
super().addItems(list(map(_get_name, self._enum_class.__members__.values())))
|
||||
|
||||
def enumClass(self) -> Optional[EnumMeta]:
|
||||
"""return current Enum class"""
|
||||
"""return current Enum class."""
|
||||
return self._enum_class
|
||||
|
||||
def isOptional(self) -> bool:
|
||||
"""return if current enum is with optional annotation"""
|
||||
"""return if current enum is with optional annotation."""
|
||||
return self._allow_none
|
||||
|
||||
def clear(self):
|
||||
@@ -68,7 +65,7 @@ class QEnumComboBox(QComboBox):
|
||||
super().clear()
|
||||
|
||||
def currentEnum(self) -> Optional[EnumType]:
|
||||
"""current value as Enum member"""
|
||||
"""Current value as Enum member."""
|
||||
if self._enum_class is not None:
|
||||
if self._allow_none:
|
||||
if self.currentText() == NONE_STRING:
|
||||
|
@@ -1,6 +1,8 @@
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QT_VERSION
|
||||
from qtpy.QtCore import Qt, Signal
|
||||
from qtpy.QtWidgets import QComboBox, QCompleter
|
||||
from qtpy.QtWidgets import QComboBox, QCompleter, QWidget
|
||||
|
||||
try:
|
||||
is_qt_bellow_5_14 = tuple(int(x) for x in QT_VERSION.split(".")[:2]) < (5, 14)
|
||||
@@ -9,14 +11,12 @@ except ValueError:
|
||||
|
||||
|
||||
class QSearchableComboBox(QComboBox):
|
||||
"""
|
||||
ComboCox with completer for fast search in multiple options
|
||||
"""
|
||||
"""ComboCox with completer for fast search in multiple options."""
|
||||
|
||||
if is_qt_bellow_5_14:
|
||||
textActivated = Signal(str) # pragma: no cover
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent: Optional[QWidget] = None):
|
||||
super().__init__(parent)
|
||||
self.setEditable(True)
|
||||
self.completer_object = QCompleter()
|
||||
|
@@ -14,7 +14,7 @@ __all__ = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._animations import Animation, pulse, spin
|
||||
from ._iconfont import IconFont, IconFontMeta
|
||||
@@ -39,13 +39,13 @@ ENTRY_POINT = _FIM.ENTRY_POINT
|
||||
def icon(
|
||||
glyph_key: str,
|
||||
scale_factor: float = DEFAULT_SCALING_FACTOR,
|
||||
color: ValidColor = None,
|
||||
color: ValidColor | None = None,
|
||||
opacity: float = 1,
|
||||
animation: Optional[Animation] = None,
|
||||
transform: Optional[QTransform] = None,
|
||||
states: Dict[str, Union[IconOptionDict, IconOpts]] | None = None,
|
||||
animation: Animation | None = None,
|
||||
transform: QTransform | None = None,
|
||||
states: dict[str, IconOptionDict | IconOpts] | None = None,
|
||||
) -> QFontIcon:
|
||||
"""Create a QIcon for `glyph_key`, with a number of optional settings
|
||||
"""Create a QIcon for `glyph_key`, with a number of optional settings.
|
||||
|
||||
The `glyph_key` (e.g. 'fa5s.smile') represents a Font-family & style, and a glpyh.
|
||||
In most cases, the key should be provided by a plugin in the environment, like:
|
||||
@@ -99,7 +99,6 @@ def icon(
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
simple example (using the string `'fa5s.smile'` assumes the `fonticon-fontawesome5`
|
||||
plugin is installed)
|
||||
|
||||
@@ -150,7 +149,7 @@ def icon(
|
||||
)
|
||||
|
||||
|
||||
def setTextIcon(widget: QWidget, glyph_key: str, size: Optional[float] = None) -> None:
|
||||
def setTextIcon(widget: QWidget, glyph_key: str, size: float | None = None) -> None:
|
||||
"""Set text on a widget to a specific font & glyph.
|
||||
|
||||
This is an alternative to setting a QIcon with a pixmap. It may be easier to
|
||||
@@ -168,8 +167,8 @@ def setTextIcon(widget: QWidget, glyph_key: str, size: Optional[float] = None) -
|
||||
return _QFIS.instance().setTextIcon(widget, glyph_key, size)
|
||||
|
||||
|
||||
def font(font_prefix: str, size: Optional[int] = None) -> QFont:
|
||||
"""Create QFont for `font_prefix`
|
||||
def font(font_prefix: str, size: int | None = None) -> QFont:
|
||||
"""Create QFont for `font_prefix`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -187,8 +186,8 @@ def font(font_prefix: str, size: Optional[int] = None) -> QFont:
|
||||
|
||||
|
||||
def addFont(
|
||||
filepath: str, prefix: str, charmap: Optional[Dict[str, str]] = None
|
||||
) -> Optional[Tuple[str, str]]:
|
||||
filepath: str, prefix: str, charmap: dict[str, str] | None = None
|
||||
) -> tuple[str, str] | None:
|
||||
"""Add OTF/TTF file at `filepath` to the registry under `prefix`.
|
||||
|
||||
If you'd like to later use a fontkey in the form of `prefix.some-name`, then
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
from qtpy.QtCore import QRectF, QTimer
|
||||
from qtpy.QtGui import QPainter
|
||||
@@ -42,5 +43,5 @@ class spin(Animation):
|
||||
class pulse(spin):
|
||||
"""Animation that spins an icon in slower, discrete steps."""
|
||||
|
||||
def __init__(self, parent_widget: QWidget = None):
|
||||
def __init__(self, parent_widget: Optional[QWidget] = None):
|
||||
super().__init__(parent_widget, interval=200, step=45)
|
||||
|
@@ -60,7 +60,6 @@ class IconFont(metaclass=IconFontMeta):
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
class FA5S(IconFont):
|
||||
__font_file__ = '...'
|
||||
some_char = 0xfa42
|
||||
@@ -76,7 +75,7 @@ def namespace2font(namespace: Union[Mapping, Type], name: str) -> Type[IconFont]
|
||||
assert isinstance(
|
||||
getattr(namespace, FONTFILE_ATTR), str
|
||||
), "Not a valid font type"
|
||||
return namespace # type: ignore
|
||||
return namespace
|
||||
elif hasattr(namespace, "__dict__"):
|
||||
ns = dict(namespace.__dict__)
|
||||
else:
|
||||
|
@@ -4,7 +4,7 @@ import warnings
|
||||
from collections import abc
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import DefaultDict, Dict, Optional, Sequence, Tuple, Type, Union, cast
|
||||
from typing import DefaultDict, Sequence, Tuple, Union, cast
|
||||
|
||||
from qtpy import QT_VERSION
|
||||
from qtpy.QtCore import QObject, QPoint, QRect, QSize, Qt
|
||||
@@ -45,14 +45,14 @@ ValidColor = Union[
|
||||
int,
|
||||
str,
|
||||
Qt.GlobalColor,
|
||||
Tuple[int, int, int, int],
|
||||
Tuple[int, int, int],
|
||||
Tuple[int, int, int, int], # noqa: U006
|
||||
Tuple[int, int, int], # noqa: U006
|
||||
None,
|
||||
]
|
||||
|
||||
StateOrMode = Union[QIcon.State, QIcon.Mode]
|
||||
StateModeKey = Union[StateOrMode, str, Sequence[StateOrMode]]
|
||||
_SM_MAP: Dict[str, StateOrMode] = {
|
||||
_SM_MAP: dict[str, StateOrMode] = {
|
||||
"on": QIcon.State.On,
|
||||
"off": QIcon.State.Off,
|
||||
"normal": QIcon.Mode.Normal,
|
||||
@@ -62,8 +62,8 @@ _SM_MAP: Dict[str, StateOrMode] = {
|
||||
}
|
||||
|
||||
|
||||
def _norm_state_mode(key: StateModeKey) -> Tuple[QIcon.State, QIcon.Mode]:
|
||||
"""return state/mode tuple given a variety of valid inputs.
|
||||
def _norm_state_mode(key: StateModeKey) -> tuple[QIcon.State, QIcon.Mode]:
|
||||
"""Return state/mode tuple given a variety of valid inputs.
|
||||
|
||||
Input can be either a string, or a sequence of state or mode enums.
|
||||
Strings can be any combination of on, off, normal, active, selected, disabled,
|
||||
@@ -73,13 +73,13 @@ def _norm_state_mode(key: StateModeKey) -> Tuple[QIcon.State, QIcon.Mode]:
|
||||
if isinstance(key, str):
|
||||
try:
|
||||
_sm = [_SM_MAP[k.lower()] for k in key.split("_")]
|
||||
except KeyError:
|
||||
except KeyError as e:
|
||||
raise ValueError(
|
||||
f"{key!r} is not a valid state key, must be a combination of {{on, "
|
||||
"off, active, disabled, selected, normal} separated by underscore"
|
||||
)
|
||||
) from e
|
||||
else:
|
||||
_sm = key if isinstance(key, abc.Sequence) else [key] # type: ignore
|
||||
_sm = key if isinstance(key, abc.Sequence) else [key]
|
||||
|
||||
state = next((i for i in _sm if isinstance(i, QIcon.State)), QIcon.State.Off)
|
||||
mode = next((i for i in _sm if isinstance(i, QIcon.Mode)), QIcon.Mode.Normal)
|
||||
@@ -91,8 +91,8 @@ class IconOptionDict(TypedDict, total=False):
|
||||
scale_factor: float
|
||||
color: ValidColor
|
||||
opacity: float
|
||||
animation: Optional[Animation]
|
||||
transform: Optional[QTransform]
|
||||
animation: Animation | None
|
||||
transform: QTransform | None
|
||||
|
||||
|
||||
# public facing, for a nicer IDE experience than a dict
|
||||
@@ -119,12 +119,12 @@ class IconOpts:
|
||||
The animation to use, by default `None`
|
||||
"""
|
||||
|
||||
glyph_key: Union[str, Unset] = _Unset
|
||||
scale_factor: Union[float, Unset] = _Unset
|
||||
color: Union[ValidColor, Unset] = _Unset
|
||||
opacity: Union[float, Unset] = _Unset
|
||||
animation: Union[Animation, Unset, None] = _Unset
|
||||
transform: Union[QTransform, Unset, None] = _Unset
|
||||
glyph_key: str | Unset = _Unset
|
||||
scale_factor: float | Unset = _Unset
|
||||
color: ValidColor | Unset = _Unset
|
||||
opacity: float | Unset = _Unset
|
||||
animation: Animation | Unset | None = _Unset
|
||||
transform: QTransform | Unset | None = _Unset
|
||||
|
||||
def dict(self) -> IconOptionDict:
|
||||
# not using asdict due to pickle errors on animation
|
||||
@@ -140,8 +140,8 @@ class _IconOptions:
|
||||
scale_factor: float = DEFAULT_SCALING_FACTOR
|
||||
color: ValidColor = None
|
||||
opacity: float = DEFAULT_OPACITY
|
||||
animation: Optional[Animation] = None
|
||||
transform: Optional[QTransform] = None
|
||||
animation: Animation | None = None
|
||||
transform: QTransform | None = None
|
||||
|
||||
def _update(self, icon_opts: IconOpts) -> _IconOptions:
|
||||
return _IconOptions(**{**vars(self), **icon_opts.dict()})
|
||||
@@ -157,7 +157,7 @@ class _QFontIconEngine(QIconEngine):
|
||||
def __init__(self, options: _IconOptions):
|
||||
super().__init__()
|
||||
self._opts: DefaultDict[
|
||||
QIcon.State, Dict[QIcon.Mode, Optional[_IconOptions]]
|
||||
QIcon.State, dict[QIcon.Mode, _IconOptions | None]
|
||||
] = DefaultDict(dict)
|
||||
self._opts[QIcon.State.Off][QIcon.Mode.Normal] = options
|
||||
self.update_hash()
|
||||
@@ -239,7 +239,7 @@ class _QFontIconEngine(QIconEngine):
|
||||
if isinstance(opts.color, tuple):
|
||||
color_args = opts.color
|
||||
else:
|
||||
color_args = (opts.color,) if opts.color else () # type: ignore
|
||||
color_args = (opts.color,) if opts.color else ()
|
||||
|
||||
# animation
|
||||
if opts.animation is not None:
|
||||
@@ -321,12 +321,12 @@ class QFontIcon(QIcon):
|
||||
self,
|
||||
state: QIcon.State = QIcon.State.Off,
|
||||
mode: QIcon.Mode = QIcon.Mode.Normal,
|
||||
glyph_key: Union[str, Unset] = _Unset,
|
||||
scale_factor: Union[float, Unset] = _Unset,
|
||||
color: Union[ValidColor, Unset] = _Unset,
|
||||
opacity: Union[float, Unset] = _Unset,
|
||||
animation: Union[Animation, Unset, None] = _Unset,
|
||||
transform: Union[QTransform, Unset, None] = _Unset,
|
||||
glyph_key: str | Unset = _Unset,
|
||||
scale_factor: float | Unset = _Unset,
|
||||
color: ValidColor | Unset = _Unset,
|
||||
opacity: float | Unset = _Unset,
|
||||
animation: Animation | Unset | None = _Unset,
|
||||
transform: QTransform | Unset | None = _Unset,
|
||||
) -> None:
|
||||
"""Set icon options for a specific mode/state."""
|
||||
if glyph_key is not _Unset:
|
||||
@@ -346,17 +346,17 @@ class QFontIcon(QIcon):
|
||||
class QFontIconStore(QObject):
|
||||
|
||||
# map of key -> (font_family, font_style)
|
||||
_LOADED_KEYS: Dict[str, Tuple[str, Optional[str]]] = dict()
|
||||
_LOADED_KEYS: dict[str, tuple[str, str]] = {}
|
||||
|
||||
# map of (font_family, font_style) -> character (char may include key)
|
||||
_CHARMAPS: Dict[Tuple[str, Optional[str]], Dict[str, str]] = dict()
|
||||
_CHARMAPS: dict[tuple[str, str | None], dict[str, str]] = {}
|
||||
|
||||
# singleton instance, use `instance()` to retrieve
|
||||
__instance: Optional[QFontIconStore] = None
|
||||
__instance: QFontIconStore | None = None
|
||||
|
||||
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent=parent)
|
||||
if tuple(QT_VERSION.split(".")) < ("6", "0"):
|
||||
if tuple(cast(str, QT_VERSION).split(".")) < ("6", "0"):
|
||||
# QT6 drops this
|
||||
QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps)
|
||||
|
||||
@@ -373,8 +373,8 @@ class QFontIconStore(QObject):
|
||||
QFontDatabase.removeAllApplicationFonts()
|
||||
|
||||
@classmethod
|
||||
def _key2family(cls, key: str) -> Tuple[str, Optional[str]]:
|
||||
"""Return (family, style) given a font `key`"""
|
||||
def _key2family(cls, key: str) -> tuple[str, str]:
|
||||
"""Return (family, style) given a font `key`."""
|
||||
key = key.split(".", maxsplit=1)[0]
|
||||
if key not in cls._LOADED_KEYS:
|
||||
from . import _plugins
|
||||
@@ -382,7 +382,7 @@ class QFontIconStore(QObject):
|
||||
try:
|
||||
font_cls = _plugins.get_font_class(key)
|
||||
result = cls.addFont(
|
||||
font_cls.__font_file__, key, charmap=font_cls.__dict__
|
||||
font_cls.__font_file__, key, charmap=dict(font_cls.__dict__)
|
||||
)
|
||||
if not result: # pragma: no cover
|
||||
raise Exception("Invalid font file")
|
||||
@@ -402,8 +402,10 @@ class QFontIconStore(QObject):
|
||||
return char
|
||||
try:
|
||||
charmap = cls._CHARMAPS[(family, style)]
|
||||
except KeyError:
|
||||
raise KeyError(f"No charmap registered for font '{family} ({style})'")
|
||||
except KeyError as e:
|
||||
raise KeyError(
|
||||
f"No charmap registered for font '{family} ({style})'"
|
||||
) from e
|
||||
if char in charmap:
|
||||
# split in case the charmap includes the key
|
||||
return charmap[char].split(".", maxsplit=1)[-1]
|
||||
@@ -416,8 +418,8 @@ class QFontIconStore(QObject):
|
||||
raise ValueError(f"Font '{family} ({style})' has no glyph with the key {ident}")
|
||||
|
||||
@classmethod
|
||||
def key2glyph(cls, glyph_key: str) -> tuple[str, str, Optional[str]]:
|
||||
"""Return (char, family, style) given a `glyph_key`"""
|
||||
def key2glyph(cls, glyph_key: str) -> tuple[str, str, str | None]:
|
||||
"""Return (char, family, style) given a `glyph_key`."""
|
||||
if "." not in glyph_key:
|
||||
raise ValueError("Glyph key must contain a period")
|
||||
font_key, char = glyph_key.split(".", maxsplit=1)
|
||||
@@ -427,8 +429,8 @@ class QFontIconStore(QObject):
|
||||
|
||||
@classmethod
|
||||
def addFont(
|
||||
cls, filepath: str, prefix: str, charmap: Optional[Dict[str, str]] = None
|
||||
) -> Optional[Tuple[str, str]]:
|
||||
cls, filepath: str, prefix: str, charmap: dict[str, str] | None = None
|
||||
) -> tuple[str, str] | None:
|
||||
"""Add font at `filepath` to the registry under `key`.
|
||||
|
||||
If you'd like to later use a fontkey in the form of `key.some-name`, then
|
||||
@@ -440,7 +442,7 @@ class QFontIconStore(QObject):
|
||||
----------
|
||||
filepath : str
|
||||
Path to an OTF or TTF file containing the fonts
|
||||
key : str
|
||||
prefix : str
|
||||
A key that will represent this font file when used for lookup. For example,
|
||||
'fa5s' for 'Font-Awesome 5 Solid'.
|
||||
charmap : Dict[str, str], optional
|
||||
@@ -455,7 +457,7 @@ class QFontIconStore(QObject):
|
||||
"""
|
||||
if prefix in cls._LOADED_KEYS:
|
||||
warnings.warn(f"Prefix {prefix} already loaded")
|
||||
return
|
||||
return None
|
||||
|
||||
if not Path(filepath).exists():
|
||||
raise FileNotFoundError(f"Font file doesn't exist: {filepath}")
|
||||
@@ -474,13 +476,13 @@ class QFontIconStore(QObject):
|
||||
family: str = families[0]
|
||||
|
||||
# in Qt6, everything becomes a static member
|
||||
QFd: Union[QFontDatabase, Type[QFontDatabase]] = (
|
||||
QFontDatabase() # type: ignore
|
||||
if tuple(QT_VERSION.split(".")) < ("6", "0")
|
||||
QFd: QFontDatabase | "type[QFontDatabase]" = (
|
||||
QFontDatabase()
|
||||
if tuple(cast(str, QT_VERSION).split(".")) < ("6", "0")
|
||||
else QFontDatabase
|
||||
)
|
||||
|
||||
styles = QFd.styles(family) # type: ignore
|
||||
styles = QFd.styles(family)
|
||||
style: str = styles[-1] if styles else ""
|
||||
if not QFd.isSmoothlyScalable(family, style): # pragma: no cover
|
||||
warnings.warn(
|
||||
@@ -498,11 +500,11 @@ class QFontIconStore(QObject):
|
||||
glyph_key: str,
|
||||
*,
|
||||
scale_factor: float = DEFAULT_SCALING_FACTOR,
|
||||
color: ValidColor = None,
|
||||
color: ValidColor | None = None,
|
||||
opacity: float = 1,
|
||||
animation: Optional[Animation] = None,
|
||||
transform: Optional[QTransform] = None,
|
||||
states: Dict[str, Union[IconOptionDict, IconOpts]] | None = None,
|
||||
animation: Animation | None = None,
|
||||
transform: QTransform | None = None,
|
||||
states: dict[str, IconOptionDict | IconOpts] | None = None,
|
||||
) -> QFontIcon:
|
||||
self.key2glyph(glyph_key) # make sure it's a valid glyph_key
|
||||
default_opts = _IconOptions(
|
||||
@@ -521,7 +523,7 @@ class QFontIconStore(QObject):
|
||||
return icon
|
||||
|
||||
def setTextIcon(
|
||||
self, widget: QWidget, glyph_key: str, size: Optional[float] = None
|
||||
self, widget: QWidget, glyph_key: str, size: float | None = None
|
||||
) -> None:
|
||||
"""Sets text on a widget to a specific font & glyph.
|
||||
|
||||
@@ -538,8 +540,8 @@ class QFontIconStore(QObject):
|
||||
widget.setFont(self.font(glyph_key, int(size)))
|
||||
setText(glyph)
|
||||
|
||||
def font(self, font_prefix: str, size: Optional[int] = None) -> QFont:
|
||||
"""Create QFont for `font_prefix`"""
|
||||
def font(self, font_prefix: str, size: int | None = None) -> QFont:
|
||||
"""Create QFont for `font_prefix`."""
|
||||
font_key, _ = font_prefix.split(".", maxsplit=1)
|
||||
family, style = self._key2family(font_key)
|
||||
font = QFont()
|
||||
@@ -552,7 +554,7 @@ class QFontIconStore(QObject):
|
||||
|
||||
|
||||
def _ensure_identifier(name: str) -> str:
|
||||
"""Normalize string to valid identifier"""
|
||||
"""Normalize string to valid identifier."""
|
||||
import keyword
|
||||
|
||||
if not name:
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from typing import Generic, List, Sequence, Tuple, TypeVar, Union
|
||||
from typing import Generic, List, Optional, Sequence, Tuple, TypeVar, Union
|
||||
|
||||
from qtpy import QtGui
|
||||
from qtpy.QtCore import Property, QEvent, QPoint, QPointF, QRect, QRectF, Qt, Signal
|
||||
@@ -233,7 +233,9 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
|
||||
|
||||
# SubControl Positions
|
||||
|
||||
def _handleRect(self, handle_index: int, opt: QStyleOptionSlider = None) -> QRect:
|
||||
def _handleRect(
|
||||
self, handle_index: int, opt: Optional[QStyleOptionSlider] = None
|
||||
) -> QRect:
|
||||
"""Return the QRect for all handles."""
|
||||
opt = opt or self._styleOption
|
||||
opt.sliderPosition = self._optSliderPositions[handle_index]
|
||||
@@ -310,7 +312,7 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
|
||||
|
||||
# NOTE: this is very much tied to mousepress... not a generic "get control"
|
||||
def _getControlAtPos(
|
||||
self, pos: QPoint, opt: QStyleOptionSlider = None
|
||||
self, pos: QPoint, opt: Optional[QStyleOptionSlider] = None
|
||||
) -> Tuple[QStyle.SubControl, int]:
|
||||
"""Update self._pressedControl based on ev.pos()."""
|
||||
opt = opt or self._styleOption
|
||||
|
@@ -1,4 +1,4 @@
|
||||
"""Generic Sliders with internal python-based models
|
||||
"""Generic Sliders with internal python-based models.
|
||||
|
||||
This module reimplements most of the logic from qslider.cpp in python:
|
||||
https://code.woboq.org/qt5/qtbase/src/widgets/widgets/qslider.cpp.html
|
||||
@@ -522,16 +522,7 @@ def _event_position(ev: QEvent) -> QPoint:
|
||||
def _sliderValueFromPosition(
|
||||
min: float, max: float, position: int, span: int, upsideDown: bool = False
|
||||
) -> float:
|
||||
"""Converts the given pixel `position` to a value.
|
||||
|
||||
0 maps to the `min` parameter, `span` maps to `max` and other values are
|
||||
distributed evenly in-between.
|
||||
|
||||
By default, this function assumes that the maximum value is on the right
|
||||
for horizontal items and on the bottom for vertical items. Set the
|
||||
`upsideDown` parameter to True to reverse this behavior.
|
||||
"""
|
||||
|
||||
"""Converts the given pixel `position` to a value."""
|
||||
if span <= 0 or position <= 0:
|
||||
return max if upsideDown else min
|
||||
if position >= span:
|
||||
|
@@ -147,10 +147,7 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
|
||||
self.setOrientation(orientation)
|
||||
|
||||
def _setValue(self, value: float):
|
||||
"""
|
||||
Convert the value from float to int before
|
||||
setting the slider value
|
||||
"""
|
||||
"""Convert the value from float to int before setting the slider value."""
|
||||
self._slider.setValue(int(value))
|
||||
|
||||
def _rename_signals(self):
|
||||
@@ -171,7 +168,7 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
|
||||
if self._edge_label_mode == EdgeLabelMode.NoLabel:
|
||||
marg = (0, 0, 5, 0)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout = QHBoxLayout() # type: ignore
|
||||
layout.addWidget(self._slider)
|
||||
layout.addWidget(self._label)
|
||||
self._label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
@@ -421,7 +418,6 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
|
||||
def setOrientation(self, orientation):
|
||||
"""Set orientation, value will be 'horizontal' or 'vertical'."""
|
||||
|
||||
self._slider.setOrientation(orientation)
|
||||
if orientation == Qt.Orientation.Vertical:
|
||||
layout = QVBoxLayout()
|
||||
|
@@ -24,7 +24,7 @@ class _AnyIntValidator(QValidator):
|
||||
|
||||
|
||||
class QLargeIntSpinBox(QAbstractSpinBox):
|
||||
"""An integer spinboxes backed by unbound python integer
|
||||
"""An integer spinboxes backed by unbound python integer.
|
||||
|
||||
Qt's built-in ``QSpinBox`` is backed by a signed 32-bit integer.
|
||||
This could become limiting, particularly in large dense segmentations.
|
||||
|
@@ -70,7 +70,7 @@ class QQuantity(QWidget):
|
||||
def __init__(
|
||||
self,
|
||||
value: Union[str, Quantity, Number] = 0,
|
||||
units: Union[UnitsContainer, str, Quantity] = None,
|
||||
units: Optional[Union[UnitsContainer, str, Quantity]] = None,
|
||||
ureg: Optional[UnitRegistry] = None,
|
||||
parent: Optional[QWidget] = None,
|
||||
) -> None:
|
||||
@@ -163,7 +163,7 @@ class QQuantity(QWidget):
|
||||
def setValue(
|
||||
self,
|
||||
value: Union[str, Quantity, Number],
|
||||
units: Union[UnitsContainer, str, Quantity] = None,
|
||||
units: Optional[Union[UnitsContainer, str, Quantity]] = None,
|
||||
) -> None:
|
||||
"""Set the current value (will cast to a pint Quantity)."""
|
||||
if isinstance(value, Quantity):
|
||||
|
@@ -12,12 +12,6 @@ from qtpy import QtGui
|
||||
|
||||
|
||||
def get_text_char_format(style):
|
||||
"""
|
||||
Return a QTextCharFormat with the given attributes.
|
||||
|
||||
https://pygments.org/docs/formatterdevelopment/#html-3-2-formatter
|
||||
"""
|
||||
|
||||
text_char_format = QtGui.QTextCharFormat()
|
||||
if hasattr(text_char_format, "setFontFamilies"):
|
||||
text_char_format.setFontFamilies(["monospace"])
|
||||
@@ -48,7 +42,8 @@ class QFormatter(Formatter):
|
||||
self._style = {name: get_text_char_format(style) for name, style in self.style}
|
||||
|
||||
def format(self, tokensource, outfile):
|
||||
"""
|
||||
"""Format the given token stream.
|
||||
|
||||
`outfile` is argument from parent class, but
|
||||
in Qt we do not produce string output, but QTextCharFormat, so it needs to be
|
||||
collected using `self.data`.
|
||||
@@ -56,12 +51,7 @@ class QFormatter(Formatter):
|
||||
self.data = []
|
||||
|
||||
for token, value in tokensource:
|
||||
self.data.extend(
|
||||
[
|
||||
self._style[token],
|
||||
]
|
||||
* len(value)
|
||||
)
|
||||
self.data.extend([self._style[token]] * len(value))
|
||||
|
||||
|
||||
class CodeSyntaxHighlight(QtGui.QSyntaxHighlighter):
|
||||
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from concurrent.futures import Future
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Callable, List, Optional, overload
|
||||
from typing import TYPE_CHECKING, Callable, overload
|
||||
|
||||
from qtpy.QtCore import (
|
||||
QCoreApplication,
|
||||
@@ -26,7 +26,7 @@ if TYPE_CHECKING:
|
||||
|
||||
class CallCallable(QObject):
|
||||
finished = Signal(object)
|
||||
instances: List[CallCallable] = []
|
||||
instances: list[CallCallable] = []
|
||||
|
||||
def __init__(self, callable, *args, **kwargs):
|
||||
super().__init__()
|
||||
@@ -69,7 +69,7 @@ def ensure_main_thread(
|
||||
|
||||
|
||||
def ensure_main_thread(
|
||||
func: Optional[Callable] = None, await_return: bool = False, timeout: int = 1000
|
||||
func: Callable | None = None, await_return: bool = False, timeout: int = 1000
|
||||
):
|
||||
"""Decorator that ensures a function is called in the main QApplication thread.
|
||||
|
||||
@@ -131,7 +131,7 @@ def ensure_object_thread(
|
||||
|
||||
|
||||
def ensure_object_thread(
|
||||
func: Optional[Callable] = None, await_return: bool = False, timeout: int = 1000
|
||||
func: Callable | None = None, await_return: bool = False, timeout: int = 1000
|
||||
):
|
||||
"""Decorator that ensures a QObject method is called in the object's thread.
|
||||
|
||||
|
@@ -28,7 +28,6 @@ class QMessageHandler:
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> handler = QMessageHandler()
|
||||
>>> handler.install() # now all Qt output will be available at mh.records
|
||||
|
||||
@@ -68,7 +67,7 @@ class QMessageHandler:
|
||||
return f"<{n} object at {hex(id(self))} with {len(self.records)} records>"
|
||||
|
||||
def __enter__(self):
|
||||
"""Enter a context with this handler installed"""
|
||||
"""Enter a context with this handler installed."""
|
||||
self.install()
|
||||
return self
|
||||
|
||||
|
@@ -8,15 +8,10 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Generator,
|
||||
Generic,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
|
||||
@@ -27,7 +22,7 @@ if TYPE_CHECKING:
|
||||
|
||||
class SigInst(Generic[_T]):
|
||||
@staticmethod
|
||||
def connect(slot: Callable[[_T], Any], type: Optional[type] = ...) -> None:
|
||||
def connect(slot: Callable[[_T], Any], type: type | None = ...) -> None:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
@@ -61,7 +56,7 @@ def as_generator_function(
|
||||
"""Turns a regular function (single return) into a generator function."""
|
||||
|
||||
@wraps(func)
|
||||
def genwrapper(*args, **kwargs) -> Generator[None, None, _R]:
|
||||
def genwrapper(*args: Any, **kwargs: Any) -> Generator[None, None, _R]:
|
||||
yield
|
||||
return func(*args, **kwargs)
|
||||
|
||||
@@ -93,7 +88,7 @@ class WorkerBase(QRunnable, Generic[_R]):
|
||||
"""
|
||||
|
||||
#: A set of Workers. Add to set using `WorkerBase.start`
|
||||
_worker_set: Set[WorkerBase] = set()
|
||||
_worker_set: set[WorkerBase] = set()
|
||||
returned: SigInst[_R]
|
||||
errored: SigInst[Exception]
|
||||
warned: SigInst[tuple]
|
||||
@@ -102,8 +97,8 @@ class WorkerBase(QRunnable, Generic[_R]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: Optional[Callable[_P, _R]] = None,
|
||||
SignalsClass: Type[WorkerBaseSignals] = WorkerBaseSignals,
|
||||
func: Callable[_P, _R] | None = None,
|
||||
SignalsClass: type[WorkerBaseSignals] = WorkerBaseSignals,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._abort_requested = False
|
||||
@@ -148,7 +143,7 @@ class WorkerBase(QRunnable, Generic[_R]):
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""Whether the worker has been started"""
|
||||
"""Whether the worker has been started."""
|
||||
return self._running
|
||||
|
||||
def run(self) -> None:
|
||||
@@ -202,7 +197,7 @@ class WorkerBase(QRunnable, Generic[_R]):
|
||||
self.finished.emit()
|
||||
self._finished.emit(self)
|
||||
|
||||
def work(self) -> Union[Exception, _R]:
|
||||
def work(self) -> Exception | _R:
|
||||
"""Main method to execute the worker.
|
||||
|
||||
The end-user should never need to call this function.
|
||||
@@ -267,7 +262,7 @@ class WorkerBase(QRunnable, Generic[_R]):
|
||||
cls._worker_set.discard(obj)
|
||||
|
||||
@classmethod
|
||||
def await_workers(cls, msecs: int = None) -> None:
|
||||
def await_workers(cls, msecs: int | None = None) -> None:
|
||||
"""Ask all workers to quit, and wait up to `msec` for quit.
|
||||
|
||||
Attempts to clean up all running workers by calling `worker.quit()`
|
||||
@@ -397,9 +392,9 @@ class GeneratorWorker(WorkerBase, Generic[_Y, _S, _R]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[_P, Generator[_Y, Optional[_S], _R]],
|
||||
func: Callable[_P, Generator[_Y, _S | None, _R]],
|
||||
*args,
|
||||
SignalsClass: Type[WorkerBaseSignals] = GeneratorWorkerSignals,
|
||||
SignalsClass: type[WorkerBaseSignals] = GeneratorWorkerSignals,
|
||||
**kwargs,
|
||||
):
|
||||
if not inspect.isgeneratorfunction(func):
|
||||
@@ -410,7 +405,7 @@ class GeneratorWorker(WorkerBase, Generic[_Y, _S, _R]):
|
||||
super().__init__(SignalsClass=SignalsClass)
|
||||
|
||||
self._gen = func(*args, **kwargs)
|
||||
self._incoming_value: Optional[_S] = None
|
||||
self._incoming_value: _S | None = None
|
||||
self._pause_requested = False
|
||||
self._resume_requested = False
|
||||
self._paused = False
|
||||
@@ -419,7 +414,7 @@ class GeneratorWorker(WorkerBase, Generic[_Y, _S, _R]):
|
||||
self._pause_interval = 0.01
|
||||
self.pbar = None
|
||||
|
||||
def work(self) -> Union[Optional[_R], Exception]:
|
||||
def work(self) -> _R | None | Exception:
|
||||
"""Core event loop that calls the original function.
|
||||
|
||||
Enters a continual loop, yielding and returning from the original
|
||||
@@ -445,8 +440,8 @@ class GeneratorWorker(WorkerBase, Generic[_Y, _S, _R]):
|
||||
self.paused.emit()
|
||||
continue
|
||||
try:
|
||||
input = self._next_value()
|
||||
output = self._gen.send(input)
|
||||
_input = self._next_value()
|
||||
output = self._gen.send(_input)
|
||||
self.yielded.emit(output)
|
||||
except StopIteration as exc:
|
||||
return exc.value
|
||||
@@ -460,7 +455,7 @@ class GeneratorWorker(WorkerBase, Generic[_Y, _S, _R]):
|
||||
"""Send a value into the function (if a generator was used)."""
|
||||
self._incoming_value = value
|
||||
|
||||
def _next_value(self) -> Optional[_S]:
|
||||
def _next_value(self) -> _S | None:
|
||||
out = None
|
||||
if self._incoming_value is not None:
|
||||
out = self._incoming_value
|
||||
@@ -499,9 +494,9 @@ class GeneratorWorker(WorkerBase, Generic[_Y, _S, _R]):
|
||||
def create_worker(
|
||||
func: Callable[_P, Generator[_Y, _S, _R]],
|
||||
*args,
|
||||
_start_thread: Optional[bool] = None,
|
||||
_connect: Optional[Dict[str, Union[Callable, Sequence[Callable]]]] = None,
|
||||
_worker_class: Union[Type[GeneratorWorker], Type[FunctionWorker], None] = None,
|
||||
_start_thread: bool | None = None,
|
||||
_connect: dict[str, Callable | Sequence[Callable]] | None = None,
|
||||
_worker_class: type[GeneratorWorker] | type[FunctionWorker] | None = None,
|
||||
_ignore_errors: bool = False,
|
||||
**kwargs,
|
||||
) -> GeneratorWorker[_Y, _S, _R]:
|
||||
@@ -512,9 +507,9 @@ def create_worker(
|
||||
def create_worker(
|
||||
func: Callable[_P, _R],
|
||||
*args,
|
||||
_start_thread: Optional[bool] = None,
|
||||
_connect: Optional[Dict[str, Union[Callable, Sequence[Callable]]]] = None,
|
||||
_worker_class: Union[Type[GeneratorWorker], Type[FunctionWorker], None] = None,
|
||||
_start_thread: bool | None = None,
|
||||
_connect: dict[str, Callable | Sequence[Callable]] | None = None,
|
||||
_worker_class: type[GeneratorWorker] | type[FunctionWorker] | None = None,
|
||||
_ignore_errors: bool = False,
|
||||
**kwargs,
|
||||
) -> FunctionWorker[_R]:
|
||||
@@ -524,12 +519,12 @@ def create_worker(
|
||||
def create_worker(
|
||||
func: Callable,
|
||||
*args,
|
||||
_start_thread: Optional[bool] = None,
|
||||
_connect: Optional[Dict[str, Union[Callable, Sequence[Callable]]]] = None,
|
||||
_worker_class: Union[Type[GeneratorWorker], Type[FunctionWorker], None] = None,
|
||||
_start_thread: bool | None = None,
|
||||
_connect: dict[str, Callable | Sequence[Callable]] | None = None,
|
||||
_worker_class: type[GeneratorWorker] | type[FunctionWorker] | None = None,
|
||||
_ignore_errors: bool = False,
|
||||
**kwargs,
|
||||
) -> Union[FunctionWorker, GeneratorWorker]:
|
||||
) -> FunctionWorker | GeneratorWorker:
|
||||
"""Convenience function to start a function in another thread.
|
||||
|
||||
By default, uses `FunctionWorker` for functions and `GeneratorWorker` for
|
||||
@@ -584,7 +579,7 @@ def create_worker(
|
||||
worker = create_worker(long_function, 10)
|
||||
```
|
||||
"""
|
||||
worker: Union[FunctionWorker, GeneratorWorker]
|
||||
worker: FunctionWorker | GeneratorWorker
|
||||
|
||||
if not _worker_class:
|
||||
if inspect.isgeneratorfunction(func):
|
||||
@@ -631,9 +626,9 @@ def create_worker(
|
||||
@overload
|
||||
def thread_worker(
|
||||
function: Callable[_P, Generator[_Y, _S, _R]],
|
||||
start_thread: Optional[bool] = None,
|
||||
connect: Optional[Dict[str, Union[Callable, Sequence[Callable]]]] = None,
|
||||
worker_class: Optional[Type[WorkerBase]] = None,
|
||||
start_thread: bool | None = None,
|
||||
connect: dict[str, Callable | Sequence[Callable]] | None = None,
|
||||
worker_class: type[WorkerBase] | None = None,
|
||||
ignore_errors: bool = False,
|
||||
) -> Callable[_P, GeneratorWorker[_Y, _S, _R]]:
|
||||
...
|
||||
@@ -642,9 +637,9 @@ def thread_worker(
|
||||
@overload
|
||||
def thread_worker(
|
||||
function: Callable[_P, _R],
|
||||
start_thread: Optional[bool] = None,
|
||||
connect: Optional[Dict[str, Union[Callable, Sequence[Callable]]]] = None,
|
||||
worker_class: Optional[Type[WorkerBase]] = None,
|
||||
start_thread: bool | None = None,
|
||||
connect: dict[str, Callable | Sequence[Callable]] | None = None,
|
||||
worker_class: type[WorkerBase] | None = None,
|
||||
ignore_errors: bool = False,
|
||||
) -> Callable[_P, FunctionWorker[_R]]:
|
||||
...
|
||||
@@ -653,19 +648,19 @@ def thread_worker(
|
||||
@overload
|
||||
def thread_worker(
|
||||
function: Literal[None] = None,
|
||||
start_thread: Optional[bool] = None,
|
||||
connect: Optional[Dict[str, Union[Callable, Sequence[Callable]]]] = None,
|
||||
worker_class: Optional[Type[WorkerBase]] = None,
|
||||
start_thread: bool | None = None,
|
||||
connect: dict[str, Callable | Sequence[Callable]] | None = None,
|
||||
worker_class: type[WorkerBase] | None = None,
|
||||
ignore_errors: bool = False,
|
||||
) -> Callable[[Callable], Callable[_P, Union[FunctionWorker, GeneratorWorker]]]:
|
||||
) -> Callable[[Callable], Callable[_P, FunctionWorker | GeneratorWorker]]:
|
||||
...
|
||||
|
||||
|
||||
def thread_worker(
|
||||
function: Optional[Callable] = None,
|
||||
start_thread: Optional[bool] = None,
|
||||
connect: Optional[Dict[str, Union[Callable, Sequence[Callable]]]] = None,
|
||||
worker_class: Optional[Type[WorkerBase]] = None,
|
||||
function: Callable | None = None,
|
||||
start_thread: bool | None = None,
|
||||
connect: dict[str, Callable | Sequence[Callable]] | None = None,
|
||||
worker_class: type[WorkerBase] | None = None,
|
||||
ignore_errors: bool = False,
|
||||
):
|
||||
"""Decorator that runs a function in a separate thread when called.
|
||||
@@ -800,28 +795,14 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def new_worker_qthread(
|
||||
Worker: Type[WorkerProtocol],
|
||||
Worker: type[WorkerProtocol],
|
||||
*args,
|
||||
_start_thread: bool = False,
|
||||
_connect: Dict[str, Callable] = None,
|
||||
_connect: dict[str, Callable] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""This is a convenience function to start a worker in a `QThread`.
|
||||
"""Convenience function to start a worker in a `QThread`.
|
||||
|
||||
In most cases, the [thread_worker][superqt.utils.thread_worker] decorator is
|
||||
sufficient and preferable. But this allows the user to completely customize the
|
||||
Worker object. However, they must then maintain control over the thread and clean up
|
||||
appropriately.
|
||||
|
||||
It follows the pattern described
|
||||
[here](https://www.qt.io/blog/2010/06/17/youre-doing-it-wrong) and in the [qt thread
|
||||
docs](https://doc.qt.io/qt-5/qthread.html#details)
|
||||
|
||||
see also:
|
||||
|
||||
https://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/
|
||||
|
||||
A QThread object is not a thread! It should be thought of as a class to *manage* a
|
||||
thread, not as the actual code or object that runs in that
|
||||
thread. The QThread object is created on the main thread and lives there.
|
||||
|
||||
@@ -892,7 +873,6 @@ def new_worker_qthread(
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
if _connect and not isinstance(_connect, dict):
|
||||
raise TypeError("_connect parameter must be a dict")
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
"""Adapted for python from the KDToolBox
|
||||
"""Adapted for python from the KDToolBox.
|
||||
|
||||
https://github.com/KDAB/KDToolBox/tree/master/qt/KDSignalThrottler
|
||||
|
||||
@@ -94,10 +94,10 @@ class GenericSignalThrottler(QObject):
|
||||
|
||||
def timeout(self) -> int:
|
||||
"""Return current timeout in milliseconds."""
|
||||
return self._timer.interval() # type: ignore
|
||||
return self._timer.interval()
|
||||
|
||||
def setTimeout(self, timeout: int) -> None:
|
||||
"""Set timeout in milliseconds"""
|
||||
"""Set timeout in milliseconds."""
|
||||
if self._timer.interval() != timeout:
|
||||
self._timer.setInterval(timeout)
|
||||
self.timeoutChanged.emit(timeout)
|
||||
@@ -230,7 +230,7 @@ def qthrottled(
|
||||
|
||||
@overload
|
||||
def qthrottled(
|
||||
func: "Literal[None]" = None,
|
||||
func: Optional["Literal[None]"] = None,
|
||||
timeout: int = 100,
|
||||
leading: bool = True,
|
||||
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
|
||||
@@ -289,7 +289,7 @@ def qdebounced(
|
||||
|
||||
@overload
|
||||
def qdebounced(
|
||||
func: "Literal[None]" = None,
|
||||
func: Optional["Literal[None]"] = None,
|
||||
timeout: int = 100,
|
||||
leading: bool = False,
|
||||
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
|
||||
@@ -371,10 +371,10 @@ def _make_decorator(
|
||||
throttle.throttle()
|
||||
return future
|
||||
|
||||
setattr(inner, "cancel", throttle.cancel) # noqa
|
||||
setattr(inner, "flush", throttle.flush) # noqa
|
||||
setattr(inner, "set_timeout", throttle.setTimeout) # noqa
|
||||
setattr(inner, "triggered", throttle.triggered) # noqa
|
||||
inner.cancel = throttle.cancel
|
||||
inner.flush = throttle.flush
|
||||
inner.set_timeout = throttle.setTimeout
|
||||
inner.triggered = throttle.triggered
|
||||
return inner # type: ignore
|
||||
|
||||
return deco(func) if func is not None else deco
|
||||
|
Reference in New Issue
Block a user