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:
Talley Lambert
2022-12-01 08:21:03 -05:00
committed by GitHub
parent 7b2d8bfb2d
commit 6abd3a21a6
24 changed files with 264 additions and 287 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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",
],
)

View File

@@ -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

View File

@@ -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 (?)

View File

@@ -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(

View File

@@ -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:

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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()

View File

@@ -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.

View File

@@ -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):

View File

@@ -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):

View File

@@ -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.

View File

@@ -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

View File

@@ -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")

View File

@@ -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