diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c3f052..c20a4f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/examples/throttler_demo.py b/examples/throttler_demo.py index ddc0a3e..99badc0 100644 --- a/examples/throttler_demo.py +++ b/examples/throttler_demo.py @@ -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 diff --git a/mkdocs.yml b/mkdocs.yml index fe33368..873e81a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/pyproject.toml b/pyproject.toml index d417cc8..8a4815f 100644 --- a/pyproject.toml +++ b/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" ] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..57275ee --- /dev/null +++ b/setup.py @@ -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", + ], +) diff --git a/src/superqt/__init__.py b/src/superqt/__init__.py index 37a4aaf..6751b68 100644 --- a/src/superqt/__init__.py +++ b/src/superqt/__init__.py @@ -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 diff --git a/src/superqt/_eliding_label.py b/src/superqt/_eliding_label.py index 877f613..21eaddc 100644 --- a/src/superqt/_eliding_label.py +++ b/src/superqt/_eliding_label.py @@ -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 (?) diff --git a/src/superqt/collapsible/_collapsible.py b/src/superqt/collapsible/_collapsible.py index 2fcc085..7fb19e5 100644 --- a/src/superqt/collapsible/_collapsible.py +++ b/src/superqt/collapsible/_collapsible.py @@ -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( diff --git a/src/superqt/combobox/_enum_combobox.py b/src/superqt/combobox/_enum_combobox.py index 80abc5f..7243673 100644 --- a/src/superqt/combobox/_enum_combobox.py +++ b/src/superqt/combobox/_enum_combobox.py @@ -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: diff --git a/src/superqt/combobox/_searchable_combo_box.py b/src/superqt/combobox/_searchable_combo_box.py index 2fe16de..07a9b31 100644 --- a/src/superqt/combobox/_searchable_combo_box.py +++ b/src/superqt/combobox/_searchable_combo_box.py @@ -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() diff --git a/src/superqt/fonticon/__init__.py b/src/superqt/fonticon/__init__.py index e4c7b55..12105a1 100644 --- a/src/superqt/fonticon/__init__.py +++ b/src/superqt/fonticon/__init__.py @@ -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 diff --git a/src/superqt/fonticon/_animations.py b/src/superqt/fonticon/_animations.py index e01c4ab..7ed5cf2 100644 --- a/src/superqt/fonticon/_animations.py +++ b/src/superqt/fonticon/_animations.py @@ -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) diff --git a/src/superqt/fonticon/_iconfont.py b/src/superqt/fonticon/_iconfont.py index e486c41..33a5391 100644 --- a/src/superqt/fonticon/_iconfont.py +++ b/src/superqt/fonticon/_iconfont.py @@ -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: diff --git a/src/superqt/fonticon/_qfont_icon.py b/src/superqt/fonticon/_qfont_icon.py index e8517a1..3884f7c 100644 --- a/src/superqt/fonticon/_qfont_icon.py +++ b/src/superqt/fonticon/_qfont_icon.py @@ -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: diff --git a/src/superqt/sliders/_generic_range_slider.py b/src/superqt/sliders/_generic_range_slider.py index d46ad1d..9509af2 100644 --- a/src/superqt/sliders/_generic_range_slider.py +++ b/src/superqt/sliders/_generic_range_slider.py @@ -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 diff --git a/src/superqt/sliders/_generic_slider.py b/src/superqt/sliders/_generic_slider.py index 038ca38..d6dc481 100644 --- a/src/superqt/sliders/_generic_slider.py +++ b/src/superqt/sliders/_generic_slider.py @@ -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: diff --git a/src/superqt/sliders/_labeled.py b/src/superqt/sliders/_labeled.py index 1bf50ed..ac203cb 100644 --- a/src/superqt/sliders/_labeled.py +++ b/src/superqt/sliders/_labeled.py @@ -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() diff --git a/src/superqt/spinbox/_intspin.py b/src/superqt/spinbox/_intspin.py index 84f8ccb..843d1b8 100644 --- a/src/superqt/spinbox/_intspin.py +++ b/src/superqt/spinbox/_intspin.py @@ -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. diff --git a/src/superqt/spinbox/_quantity.py b/src/superqt/spinbox/_quantity.py index 869d707..fc50b3c 100644 --- a/src/superqt/spinbox/_quantity.py +++ b/src/superqt/spinbox/_quantity.py @@ -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): diff --git a/src/superqt/utils/_code_syntax_highlight.py b/src/superqt/utils/_code_syntax_highlight.py index 11d137c..cfd7d79 100644 --- a/src/superqt/utils/_code_syntax_highlight.py +++ b/src/superqt/utils/_code_syntax_highlight.py @@ -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): diff --git a/src/superqt/utils/_ensure_thread.py b/src/superqt/utils/_ensure_thread.py index a29632f..aadadc6 100644 --- a/src/superqt/utils/_ensure_thread.py +++ b/src/superqt/utils/_ensure_thread.py @@ -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. diff --git a/src/superqt/utils/_message_handler.py b/src/superqt/utils/_message_handler.py index 51259c2..4e0629f 100644 --- a/src/superqt/utils/_message_handler.py +++ b/src/superqt/utils/_message_handler.py @@ -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 diff --git a/src/superqt/utils/_qthreading.py b/src/superqt/utils/_qthreading.py index 8ef9c2d..b096680 100644 --- a/src/superqt/utils/_qthreading.py +++ b/src/superqt/utils/_qthreading.py @@ -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") diff --git a/src/superqt/utils/_throttler.py b/src/superqt/utils/_throttler.py index 86e18f1..a4f5181 100644 --- a/src/superqt/utils/_throttler.py +++ b/src/superqt/utils/_throttler.py @@ -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