Compare commits

..

31 Commits

Author SHA1 Message Date
Talley Lambert
34b9851b36 chore: changelog v0.7.0 2024-12-14 14:41:40 -05:00
Talley Lambert
8ede2a2f39 build: support py313 (#264)
* build: drop py38

* bump min typing ext

* add py313

* only use pyqt6

* fix ubunt
2024-12-14 12:37:26 -05:00
Hanjin Liu
df008464cc Fix KeyError in CodeSyntaxHighlight (#258)
* use dict.get

* typing

* Update src/superqt/utils/_code_syntax_highlight.py

Co-authored-by: Grzegorz Bokota <bokota+github@gmail.com>

---------

Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
Co-authored-by: Grzegorz Bokota <bokota+github@gmail.com>
2024-12-14 12:37:17 -05:00
Talley Lambert
e99adaac03 Revert "remove stylesheet on sliderLabel (#254)" (#265)
This reverts commit 7e92b81711.
2024-12-14 12:36:48 -05:00
Talley Lambert
8a40170c89 build: drop py38 (#263)
* build: drop py38

* bump min typing ext

* ignore cleanup warning from pyside6

* change minreq

* bump min

* fix for pint again
2024-12-13 09:30:27 -05:00
Gabriel Selzer
2f3113f0f6 End painter when drawing colormap (#262)
* End painter when drawing colormap

* Only end painter if we created it
2024-12-12 19:27:54 -05:00
pre-commit-ci[bot]
c9528ff85a ci: [pre-commit.ci] autoupdate (#257)
* ci: [pre-commit.ci] autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.9 → v0.8.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.9...v0.8.1)
- [github.com/abravalheri/validate-pyproject: v0.20.2 → v0.23](https://github.com/abravalheri/validate-pyproject/compare/v0.20.2...v0.23)
- [github.com/pre-commit/mirrors-mypy: v1.11.2 → v1.13.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.2...v1.13.0)

* style: [pre-commit.ci] auto fixes [...]

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-03 11:03:25 -05:00
Gabriel Selzer
e7a87897f5 fix: minimum size hint for QElidingLabel (#260) 2024-11-26 16:53:12 -05:00
pre-commit-ci[bot]
952ac336bf ci: [pre-commit.ci] autoupdate (#253)
* ci: [pre-commit.ci] autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.7 → v0.6.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.7...v0.6.3)
- [github.com/abravalheri/validate-pyproject: v0.18 → v0.19](https://github.com/abravalheri/validate-pyproject/compare/v0.18...v0.19)
- [github.com/pre-commit/mirrors-mypy: v1.10.0 → v1.11.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.0...v1.11.2)

* style: [pre-commit.ci] auto fixes [...]

* fix lint

* update

* no pyside 6.8

* update pins

* quotes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
2024-10-12 12:35:17 -04:00
Talley Lambert
7e92b81711 remove stylesheet on sliderLabel (#254) 2024-10-12 12:01:32 -04:00
Talley Lambert
ac4adf5234 chore: changelog v0.6.8 2024-06-15 16:58:36 -04:00
Talley Lambert
5f68795a82 feat: graceful offline fallback for qiconify (#251) 2024-06-15 07:54:40 -04:00
Talley Lambert
17ad1079a8 chore: changelog v0.6.7 2024-06-07 16:39:41 -04:00
pre-commit-ci[bot]
6bb050c499 ci: [pre-commit.ci] autoupdate (#250)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.3 → v0.4.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.3...v0.4.7)
- [github.com/abravalheri/validate-pyproject: v0.16 → v0.18](https://github.com/abravalheri/validate-pyproject/compare/v0.16...v0.18)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-07 16:36:52 -04:00
Talley Lambert
1f4d9081b9 fix: prevent qthrottled and qdebounced from holding strong references with bound methods (#247)
* finish

* linting

* done

* use weakmethod, add signature

* add test for warning
2024-06-03 10:24:03 -04:00
Daniel Althviz Moré
7b1aefd119 Prevent computing full document content highlight and only parse current block content for performance (#246) 2024-05-28 07:11:57 -04:00
Talley Lambert
0ec5cd3a2f chore: changelog v0.6.6 2024-05-12 11:11:56 -04:00
Talley Lambert
8f62b0b00d perf: improve paint time for QColormapLineEdit (#245) 2024-05-12 10:32:59 -04:00
pre-commit-ci[bot]
4a0aaca2e9 ci: [pre-commit.ci] autoupdate (#244)
* ci: [pre-commit.ci] autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.4.3)
- [github.com/pre-commit/mirrors-mypy: v1.9.0 → v1.10.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.9.0...v1.10.0)

* style: [pre-commit.ci] auto fixes [...]

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-06 19:18:03 -04:00
Talley Lambert
2d49e77c3d chore: changelog v0.6.5 2024-05-06 17:45:31 -04:00
Talley Lambert
ba495a5e72 fix: fix a number of issues with Labeled and Range Sliders, add LabelsOnHandle mode. (#242)
* fix: remove processEvents

* merge in fixes

* remove comment

* fix hint

* fix napari

* change pyqt6

* fix: fix range slider styles
2024-05-06 17:43:53 -04:00
Talley Lambert
12f10be8da ci: trying to fix tests on various platforms (#243)
* ci: attempt1

* add lxml_html_clean

* fix napari version name

* breakout coverage

* use main

* cump

* bump again

* bump

* bump

* skip more napari tests

* add always

* back to v1

* try editabel

* use main again

* remove editable

* editable again

* bump

* bump

* bump

* use v2
2024-05-06 15:29:43 -04:00
Talley Lambert
9ca0bbf858 chore: changelog v0.6.4 2024-04-25 15:56:57 -04:00
Talley Lambert
0ab6758972 fix: fix inverted appearance (#240)
* fix: fix inverted appearance

* style: [pre-commit.ci] auto fixes [...]

* pass codecov token

* inherit secrets

* explicitly pass token

* pin "'PyQt6<6.7'"

* pin upper pyqt6

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-25 15:55:02 -04:00
Talley Lambert
d2bc3d898c drop to macos-13 2024-04-25 13:54:44 -04:00
Talley Lambert
1bb1a58a73 inherit secret 2024-04-22 14:08:53 -04:00
Talley Lambert
1288250597 add secret 2024-04-22 13:51:54 -04:00
pre-commit-ci[bot]
34a776e8d0 ci: [pre-commit.ci] autoupdate (#238)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.0 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.0...v0.3.5)
- [github.com/pre-commit/mirrors-mypy: v1.8.0 → v1.9.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.8.0...v1.9.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-22 13:15:40 -04:00
Talley Lambert
146644e105 chore: changelog v0.6.3 2024-03-27 17:34:31 -04:00
Talley Lambert
e7873ad93d fix: fix sliderReleased, sliderPressed signals, and setTracking (#237) 2024-03-27 17:32:25 -04:00
dependabot[bot]
0396d465e2 ci(dependabot): bump softprops/action-gh-release from 1 to 2 (#236)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 15:56:10 -04:00
44 changed files with 598 additions and 236 deletions

View File

@@ -16,18 +16,18 @@ on:
jobs:
test:
name: Test
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v1
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v2
with:
os: ${{ matrix.platform }}
python-version: ${{ matrix.python-version }}
qt: ${{ matrix.backend }}
pip-install-pre-release: ${{ github.event_name == 'schedule' }}
report-failures: ${{ github.event_name == 'schedule' }}
coverage-upload: artifact
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.8", "3.9", "3.10", "3.11"]
platform: [ubuntu-latest, windows-latest, macos-13]
python-version: ["3.9", "3.10", "3.11", "3.12"]
backend: [pyqt5, pyside2, pyqt6]
exclude:
# Abort (core dumped) on linux pyqt6, unknown reason
@@ -36,57 +36,71 @@ jobs:
# lack of wheels for pyside2/py3.11
- python-version: "3.11"
backend: pyside2
include:
# https://bugreports.qt.io/browse/PYSIDE-2627
- python-version: "3.10"
platform: macos-latest
backend: "'pyside6!=6.6.2'"
- python-version: "3.11"
platform: macos-latest
backend: "'pyside6!=6.6.2'"
- python-version: "3.10"
platform: windows-latest
backend: "'pyside6!=6.6.2'"
- python-version: "3.11"
platform: windows-latest
backend: "'pyside6!=6.6.2'"
- python-version: "3.12"
backend: pyside2
- python-version: "3.12"
backend: pyqt5
include:
- python-version: "3.13"
platform: windows-latest
backend: "pyqt6"
- python-version: "3.13"
platform: ubuntu-latest
backend: "pyqt6"
- python-version: "3.10"
platform: macos-latest
backend: pyqt6
backend: "'pyside6<6.8'"
- python-version: "3.11"
platform: macos-latest
backend: "'pyside6<6.8'"
- python-version: "3.10"
platform: windows-latest
backend: "'pyside6<6.8'"
- python-version: "3.12"
platform: windows-latest
backend: "'pyside6<6.8'"
# legacy Qt
- python-version: 3.8
- python-version: 3.9
platform: ubuntu-latest
backend: "pyqt5==5.12.*"
- python-version: 3.8
- python-version: 3.9
platform: ubuntu-latest
backend: "pyqt5==5.13.*"
- python-version: 3.8
- python-version: 3.9
platform: ubuntu-latest
backend: "pyqt5==5.14.*"
test-qt-minreqs:
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v1
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v2
with:
python-version: "3.8"
python-version: "3.9"
qt: pyqt5
pip-post-installs: 'qtpy==1.1.0 typing-extensions==3.7.4.3'
pip-post-installs: "qtpy==1.1.0 typing-extensions==4.5.0" # 4.5.0 is just for pint
pip-install-flags: -e
coverage-upload: artifact
upload_coverage:
if: always()
needs: [test, test-qt-minreqs]
uses: pyapp-kit/workflows/.github/workflows/upload-coverage.yml@v2
secrets: inherit
test_napari:
uses: pyapp-kit/workflows/.github/workflows/test-dependents.yml@v1
uses: pyapp-kit/workflows/.github/workflows/test-dependents.yml@v2
with:
dependency-repo: napari/napari
dependency-ref: ${{ matrix.napari-version }}
dependency-extras: 'testing'
dependency-extras: "testing"
qt: ${{ matrix.qt }}
pytest-args: 'napari/_qt -k "not async and not qt_dims_2"'
pytest-args: 'napari/_qt -k "not async and not qt_dims_2 and not qt_viewer_console_focus and not keybinding_editor"'
python-version: "3.10"
post-install-cmd: "pip install lxml_html_clean"
strategy:
fail-fast: false
matrix:
napari-version: ["", "v0.4.18"]
napari-version: ["", "v0.4.19.post1"]
qt: ["pyqt5", "pyside2"]
check-manifest:
@@ -125,6 +139,6 @@ jobs:
twine check dist/*
twine upload dist/*
- uses: softprops/action-gh-release@v1
- uses: softprops/action-gh-release@v2
with:
generate_release_notes: true

View File

@@ -5,19 +5,19 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
rev: v0.8.1
hooks:
- id: ruff
args: [--fix, --unsafe-fixes]
- id: ruff-format
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.16
rev: v0.23
hooks:
- id: validate-pyproject
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
rev: v1.13.0
hooks:
- id: mypy
exclude: tests|examples

View File

@@ -1,5 +1,91 @@
# Changelog
## [v0.7.0](https://github.com/pyapp-kit/superqt/tree/v0.7.0) (2024-12-14)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.8...v0.7.0)
**Fixed bugs:**
- fix: End painter when drawing colormap [\#262](https://github.com/pyapp-kit/superqt/pull/262) ([gselzer](https://github.com/gselzer))
- fix: minimum size hint for QElidingLabel [\#260](https://github.com/pyapp-kit/superqt/pull/260) ([gselzer](https://github.com/gselzer))
- fix: KeyError in CodeSyntaxHighlight [\#258](https://github.com/pyapp-kit/superqt/pull/258) ([hanjinliu](https://github.com/hanjinliu))
**Refactors:**
- chore: Revert "remove stylesheet on sliderLabel \(\#254\)" [\#265](https://github.com/pyapp-kit/superqt/pull/265) ([tlambert03](https://github.com/tlambert03))
- refactor: remove stylesheet on sliderLabel [\#254](https://github.com/pyapp-kit/superqt/pull/254) ([tlambert03](https://github.com/tlambert03))
**Merged pull requests:**
- build: support py313 [\#264](https://github.com/pyapp-kit/superqt/pull/264) ([tlambert03](https://github.com/tlambert03))
- build: drop py38 [\#263](https://github.com/pyapp-kit/superqt/pull/263) ([tlambert03](https://github.com/tlambert03))
- ci: \[pre-commit.ci\] autoupdate [\#257](https://github.com/pyapp-kit/superqt/pull/257) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
- ci: \[pre-commit.ci\] autoupdate [\#253](https://github.com/pyapp-kit/superqt/pull/253) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
## [v0.6.8](https://github.com/pyapp-kit/superqt/tree/v0.6.8) (2024-06-15)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.7...v0.6.8)
**Implemented enhancements:**
- feat: graceful offline fallback for qiconify [\#251](https://github.com/pyapp-kit/superqt/pull/251) ([tlambert03](https://github.com/tlambert03))
## [v0.6.7](https://github.com/pyapp-kit/superqt/tree/v0.6.7) (2024-06-07)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.6...v0.6.7)
**Fixed bugs:**
- fix: prevent qthrottled and qdebounced from holding strong references with bound methods [\#247](https://github.com/pyapp-kit/superqt/pull/247) ([tlambert03](https://github.com/tlambert03))
**Merged pull requests:**
- Prevent computing full document content highlight per block and only compute current block content for performance [\#246](https://github.com/pyapp-kit/superqt/pull/246) ([dalthviz](https://github.com/dalthviz))
## [v0.6.6](https://github.com/pyapp-kit/superqt/tree/v0.6.6) (2024-05-12)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.5...v0.6.6)
**Refactors:**
- perf: improve paint time for QColormapLineEdit [\#245](https://github.com/pyapp-kit/superqt/pull/245) ([tlambert03](https://github.com/tlambert03))
## [v0.6.5](https://github.com/pyapp-kit/superqt/tree/v0.6.5) (2024-05-06)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.4...v0.6.5)
**Implemented enhancements:**
- fix: fix a number of issues with Labeled and Range Sliders, add LabelsOnHandle mode. [\#242](https://github.com/pyapp-kit/superqt/pull/242) ([tlambert03](https://github.com/tlambert03))
**Tests & CI:**
- ci: trying to fix tests on various platforms [\#243](https://github.com/pyapp-kit/superqt/pull/243) ([tlambert03](https://github.com/tlambert03))
## [v0.6.4](https://github.com/pyapp-kit/superqt/tree/v0.6.4) (2024-04-25)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.3...v0.6.4)
**Fixed bugs:**
- fix: fix inverted appearance [\#240](https://github.com/pyapp-kit/superqt/pull/240) ([tlambert03](https://github.com/tlambert03))
**Merged pull requests:**
- ci: \[pre-commit.ci\] autoupdate [\#238](https://github.com/pyapp-kit/superqt/pull/238) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
## [v0.6.3](https://github.com/pyapp-kit/superqt/tree/v0.6.3) (2024-03-27)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.2...v0.6.3)
**Fixed bugs:**
- fix: fix sliderReleased, sliderPressed signals, and setTracking [\#237](https://github.com/pyapp-kit/superqt/pull/237) ([tlambert03](https://github.com/tlambert03))
**Merged pull requests:**
- ci\(dependabot\): bump softprops/action-gh-release from 1 to 2 [\#236](https://github.com/pyapp-kit/superqt/pull/236) ([dependabot[bot]](https://github.com/apps/dependabot))
## [v0.6.2](https://github.com/pyapp-kit/superqt/tree/v0.6.2) (2024-03-06)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.1...v0.6.2)
@@ -408,13 +494,17 @@
## [v0.2.1](https://github.com/pyapp-kit/superqt/tree/v0.2.1) (2021-07-10)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.0rc0...v0.2.1)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.0rc1...v0.2.1)
**Fixed bugs:**
- Fix QLabeledRangeSlider API \(fix slider proxy\) [\#10](https://github.com/pyapp-kit/superqt/pull/10) ([tlambert03](https://github.com/tlambert03))
- Fix range slider with negative min range [\#9](https://github.com/pyapp-kit/superqt/pull/9) ([tlambert03](https://github.com/tlambert03))
## [v0.2.0rc1](https://github.com/pyapp-kit/superqt/tree/v0.2.0rc1) (2021-06-26)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.0rc0...v0.2.0rc1)
## [v0.2.0rc0](https://github.com/pyapp-kit/superqt/tree/v0.2.0rc0) (2021-06-26)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.0...v0.2.0rc0)

View File

@@ -26,7 +26,7 @@ pytest
All widgets must be well-tested, and should work on:
- Python 3.8 and above
- Python 3.9 and above
- PyQt5 (5.11 and above) & PyQt6
- PySide2 (5.11 and above) & PySide6
- macOS, Windows, & Linux

View File

@@ -15,7 +15,7 @@ that are not provided in the native QtWidgets module.
Components are tested on:
- macOS, Windows, & Linux
- Python 3.8 and above
- Python 3.9 and above
- PyQt5 (5.11 and above) & PyQt6
- PySide2 (5.11 and above) & PySide6

View File

@@ -10,7 +10,7 @@ QtWidgets module.
Components are tested on:
- macOS, Windows, & Linux
- Python 3.8 and above
- Python 3.9 and above
- PyQt5 (5.11 and above) & PyQt6
- PySide2 (5.11 and above) & PySide6

View File

@@ -6,7 +6,7 @@ from qtpy import QtWidgets as QtW
# patch for Qt 5.15 on macos >= 12
os.environ["USE_MAC_SLIDER_PATCH"] = "1"
from superqt import QRangeSlider # noqa
from superqt import QRangeSlider
QSS = """
QSlider {

View File

@@ -88,7 +88,7 @@ class IconPreviewArea(QtWidgets.QWidget):
self.updatePixmapLabels()
def createHeaderLabel(self, text):
label = QtWidgets.QLabel("<b>%s</b>" % text)
label = QtWidgets.QLabel(f"<b>{text}</b>")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
return label

View File

@@ -27,7 +27,7 @@ SOFTWARE.
"""
from typing import Deque
from collections import deque
from qtpy.QtCore import QRect, QSize, Qt, QTimer, Signal
from qtpy.QtGui import QPainter, QPen
@@ -65,8 +65,8 @@ class DrawSignalsWidget(QWidget):
self._scrollTimer.timeout.connect(self._scroll)
self._scrollTimer.start()
self._signalActivations: Deque[int] = Deque()
self._throttledSignalActivations: Deque[int] = Deque()
self._signalActivations: deque[int] = deque()
self._throttledSignalActivations: deque[int] = deque()
def sizeHint(self):
return QSize(400, 200)
@@ -84,7 +84,7 @@ class DrawSignalsWidget(QWidget):
self.update()
def scrollAndCut(self, v: Deque[int], cutoff: int):
def scrollAndCut(self, v: deque[int], cutoff: int):
L = len(v)
for p in range(L):
v[p] += 1
@@ -121,7 +121,7 @@ class DrawSignalsWidget(QWidget):
p.drawLine(0, h2, w, h2)
p.restore()
def _drawSignals(self, p: QPainter, v: Deque[int], color, yStart, yEnd):
def _drawSignals(self, p: QPainter, v: deque[int], color, yStart, yEnd):
p.save()
pen = QPen()
pen.setWidthF(2.0)

View File

@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
name = "superqt"
description = "Missing widgets and components for PyQt/PySide"
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.9"
license = { text = "BSD 3-Clause License" }
authors = [{ email = "talley.lambert@gmail.com", name = "Talley Lambert" }]
keywords = [
@@ -28,11 +28,11 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Desktop Environment",
"Topic :: Software Development :: User Interfaces",
"Topic :: Software Development :: Widget Sets",
@@ -41,13 +41,21 @@ dynamic = ["version"]
dependencies = [
"pygments>=2.4.0",
"qtpy>=1.1.0",
"typing-extensions >=3.7.4.3,!=3.10.0.0",
"typing-extensions >=3.7.4.3,!=3.10.0.0", # however, pint requires >4.5.0
]
# extras
# https://peps.python.org/pep-0621/#dependencies-optional-dependencies
[project.optional-dependencies]
test = ["pint", "pytest", "pytest-cov", "pytest-qt", "numpy", "cmap", "pyconify"]
test = [
"pint",
"pytest",
"pytest-cov",
"pytest-qt",
"numpy",
"cmap",
"pyconify",
]
dev = [
"ipython",
"ruff",
@@ -58,7 +66,13 @@ dev = [
"rich",
"types-Pygments",
]
docs = ["mkdocs-macros-plugin", "mkdocs-material", "mkdocstrings[python]", "pint", "cmap"]
docs = [
"mkdocs-macros-plugin",
"mkdocs-material",
"mkdocstrings[python]",
"pint",
"cmap",
]
quantity = ["pint"]
cmap = ["cmap >=0.1.1"]
pyside2 = ["pyside2"]
@@ -66,9 +80,9 @@ pyside2 = ["pyside2"]
# https://github.com/pyapp-kit/superqt/pull/177
# https://github.com/pyapp-kit/superqt/pull/164
# https://bugreports.qt.io/browse/PYSIDE-2627
pyside6 = ["pyside6 !=6.5.0,!=6.5.1,!=6.6.2"]
pyside6 = ["pyside6 !=6.5.0,!=6.5.1,!=6.6.2,<6.8"]
pyqt5 = ["pyqt5"]
pyqt6 = ["pyqt6"]
pyqt6 = ["pyqt6<6.7"]
font-fa5 = ["fonticon-fontawesome5"]
font-fa6 = ["fonticon-fontawesome6"]
font-mi6 = ["fonticon-materialdesignicons6"]
@@ -100,21 +114,30 @@ python = ["3.11"]
[[tool.hatch.envs.test.matrix]]
qt = ["pyside2", "pyqt5", "pyqt5.12"]
python = ["3.8"]
python = ["3.9"]
[tool.hatch.envs.test.overrides]
matrix.qt.extra-dependencies = [
{value = "pyside2", if = ["pyside2"]},
{value = "pyside6", if = ["pyside6"]},
{value = "pyqt5", if = ["pyqt5"]},
{value = "pyqt6", if = ["pyqt6"]},
{value = "pyqt5==5.12", if = ["pyqt5.12"]},
{ value = "pyside2", if = [
"pyside2",
] },
{ value = "pyside6", if = [
"pyside6",
] },
{ value = "pyqt5", if = [
"pyqt5",
] },
{ value = "pyqt6", if = [
"pyqt6",
] },
{ value = "pyqt5==5.12", if = [
"pyqt5.12",
] },
]
# https://github.com/charliermarsh/ruff
[tool.ruff]
line-length = 88
target-version = "py38"
target-version = "py39"
src = ["src", "tests"]
# https://docs.astral.sh/ruff/rules
@@ -132,7 +155,7 @@ select = [
"B", # flake8-bugbear
"A001", # flake8-builtins
"RUF", # ruff-specific rules
"TCH", # flake8-type-checking
"TC", # flake8-type-checking
"TID", # flake8-tidy-imports
]
ignore = [
@@ -155,9 +178,11 @@ minversion = "6.0"
testpaths = ["tests"]
filterwarnings = [
"error",
"ignore:Failed to disconnect::pytestqt",
"ignore:QPixmapCache.find:DeprecationWarning:",
"ignore:SelectableGroups dict interface:DeprecationWarning",
"ignore:The distutils package is deprecated:DeprecationWarning",
"ignore:.*Skipping callback call set_result",
]
# https://mypy.readthedocs.io/en/stable/config_file.html
@@ -191,7 +216,7 @@ exclude_lines = [
"@overload",
"except ImportError",
"\\.\\.\\.",
"pass"
"pass",
]
# https://github.com/mgedmin/check-manifest#configuration

View File

@@ -26,8 +26,6 @@ from .spinbox import QLargeIntSpinBox
from .utils import QMessageHandler, ensure_main_thread, ensure_object_thread
__all__ = [
"ensure_main_thread",
"ensure_object_thread",
"QCollapsible",
"QColorComboBox",
"QColormapComboBox",
@@ -36,8 +34,8 @@ __all__ = [
"QElidingLabel",
"QElidingLineEdit",
"QEnumComboBox",
"QLabeledDoubleRangeSlider",
"QIconifyIcon",
"QLabeledDoubleRangeSlider",
"QLabeledDoubleSlider",
"QLabeledRangeSlider",
"QLabeledSlider",
@@ -48,11 +46,13 @@ __all__ = [
"QSearchableComboBox",
"QSearchableListWidget",
"QSearchableTreeWidget",
"ensure_main_thread",
"ensure_object_thread",
]
if TYPE_CHECKING:
from .combobox import QColormapComboBox # noqa: TCH004
from .spinbox._quantity import QQuantity # noqa: TCH004
from .combobox import QColormapComboBox # noqa: TC004
from .spinbox._quantity import QQuantity # noqa: TC004
def __getattr__(name: str) -> Any:

View File

@@ -15,9 +15,9 @@ from ._cmap_line_edit import QColormapLineEdit
from ._cmap_utils import draw_colormap
__all__ = [
"QColormapItemDelegate",
"draw_colormap",
"QColormapLineEdit",
"CmapCatalogComboBox",
"QColormapComboBox",
"QColormapItemDelegate",
"QColormapLineEdit",
"draw_colormap",
]

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Container
from typing import TYPE_CHECKING
from cmap import Colormap
from qtpy.QtCore import Qt, Signal
@@ -11,6 +11,8 @@ from ._cmap_line_edit import QColormapLineEdit
from ._cmap_utils import try_cast_colormap
if TYPE_CHECKING:
from collections.abc import Container
from cmap._catalog import Category, Interpolation
from qtpy.QtGui import QKeyEvent

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Sequence
from typing import TYPE_CHECKING, Any
from cmap import Colormap
from qtpy.QtCore import Qt, Signal
@@ -23,6 +23,8 @@ from ._cmap_line_edit import QColormapLineEdit
from ._cmap_utils import try_cast_colormap
if TYPE_CHECKING:
from collections.abc import Sequence
from cmap._colormap import ColorStopsLike

View File

@@ -1,8 +1,8 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from qtpy.QtCore import Qt
from qtpy.QtCore import QRect, Qt
from qtpy.QtGui import QIcon, QPainter, QPaintEvent, QPalette
from qtpy.QtWidgets import QApplication, QLineEdit, QStyle, QWidget
@@ -103,6 +103,19 @@ class QColormapLineEdit(QLineEdit):
def _cmap_is_full_width(self):
return self._colormap_fraction >= 0.75
def _cmap_rect(self) -> QRect:
cmap_rect = self.rect().adjusted(2, 0, 0, 0)
cmap_rect.setWidth(int(cmap_rect.width() * self._colormap_fraction))
return cmap_rect
def resizeEvent(self, e: Any) -> None:
left_margin = 6
if not self._cmap_is_full_width():
# leave room for the colormap
left_margin += self._cmap_rect().width()
self.setTextMargins(left_margin, 2, 0, 0)
super().resizeEvent(e)
def paintEvent(self, e: QPaintEvent) -> None:
# don't draw the background
# otherwise it will cover the colormap during super().paintEvent
@@ -112,15 +125,7 @@ class QColormapLineEdit(QLineEdit):
palette.setColor(palette.ColorRole.Base, Qt.GlobalColor.transparent)
self.setPalette(palette)
cmap_rect = self.rect().adjusted(2, 0, 0, 0)
cmap_rect.setWidth(int(cmap_rect.width() * self._colormap_fraction))
left_margin = 6
if not self._cmap_is_full_width():
# leave room for the colormap
left_margin += cmap_rect.width()
self.setTextMargins(left_margin, 2, 0, 0)
cmap_rect = self._cmap_rect()
if self._cmap:
draw_colormap(
self, self._cmap, cmap_rect, checkerboard_size=self._checkerboard_size

View File

@@ -121,6 +121,10 @@ def draw_colormap(
painter.setBrush(gradient)
painter.drawRect(rect)
# If we created a new Painter, free its resources
if isinstance(painter_or_device, QPaintDevice):
painter.end()
def _draw_checkerboard(
painter: QPainter, rect: QRect | QRectF, checker_size: int

View File

@@ -13,7 +13,7 @@ __all__ = (
if TYPE_CHECKING:
from superqt.cmap import QColormapComboBox # noqa: TCH004
from superqt.cmap import QColormapComboBox # noqa: TC004
def __getattr__(name: str) -> Any: # pragma: no cover

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import warnings
from contextlib import suppress
from enum import IntEnum, auto
from typing import Any, Literal, Sequence, cast
from typing import TYPE_CHECKING, Any, Literal, cast
from qtpy.QtCore import QModelIndex, QPersistentModelIndex, QRect, QSize, Qt, Signal
from qtpy.QtGui import QColor, QPainter
@@ -19,6 +19,9 @@ from qtpy.QtWidgets import (
from superqt.utils import signals_blocked
if TYPE_CHECKING:
from collections.abc import Sequence
_NAME_MAP = {QColor(x).name(): x for x in QColor.colorNames()}
COLOR_ROLE = Qt.ItemDataRole.BackgroundRole

View File

@@ -3,7 +3,7 @@ from enum import Enum, EnumMeta, Flag
from functools import reduce
from itertools import combinations
from operator import or_
from typing import Optional, Tuple, TypeVar
from typing import Optional, TypeVar
from qtpy.QtCore import Signal
from qtpy.QtWidgets import QComboBox
@@ -47,7 +47,7 @@ def _get_name(enum_value: Enum):
return name
def _get_name_with_value(enum_value: Enum) -> Tuple[str, Enum]:
def _get_name_with_value(enum_value: Enum) -> tuple[str, Enum]:
return _get_name(enum_value), enum_value

View File

@@ -1,5 +1,3 @@
from typing import List
from qtpy.QtCore import Qt
from qtpy.QtGui import QFont, QFontMetrics, QTextLayout
@@ -36,7 +34,7 @@ class _GenericEliding:
self._ellipses_width = width
@staticmethod
def wrapText(text, width, font=None) -> List[str]:
def wrapText(text, width, font=None) -> list[str]:
"""Returns `text`, split as it would be wrapped for `width`, given `font`.
Static method.
@@ -74,5 +72,5 @@ class _GenericEliding:
# join them
return "".join(text[:nlines] + [last_line])
def _wrappedText(self) -> List[str]:
def _wrappedText(self) -> list[str]:
return _GenericEliding.wrapText(self._text, self.width(), self.font())

View File

@@ -73,3 +73,10 @@ class QElidingLabel(_GenericEliding, QLabel):
flags = int(self.alignment() | Qt.TextFlag.TextWordWrap)
r = fm.boundingRect(QRect(QPoint(0, 0), self.size()), flags, self._text)
return QSize(self.width(), r.height())
def minimumSizeHint(self) -> QSize:
# The smallest that self._elidedText can be is just the ellipsis.
fm = QFontMetrics(self.font())
flags = int(self.alignment() | Qt.TextFlag.TextWordWrap)
r = fm.boundingRect(QRect(QPoint(0, 0), self.size()), flags, "...")
return QSize(r.width(), r.height())

View File

@@ -1,16 +1,16 @@
from __future__ import annotations
__all__ = [
"addFont",
"Animation",
"ENTRY_POINT",
"font",
"icon",
"Animation",
"IconFont",
"IconFontMeta",
"IconOpts",
"pulse",
"QIconifyIcon",
"addFont",
"font",
"icon",
"pulse",
"setTextIcon",
"spin",
]

View File

@@ -1,4 +1,5 @@
from typing import Mapping, Type, Union
from collections.abc import Mapping
from typing import Union
FONTFILE_ATTR = "__font_file__"
@@ -69,7 +70,7 @@ class IconFont(metaclass=IconFontMeta):
__font_file__ = "..."
def namespace2font(namespace: Union[Mapping, Type], name: str) -> Type[IconFont]:
def namespace2font(namespace: Union[Mapping, type], name: str) -> type[IconFont]:
"""Convenience to convert a namespace (class, module, dict) into an IconFont."""
if isinstance(namespace, type):
if not isinstance(getattr(namespace, FONTFILE_ATTR), str):

View File

@@ -1,5 +1,5 @@
import contextlib
from typing import ClassVar, Dict, List, Set, Tuple
from typing import ClassVar
from ._iconfont import IconFontMeta, namespace2font
@@ -11,9 +11,9 @@ except ImportError:
class FontIconManager:
ENTRY_POINT: ClassVar[str] = "superqt.fonticon"
_PLUGINS: ClassVar[Dict[str, EntryPoint]] = {}
_LOADED: ClassVar[Dict[str, IconFontMeta]] = {}
_BLOCKED: ClassVar[Set[EntryPoint]] = set()
_PLUGINS: ClassVar[dict[str, EntryPoint]] = {}
_LOADED: ClassVar[dict[str, IconFontMeta]] = {}
_BLOCKED: ClassVar[set[EntryPoint]] = set()
def _discover_fonts(self) -> None:
self._PLUGINS.clear()
@@ -86,15 +86,15 @@ _manager = FontIconManager()
get_font_class = _manager._get_font_class
def discover() -> Tuple[str]:
def discover() -> tuple[str]:
_manager._discover_fonts()
def available() -> Tuple[str]:
def available() -> tuple[str]:
return tuple(_manager._PLUGINS)
def loaded(load_all=False) -> Dict[str, List[str]]:
def loaded(load_all=False) -> dict[str, list[str]]:
if load_all:
discover()
for x in available():

View File

@@ -2,9 +2,10 @@ from __future__ import annotations
import warnings
from collections import abc, defaultdict
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, ClassVar, DefaultDict, Sequence, Tuple, Union, cast
from typing import TYPE_CHECKING, ClassVar, Union, cast
from qtpy import QT_VERSION
from qtpy.QtCore import QObject, QPoint, QRect, QSize, Qt
@@ -47,8 +48,8 @@ ValidColor = Union[
int,
str,
Qt.GlobalColor,
Tuple[int, int, int, int],
Tuple[int, int, int],
tuple[int, int, int, int],
tuple[int, int, int],
None,
]
@@ -159,7 +160,7 @@ class _QFontIconEngine(QIconEngine):
def __init__(self, options: _IconOptions):
super().__init__()
self._opts: defaultdict[QIcon.State, dict[QIcon.Mode, _IconOptions | None]] = (
DefaultDict(dict)
defaultdict(dict)
)
self._opts[QIcon.State.Off][QIcon.Mode.Normal] = options
self.update_hash()

View File

@@ -1,9 +1,11 @@
from __future__ import annotations
import warnings
from typing import TYPE_CHECKING
from qtpy.QtCore import QSize
from qtpy.QtGui import QIcon
from qtpy.QtCore import QSize, Qt
from qtpy.QtGui import QIcon, QPainter, QPixmap
from qtpy.QtWidgets import QApplication
if TYPE_CHECKING:
from typing import Literal
@@ -122,5 +124,25 @@ class QIconifyIcon(QIcon):
state : QIcon.State, optional
State specified for the icon, passed to `QIcon.addFile`.
"""
path = svg_path(*key, color=color, flip=flip, rotate=rotate, dir=dir)
self.addFile(str(path), size or QSize(), mode, state)
try:
path = svg_path(*key, color=color, flip=flip, rotate=rotate, dir=dir)
except OSError:
warnings.warn(
f"Unable to connect to internet, and icon {key} not cached.",
stacklevel=2,
)
self._draw_text_fallback(key)
else:
self.addFile(str(path), size or QSize(), mode, state)
def _draw_text_fallback(self, key: tuple[str, ...]) -> None:
if style := QApplication.style():
pixmap = style.standardPixmap(style.StandardPixmap.SP_MessageBoxQuestion)
else:
pixmap = QPixmap(18, 18)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "?")
painter.end()
self.addPixmap(pixmap)

View File

@@ -1,5 +1,6 @@
import logging
from typing import Any, Iterable, Mapping
from collections.abc import Iterable, Mapping
from typing import Any
from qtpy.QtCore import QRegularExpression
from qtpy.QtWidgets import QLineEdit, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget

View File

@@ -8,6 +8,7 @@ from ._range_style import MONTEREY_SLIDER_STYLES_FIX
from ._sliders import QDoubleRangeSlider, QDoubleSlider, QRangeSlider
__all__ = [
"MONTEREY_SLIDER_STYLES_FIX",
"QDoubleRangeSlider",
"QDoubleSlider",
"QLabeledDoubleRangeSlider",
@@ -15,5 +16,4 @@ __all__ = [
"QLabeledRangeSlider",
"QLabeledSlider",
"QRangeSlider",
"MONTEREY_SLIDER_STYLES_FIX",
]

View File

@@ -1,4 +1,5 @@
from typing import List, Optional, Sequence, Tuple, TypeVar, Union
from collections.abc import Sequence
from typing import Optional, TypeVar, Union
from qtpy import QtGui
from qtpy.QtCore import Property, QEvent, QPoint, QPointF, QRect, QRectF, Qt, Signal
@@ -42,11 +43,11 @@ class _GenericRangeSlider(_GenericSlider):
self.valueChanged = self._valuesChanged
self.sliderMoved = self._slidersMoved
# list of values
self._value: List[_T] = [20, 80]
self._value: list[_T] = [20, 80]
# list of current positions of each handle. same length as _value
# If tracking is enabled (the default) this will be identical to _value
self._position: List[_T] = [20, 80]
self._position: list[_T] = [20, 80]
# which handle is being pressed/hovered
self._pressedIndex = 0
@@ -103,7 +104,7 @@ class _GenericRangeSlider(_GenericSlider):
"""Show the bar between the first and last handle."""
self.setBarVisible(True)
def applyMacStylePatch(self) -> str:
def applyMacStylePatch(self) -> None:
"""Apply a QSS patch to fix sliders on macos>=12 with QT < 6.
see [FAQ](../faq.md#sliders-not-dragging-properly-on-macos-12) for more details.
@@ -113,7 +114,7 @@ class _GenericRangeSlider(_GenericSlider):
# ############### QtOverrides #######################
def value(self) -> Tuple[_T, ...]:
def value(self) -> tuple[_T, ...]:
"""Get current value of the widget as a tuple of integers."""
return tuple(self._value)
@@ -124,11 +125,27 @@ class _GenericRangeSlider(_GenericSlider):
"""
return tuple(float(i) for i in self._position)
def setSliderPosition(self, pos: Union[float, Sequence[float]], index=None) -> None:
def setSliderPosition( # type: ignore
self,
pos: Union[float, Sequence[float]],
index: Optional[int] = None,
*,
reversed: bool = False,
) -> None:
"""Set current position of the handles with a sequence of integers.
If `pos` is a sequence, it must have the same length as `value()`.
If it is a scalar, index will be
Parameters
----------
pos : Union[float, Sequence[float]]
The new position of the slider handle(s). If a sequence, it must have the
same length as `value()`. If it is a scalar, index will be used to set the
position of the handle at that index.
index : int | None
The index of the handle to set the position of. If None, the "pressedIndex"
will be used.
reversed : bool
Order in which to set the positions. Can be useful when setting multiple
positions, to avoid intermediate overlapping values.
"""
if isinstance(pos, (list, tuple)):
val_len = len(self.value())
@@ -139,6 +156,9 @@ class _GenericRangeSlider(_GenericSlider):
else:
pairs = [(self._pressedIndex if index is None else index, pos)]
if reversed:
pairs = pairs[::-1]
for idx, position in pairs:
self._position[idx] = self._bound(position, idx)
@@ -222,7 +242,7 @@ class _GenericRangeSlider(_GenericSlider):
offset = self.maximum() - ref[-1]
elif ref[0] + offset < self.minimum():
offset = self.minimum() - ref[0]
self.setSliderPosition([i + offset for i in ref])
self.setSliderPosition([i + offset for i in ref], reversed=offset > 0)
def _fixStyleOption(self, option):
pass
@@ -313,7 +333,7 @@ class _GenericRangeSlider(_GenericSlider):
# NOTE: this is very much tied to mousepress... not a generic "get control"
def _getControlAtPos(
self, pos: QPoint, opt: Optional[QStyleOptionSlider] = None
) -> Tuple[QStyle.SubControl, int]:
) -> tuple[QStyle.SubControl, int]:
"""Update self._pressedControl based on ev.pos()."""
opt = opt or self._styleOption

View File

@@ -74,6 +74,7 @@ class _GenericSlider(QSlider):
self._position: _T = 0.0
self._singleStep = 1.0
self._offsetAccumulated = 0.0
self._inverted_appearance = False
self._blocktracking = False
self._tickInterval = 0.0
self._pressedControl = SC_NONE
@@ -98,7 +99,7 @@ class _GenericSlider(QSlider):
if USE_MAC_SLIDER_PATCH:
self.applyMacStylePatch()
def applyMacStylePatch(self) -> str:
def applyMacStylePatch(self) -> None:
"""Apply a QSS patch to fix sliders on macos>=12 with QT < 6.
see [FAQ](../faq.md#sliders-not-dragging-properly-on-macos-12) for more details.
@@ -174,6 +175,13 @@ class _GenericSlider(QSlider):
self._tickInterval = max(0.0, ts)
self.update()
def invertedAppearance(self) -> bool:
return self._inverted_appearance
def setInvertedAppearance(self, inverted: bool) -> None:
self._inverted_appearance = inverted
self.update()
def triggerAction(self, action: QSlider.SliderAction) -> None:
self._blocktracking = True
# other actions here
@@ -193,9 +201,8 @@ class _GenericSlider(QSlider):
if self.orientation() == Qt.Orientation.Horizontal
else not self.invertedAppearance()
)
option.direction = (
Qt.LayoutDirection.LeftToRight
) # we use the upsideDown option instead
# we use the upsideDown option instead
option.direction = Qt.LayoutDirection.LeftToRight
# option.sliderValue = self._value # type: ignore
# option.singleStep = self._singleStep # type: ignore
if self.orientation() == Qt.Orientation.Horizontal:
@@ -335,8 +342,12 @@ class _GenericSlider(QSlider):
option.sliderValue = self._to_qinteger_space(self._value - self._minimum)
def _to_qinteger_space(self, val, _max=None):
"""Converts a value to the internal integer space."""
_max = _max or self.MAX_DISPLAY
return int(min(QOVERFLOW, val / (self._maximum - self._minimum) * _max))
range_ = self._maximum - self._minimum
if range_ == 0:
return self._minimum
return int(min(QOVERFLOW, val / range_ * _max))
def _pick(self, pt: QPoint) -> int:
return pt.x() if self.orientation() == Qt.Orientation.Horizontal else pt.y()

View File

@@ -3,13 +3,13 @@ from __future__ import annotations
import contextlib
from enum import IntEnum, IntFlag, auto
from functools import partial
from typing import Any, overload
from typing import TYPE_CHECKING, Any, overload
from qtpy.QtCore import QPoint, QSize, Qt, Signal
from qtpy import QtGui
from qtpy.QtCore import Property, QPoint, QSize, Qt, Signal
from qtpy.QtGui import QFontMetrics, QValidator
from qtpy.QtWidgets import (
QAbstractSlider,
QApplication,
QBoxLayout,
QDoubleSpinBox,
QHBoxLayout,
@@ -25,6 +25,9 @@ from superqt.utils import signals_blocked
from ._sliders import QDoubleRangeSlider, QDoubleSlider, QRangeSlider
if TYPE_CHECKING:
from collections.abc import Iterable
class LabelPosition(IntEnum):
NoLabel = 0
@@ -32,6 +35,7 @@ class LabelPosition(IntEnum):
LabelsBelow = auto()
LabelsRight = LabelsAbove
LabelsLeft = LabelsBelow
LabelsOnHandle = auto()
class EdgeLabelMode(IntFlag):
@@ -43,10 +47,10 @@ class EdgeLabelMode(IntFlag):
class _SliderProxy:
_slider: QSlider
def value(self) -> int:
def value(self) -> Any:
return self._slider.value()
def setValue(self, value: int) -> None:
def setValue(self, value: Any) -> None:
self._slider.setValue(value)
def sliderPosition(self) -> int:
@@ -94,6 +98,36 @@ class _SliderProxy:
def setTickPosition(self, pos: QSlider.TickPosition) -> None:
self._slider.setTickPosition(pos)
def triggerAction(self, action: QAbstractSlider.SliderAction) -> None:
return self._slider.triggerAction(action)
def invertedControls(self) -> bool:
return self._slider.invertedControls()
def setInvertedControls(self, a0: bool) -> None:
return self._slider.setInvertedControls(a0)
def invertedAppearance(self) -> bool:
return self._slider.invertedAppearance()
def setInvertedAppearance(self, a0: bool) -> None:
return self._slider.setInvertedAppearance(a0)
def isSliderDown(self) -> bool:
return self._slider.isSliderDown()
def setSliderDown(self, a0: bool) -> None:
return self._slider.setSliderDown(a0)
def hasTracking(self) -> bool:
return self._slider.hasTracking()
def setTracking(self, enable: bool) -> None:
return self._slider.setTracking(enable)
def orientation(self) -> Qt.Orientation:
return self._slider.orientation()
def __getattr__(self, name: Any) -> Any:
return getattr(self._slider, name)
@@ -128,6 +162,9 @@ def _handle_overloaded_slider_sig(
class QLabeledSlider(_SliderProxy, QAbstractSlider):
editingFinished = Signal()
_ivalueChanged = Signal(int)
_isliderMoved = Signal(int)
_irangeChanged = Signal(int, int)
_slider_class = QSlider
_slider: QSlider
@@ -227,8 +264,6 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
self.layout().setContentsMargins(0, 0, 0, 0)
self._on_slider_range_changed(self.minimum(), self.maximum())
QApplication.processEvents()
# putting this after labelMode methods for the sake of mypy
EdgeLabelMode = EdgeLabelMode
@@ -249,8 +284,9 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
self._slider.setValue(int(value))
def _rename_signals(self) -> None:
# for subclasses
pass
self.valueChanged = self._ivalueChanged
self.sliderMoved = self._isliderMoved
self.rangeChanged = self._irangeChanged
class QLabeledDoubleSlider(QLabeledSlider):
@@ -290,6 +326,8 @@ class QLabeledDoubleSlider(QLabeledSlider):
class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
_valueChanged = Signal(tuple)
_sliderPressed = Signal()
_sliderReleased = Signal()
editingFinished = Signal()
_slider_class = QRangeSlider
@@ -309,7 +347,7 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
self._rename_signals()
self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating)
self._handle_labels = []
self._handle_labels: list[SliderLabel] = []
self._handle_label_position: LabelPosition = LabelPosition.LabelsAbove
# for fine tuning label position
@@ -318,6 +356,8 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
self._slider = self._slider_class()
self._slider.valueChanged.connect(self.valueChanged.emit)
self._slider.sliderPressed.connect(self.sliderPressed.emit)
self._slider.sliderReleased.connect(self.sliderReleased.emit)
self._slider.rangeChanged.connect(self.rangeChanged.emit)
self.sliderMoved = self._slider._slidersMoved
@@ -352,10 +392,10 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
"""Set where/whether labels are shown adjacent to slider handles."""
self._handle_label_position = opt
for lbl in self._handle_labels:
if not opt:
lbl.hide()
else:
lbl.show()
lbl.setVisible(bool(opt))
trans = opt == LabelPosition.LabelsOnHandle
# TODO: make double clickable to edit
lbl.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, trans)
self.setOrientation(self.orientation())
def edgeLabelMode(self) -> EdgeLabelMode:
@@ -381,27 +421,33 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
elif opt == EdgeLabelMode.LabelIsRange:
self._min_label.setValue(self._slider.minimum())
self._max_label.setValue(self._slider.maximum())
QApplication.processEvents()
self._reposition_labels()
def setRange(self, min: int, max: int) -> None:
self._on_range_changed(min, max)
def _add_labels(self, layout: QBoxLayout, inverted: bool = False) -> None:
if inverted:
first, second = self._max_label, self._min_label
else:
first, second = self._min_label, self._max_label
layout.addWidget(first)
layout.addWidget(self._slider)
layout.addWidget(second)
def setOrientation(self, orientation: Qt.Orientation) -> None:
"""Set orientation, value will be 'horizontal' or 'vertical'."""
self._slider.setOrientation(orientation)
inverted = self._slider.invertedAppearance()
marg = (0, 0, 0, 0)
if orientation == Qt.Orientation.Vertical:
layout: QBoxLayout = QVBoxLayout()
layout.setSpacing(1)
layout.addWidget(self._max_label)
layout.addWidget(self._slider)
layout.addWidget(self._min_label)
self._add_labels(layout, inverted=not inverted)
# TODO: set margins based on label width
if self._handle_label_position == LabelPosition.LabelsLeft:
marg = (30, 0, 0, 0)
elif self._handle_label_position == LabelPosition.NoLabel:
marg = (0, 0, 0, 0)
else:
elif self._handle_label_position == LabelPosition.LabelsRight:
marg = (0, 0, 20, 0)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
else:
@@ -409,13 +455,9 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
layout.setSpacing(7)
if self._handle_label_position == LabelPosition.LabelsBelow:
marg = (0, 0, 0, 25)
elif self._handle_label_position == LabelPosition.NoLabel:
marg = (0, 0, 0, 0)
else:
elif self._handle_label_position == LabelPosition.LabelsAbove:
marg = (0, 25, 0, 0)
layout.addWidget(self._min_label)
layout.addWidget(self._slider)
layout.addWidget(self._max_label)
self._add_labels(layout, inverted=inverted)
# remove old layout
old_layout = self.layout()
@@ -425,10 +467,13 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
self.setLayout(layout)
layout.setContentsMargins(*marg)
super().setOrientation(orientation)
QApplication.processEvents()
self._reposition_labels()
def resizeEvent(self, a0) -> None:
def setInvertedAppearance(self, a0: bool) -> None:
self._slider.setInvertedAppearance(a0)
self.setOrientation(self._slider.orientation())
def resizeEvent(self, a0: Any) -> None:
super().resizeEvent(a0)
self._reposition_labels()
@@ -436,9 +481,20 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
LabelPosition = LabelPosition
EdgeLabelMode = EdgeLabelMode
def _getBarColor(self) -> QtGui.QBrush:
return self._slider._style.brush(self._slider._styleOption)
def _setBarColor(self, color: str) -> None:
self._slider._style.brush_active = color
barColor = Property(QtGui.QBrush, _getBarColor, _setBarColor)
"""The color of the bar between the first and last handle."""
# ------------- private methods ----------------
def _rename_signals(self) -> None:
self.valueChanged = self._valueChanged
self.sliderReleased = self._sliderReleased
self.sliderPressed = self._sliderPressed
def _reposition_labels(self) -> None:
if (
@@ -449,17 +505,26 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
horizontal = self.orientation() == Qt.Orientation.Horizontal
labels_above = self._handle_label_position == LabelPosition.LabelsAbove
labels_on_handle = self._handle_label_position == LabelPosition.LabelsOnHandle
last_edge = None
for i, label in enumerate(self._handle_labels):
labels: Iterable[tuple[int, SliderLabel]] = enumerate(self._handle_labels)
if self._slider.invertedAppearance():
labels = reversed(list(labels))
for i, label in labels:
rect = self._slider._handleRect(i)
dx = -label.width() / 2
dx = (-label.width() / 2) + 2
dy = -label.height() / 2
if labels_above:
if labels_above: # or on the right
if horizontal:
dy *= 3
else:
dx *= -1
elif labels_on_handle:
if horizontal:
dy += 0.5
else:
dx += 0.5
else:
if horizontal:
dy *= -1
@@ -476,6 +541,7 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
label.move(pos)
last_edge = pos
label.clearFocus()
label.raise_()
label.show()
self.update()
@@ -563,6 +629,15 @@ class QLabeledDoubleRangeSlider(QLabeledRangeSlider):
for lbl in self._handle_labels:
lbl.setDecimals(prec)
def _getBarColor(self) -> QtGui.QBrush:
return self._slider._style.brush(self._slider._styleOption)
def _setBarColor(self, color: str) -> None:
self._slider._style.brush_active = color
barColor = Property(QtGui.QBrush, _getBarColor, _setBarColor)
"""The color of the bar between the first and last handle."""
class SliderLabel(QDoubleSpinBox):
def __init__(

View File

@@ -5,7 +5,6 @@ import re
from dataclasses import dataclass, replace
from typing import TYPE_CHECKING
from qtpy import QT_VERSION
from qtpy.QtCore import Qt
from qtpy.QtGui import (
QBrush,
@@ -140,8 +139,9 @@ CATALINA_STYLE = replace(
tick_offset=4,
)
if QT_VERSION and int(QT_VERSION.split(".")[0]) == 6:
CATALINA_STYLE = replace(CATALINA_STYLE, tick_offset=2)
# I can no longer reproduce the cases in which this was necessary
# if QT_VERSION and int(QT_VERSION.split(".")[0]) == 6:
# CATALINA_STYLE = replace(CATALINA_STYLE, tick_offset=2)
BIG_SUR_STYLE = replace(
CATALINA_STYLE,
@@ -155,8 +155,9 @@ BIG_SUR_STYLE = replace(
tick_bar_alpha=0.2,
)
if QT_VERSION and int(QT_VERSION.split(".")[0]) == 6:
BIG_SUR_STYLE = replace(BIG_SUR_STYLE, tick_offset=-3)
# I can no longer reproduce the cases in which this was necessary
# if QT_VERSION and int(QT_VERSION.split(".")[0]) == 6:
# BIG_SUR_STYLE = replace(BIG_SUR_STYLE, tick_offset=-3)
WINDOWS_STYLE = replace(
BASE_STYLE,
@@ -229,7 +230,7 @@ rgba_pattern = re.compile(
)
def parse_color(color: str, default_attr) -> QColor | QGradient:
def parse_color(color: str, default_attr: str) -> QColor | QGradient:
qc = QColor(color)
if qc.isValid():
return qc
@@ -241,6 +242,7 @@ def parse_color(color: str, default_attr) -> QColor | QGradient:
# try linear gradient:
match = qlineargrad_pattern.search(color)
grad: QGradient
if match:
grad = QLinearGradient(*(float(i) for i in match.groups()[:4]))
grad.setColorAt(0, QColor(match.groupdict()["stop0"]))
@@ -259,11 +261,11 @@ def parse_color(color: str, default_attr) -> QColor | QGradient:
return QColor(getattr(SYSTEM_STYLE, default_attr))
def update_styles_from_stylesheet(obj: _GenericRangeSlider):
def update_styles_from_stylesheet(obj: _GenericRangeSlider) -> None:
qss: str = obj.styleSheet()
parent = obj.parent()
while parent is not None:
while parent and hasattr(parent, "styleSheet"):
qss = parent.styleSheet() + qss
parent = parent.parent()
qss = QApplication.instance().styleSheet() + qss

View File

@@ -1,27 +1,27 @@
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from superqt.cmap import draw_colormap # noqa: TCH004
from superqt.cmap import draw_colormap # noqa: TC004
__all__ = (
"CodeSyntaxHighlight",
"FunctionWorker",
"GeneratorWorker",
"QMessageHandler",
"QSignalDebouncer",
"QSignalThrottler",
"WorkerBase",
"create_worker",
"qimage_to_array",
"draw_colormap",
"ensure_main_thread",
"ensure_object_thread",
"exceptions_as_dialog",
"FunctionWorker",
"GeneratorWorker",
"new_worker_qthread",
"qdebounced",
"QMessageHandler",
"QSignalDebouncer",
"QSignalThrottler",
"qimage_to_array",
"qthrottled",
"signals_blocked",
"thread_worker",
"WorkerBase",
)
from ._code_syntax_highlight import CodeSyntaxHighlight

View File

@@ -1,5 +1,3 @@
from itertools import takewhile
from pygments import highlight
from pygments.formatter import Formatter
from pygments.lexers import find_lexer_class, get_lexer_by_name
@@ -11,7 +9,9 @@ from qtpy import QtGui
# https://pygments.org/docs/formatterdevelopment/#html-3-2-formatter
def get_text_char_format(style):
def get_text_char_format(
style: dict[str, QtGui.QTextCharFormat],
) -> QtGui.QTextCharFormat:
text_char_format = QtGui.QTextCharFormat()
if hasattr(text_char_format, "setFontFamilies"):
text_char_format.setFontFamilies(["monospace"])
@@ -38,7 +38,7 @@ def get_text_char_format(style):
class QFormatter(Formatter):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.data = []
self.data: list[QtGui.QTextCharFormat] = []
self._style = {name: get_text_char_format(style) for name, style in self.style}
def format(self, tokensource, outfile):
@@ -51,7 +51,11 @@ class QFormatter(Formatter):
self.data = []
for token, value in tokensource:
self.data.extend([self._style[token]] * len(value))
# using get method to workaround not defined style for plain token
# https://github.com/pygments/pygments/issues/2149
self.data.extend(
[self._style.get(token, QtGui.QTextCharFormat())] * len(value)
)
class CodeSyntaxHighlight(QtGui.QSyntaxHighlighter):
@@ -68,21 +72,10 @@ class CodeSyntaxHighlight(QtGui.QSyntaxHighlighter):
return self.formatter.style.background_color
def highlightBlock(self, text):
cb = self.currentBlock()
p = cb.position()
text_ = self.document().toPlainText() + "\n"
highlight(text_, self.lexer, self.formatter)
enters = sum(1 for _ in takewhile(lambda x: x == "\n", text_))
# pygments lexer ignore leading empty lines, so we need to do correction
# here calculating the number of empty lines.
# dirty, dirty hack
# The core problem is that pygemnts by default use string streams,
# that will not handle QTextCharFormat, so we need use `data` property to
# work around this.
highlight(text, self.lexer, self.formatter)
for i in range(len(text)):
try:
self.setFormat(i, 1, self.formatter.data[p + i - enters])
except IndexError: # pragma: no cover
pass
self.setFormat(i, 1, self.formatter.data[i])

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from concurrent.futures import Future
from contextlib import suppress
from functools import wraps
from typing import TYPE_CHECKING, Any, Callable, ClassVar, overload
@@ -41,7 +42,8 @@ class CallCallable(QObject):
def call(self):
CallCallable.instances.remove(self)
res = self._callable(*self._args, **self._kwargs)
self.finished.emit(res)
with suppress(RuntimeError):
self.finished.emit(res)
# fmt: off

View File

@@ -1,5 +1,6 @@
from collections.abc import Iterator
from contextlib import contextmanager
from typing import TYPE_CHECKING, Iterator
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from qtpy.QtCore import QObject

View File

@@ -9,9 +9,7 @@ from typing import (
Any,
Callable,
ClassVar,
Generator,
Generic,
Sequence,
TypeVar,
overload,
)
@@ -19,6 +17,8 @@ from typing import (
from qtpy.QtCore import QObject, QRunnable, QThread, QThreadPool, QTimer, Signal
if TYPE_CHECKING:
from collections.abc import Generator, Sequence
_T = TypeVar("_T")
class SigInst(Generic[_T]):

View File

@@ -29,11 +29,15 @@ SOFTWARE.
from __future__ import annotations
import warnings
from concurrent.futures import Future
from contextlib import suppress
from enum import IntFlag, auto
from functools import wraps
from typing import TYPE_CHECKING, Callable, Generic, TypeVar, overload
from weakref import WeakKeyDictionary
from inspect import signature
from types import MethodType
from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, overload
from weakref import WeakKeyDictionary, WeakMethod
from qtpy.QtCore import QObject, Qt, QTimer, Signal
@@ -53,6 +57,12 @@ else:
P = TypeVar("P")
R = TypeVar("R")
REF_ERROR = (
"To use qthrottled or qdebounced as a method decorator, "
"objects must have `__dict__` or be weak referenceable. "
"Please either add `__weakref__` to `__slots__` or use"
"qthrottled/qdebounced as a function (not a decorator)."
)
class Kind(IntFlag):
@@ -157,7 +167,7 @@ class GenericSignalThrottler(QObject):
self.triggered.emit()
self._timer.start()
def _maybeEmitTriggered(self, restart_timer=True) -> None:
def _maybeEmitTriggered(self, restart_timer: bool = True) -> None:
if self._hasPendingEmission:
self._emitTriggered()
if not restart_timer:
@@ -203,6 +213,26 @@ class QSignalDebouncer(GenericSignalThrottler):
# below here part is unique to superqt (not from KD)
def _weak_func(func: Callable[P, R]) -> Callable[P, R]:
if isinstance(func, MethodType):
# this is a bound method, we need to avoid strong references
try:
weak_method = WeakMethod(func)
except TypeError as e:
raise TypeError(REF_ERROR) from e
def weak_func(*args, **kwargs):
if method := weak_method():
return method(*args, **kwargs)
warnings.warn(
"Method has been garbage collected", RuntimeWarning, stacklevel=2
)
return weak_func
return func
class ThrottledCallable(GenericSignalThrottler, Generic[P, R]):
def __init__(
self,
@@ -214,26 +244,32 @@ class ThrottledCallable(GenericSignalThrottler, Generic[P, R]):
super().__init__(kind, emissionPolicy, parent)
self._future: Future[R] = Future()
if isinstance(func, staticmethod):
self._func = func.__func__
else:
self._func = func
self.__wrapped__ = func
self._is_static_method: bool = False
if isinstance(func, staticmethod):
self._is_static_method = True
func = func.__func__
max_args = get_max_args(func)
with suppress(TypeError, ValueError):
self.__signature__ = signature(func)
self._func = _weak_func(func)
self.__wrapped__ = self._func
self._args: tuple = ()
self._kwargs: dict = {}
self.triggered.connect(self._set_future_result)
self._name = None
self._obj_dkt = WeakKeyDictionary()
self._obj_dkt: WeakKeyDictionary[Any, ThrottledCallable] = WeakKeyDictionary()
# even if we were to compile __call__ with a signature matching that of func,
# PySide wouldn't correctly inspect the signature of the ThrottledCallable
# instance: https://bugreports.qt.io/browse/PYSIDE-2423
# so we do it ourselfs and limit the number of positional arguments
# that we pass to func
self._max_args: int | None = get_max_args(self._func)
self._max_args: int | None = max_args
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> "Future[R]": # noqa
if not self._future.done():
@@ -251,12 +287,18 @@ class ThrottledCallable(GenericSignalThrottler, Generic[P, R]):
self._future.set_result(result)
def __set_name__(self, owner, name):
if not isinstance(self.__wrapped__, staticmethod):
if not self._is_static_method:
self._name = name
def _get_throttler(self, instance, owner, parent, obj):
def _get_throttler(self, instance, owner, parent, obj, name):
try:
bound_method = self._func.__get__(instance, owner)
except Exception as e: # pragma: no cover
raise RuntimeError(
f"Failed to bind function {self._func!r} to object {instance!r}"
) from e
throttler = ThrottledCallable(
self.__wrapped__.__get__(instance, owner),
bound_method,
self._kind,
self._emissionPolicy,
parent=parent,
@@ -264,21 +306,12 @@ class ThrottledCallable(GenericSignalThrottler, Generic[P, R]):
throttler.setTimerType(self.timerType())
throttler.setTimeout(self.timeout())
try:
setattr(
obj,
self._name,
throttler,
)
setattr(obj, name, throttler)
except AttributeError:
try:
self._obj_dkt[obj] = throttler
except TypeError as e:
raise TypeError(
"To use qthrottled or qdebounced as a method decorator, "
"objects must have `__dict__` or be weak referenceable. "
"Please either add `__weakref__` to `__slots__` or use"
"qthrottled/qdebounced as a function (not a decorator)."
) from e
raise TypeError(REF_ERROR) from e
return throttler
def __get__(self, instance, owner):
@@ -292,7 +325,7 @@ class ThrottledCallable(GenericSignalThrottler, Generic[P, R]):
if parent is None and isinstance(instance, QObject):
parent = instance
return self._get_throttler(instance, owner, parent, instance)
return self._get_throttler(instance, owner, parent, instance, self._name)
@overload
@@ -438,6 +471,11 @@ def _make_decorator(
obj = ThrottledCallable(func, kind, policy, parent=parent)
obj.setTimerType(timer_type)
obj.setTimeout(timeout)
if instance is not None:
# this is a bound method, we need to avoid strong references,
# and functools.wraps will prevent garbage collection on bound methods
return obj
return wraps(func)(obj)
return deco(func) if func is not None else deco

View File

@@ -69,3 +69,14 @@ def test_wrap_text():
assert isinstance(wrap, list)
assert all(isinstance(x, str) for x in wrap)
assert 9 <= len(wrap) <= 13
def test_minimum_size_hint():
# The hint should always just be the space needed for "..."
wdg = QElidingLabel()
size_hint = wdg.minimumSizeHint()
# Regardless of what text is contained
wdg.setText(TEXT)
new_hint = wdg.minimumSizeHint()
assert size_hint.width() == new_hint.width()
assert size_hint.height() == new_hint.height()

View File

@@ -162,7 +162,7 @@ def test_names(qapp):
signature = inspect.signature(ob.check_object_thread_return_future)
assert len(signature.parameters) == 1
assert next(iter(signature.parameters.values())).name == "a"
assert next(iter(signature.parameters.values())).annotation == int
assert next(iter(signature.parameters.values())).annotation is int
assert ob.check_main_thread_return.__name__ == "check_main_thread_return"

View File

@@ -1,5 +1,3 @@
from typing import List, Tuple
import pytest
from pytestqt.qtbot import QtBot
from qtpy.QtCore import Qt
@@ -30,15 +28,15 @@ def widget(qtbot: QtBot, data: dict) -> QSearchableTreeWidget:
return widget
def columns(item: QTreeWidgetItem) -> Tuple[str, str]:
def columns(item: QTreeWidgetItem) -> tuple[str, str]:
return item.text(0), item.text(1)
def all_items(tree: QTreeWidget) -> List[QTreeWidgetItem]:
def all_items(tree: QTreeWidget) -> list[QTreeWidgetItem]:
return tree.findItems("", Qt.MatchContains | Qt.MatchRecursive)
def shown_items(tree: QTreeWidget) -> List[QTreeWidgetItem]:
def shown_items(tree: QTreeWidget) -> list[QTreeWidgetItem]:
items = all_items(tree)
return [item for item in items if not item.isHidden()]

View File

@@ -1,3 +1,5 @@
import gc
import weakref
from unittest.mock import Mock
import pytest
@@ -116,7 +118,6 @@ def test_debouncer_method_definition(qtbot):
A.call2(32)
qtbot.wait(5)
assert a.count == 1
mock1.assert_called_once()
mock2.assert_called_once()
@@ -124,7 +125,7 @@ def test_debouncer_method_definition(qtbot):
def test_class_with_slots(qtbot):
class A:
__slots__ = ("count", "__weakref__")
__slots__ = ("__weakref__", "count")
def __init__(self):
self.count = 0
@@ -201,3 +202,36 @@ def test_ensure_throttled_sig_inspection(deco, qtbot):
mock.assert_called_once_with(1, 2)
assert func.__doc__ == "docstring"
assert func.__name__ == "func"
def test_qthrottled_does_not_prevent_gc(qtbot):
mock = Mock()
class Thing:
@qdebounced(timeout=1)
def dmethod(self) -> None:
mock()
@qthrottled(timeout=1)
def tmethod(self, x: int = 1) -> None:
mock()
thing = Thing()
thing_ref = weakref.ref(thing)
assert thing_ref() is not None
thing.dmethod()
qtbot.waitUntil(thing.dmethod._future.done, timeout=2000)
assert mock.call_count == 1
thing.tmethod()
qtbot.waitUntil(thing.tmethod._future.done, timeout=2000)
assert mock.call_count == 2
wm = thing.tmethod
assert isinstance(wm, ThrottledCallable)
del thing
gc.collect()
assert thing_ref() is None
with pytest.warns(RuntimeWarning, match="Method has been garbage collected"):
wm()
wm._set_future_result()

View File

@@ -1,4 +1,5 @@
from typing import Any, Iterable
from collections.abc import Iterable
from typing import Any
from unittest.mock import Mock
import pytest

View File

@@ -1,6 +1,7 @@
import math
from collections.abc import Iterable
from itertools import product
from typing import Any, Iterable
from typing import Any
from unittest.mock import Mock
import pytest