mirror of
https://github.com/pyapp-kit/superqt.git
synced 2025-08-13 14:01:31 +02:00
Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
16b383e783 | ||
|
38d15d1b3b | ||
|
8f09c38074 | ||
|
3c8b5bcf98 | ||
|
3ece7a27b1 | ||
|
e0bb2ea871 | ||
|
78997fe155 | ||
|
021f164419 | ||
|
7f50e69e28 | ||
|
2c747c5a4f | ||
|
b79c8e95b7 | ||
|
b393c6d039 | ||
|
61b8ab30ab | ||
|
abf544cf0e |
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
commit-message:
|
||||||
|
prefix: "ci(dependabot):"
|
113
.github/workflows/test_and_deploy.yml
vendored
113
.github/workflows/test_and_deploy.yml
vendored
@@ -3,13 +3,11 @@ name: Test
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
|
||||||
- main
|
- main
|
||||||
tags:
|
tags:
|
||||||
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
|
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
|
||||||
- main
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -17,60 +15,54 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: ${{ matrix.platform }} py${{ matrix.python-version }} ${{ matrix.backend }}
|
name: ${{ matrix.platform }} py${{ matrix.python-version }} ${{ matrix.backend }}
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
timeout-minutes: 10
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform: [ubuntu-latest, windows-latest, macos-latest]
|
platform: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
python-version: [3.7, 3.8, 3.9]
|
python-version: ["3.8", "3.9", "3.10"]
|
||||||
backend: [pyqt5, pyside2]
|
backend: [pyqt5, pyside2]
|
||||||
include:
|
include:
|
||||||
# pyqt6 and pyside6 on latest platforms
|
# pyqt6 and pyside6 on latest platforms
|
||||||
- python-version: 3.9
|
- python-version: "3.10"
|
||||||
|
platform: ubuntu-latest
|
||||||
|
backend: pyqt6
|
||||||
|
- python-version: "3.10"
|
||||||
|
platform: windows-latest
|
||||||
|
backend: pyqt6
|
||||||
|
- python-version: "3.10"
|
||||||
|
platform: macos-latest
|
||||||
|
backend: pyqt6
|
||||||
|
# also take screenshots
|
||||||
|
- python-version: "3.10"
|
||||||
platform: ubuntu-latest
|
platform: ubuntu-latest
|
||||||
backend: pyside6
|
backend: pyside6
|
||||||
screenshot: 1
|
screenshot: 1
|
||||||
- python-version: 3.9
|
- python-version: "3.10"
|
||||||
platform: windows-latest
|
platform: windows-latest
|
||||||
backend: pyside6
|
backend: pyside6
|
||||||
screenshot: 1
|
screenshot: 1
|
||||||
- python-version: 3.9
|
- python-version: "3.10"
|
||||||
platform: macos-11.0
|
platform: macos-latest
|
||||||
backend: pyside6
|
backend: pyside6
|
||||||
screenshot: 1
|
screenshot: 1
|
||||||
- python-version: 3.9
|
|
||||||
|
- python-version: "3.11"
|
||||||
platform: ubuntu-latest
|
platform: ubuntu-latest
|
||||||
backend: pyqt6
|
backend: pyqt6
|
||||||
- python-version: 3.9
|
- python-version: "3.11"
|
||||||
platform: windows-latest
|
platform: windows-latest
|
||||||
backend: pyqt6
|
backend: pyqt5
|
||||||
- python-version: 3.9
|
- python-version: "3.11"
|
||||||
platform: macos-11.0
|
platform: macos-latest
|
||||||
backend: pyqt6
|
|
||||||
# py3.10
|
|
||||||
- python-version: "3.10"
|
|
||||||
platform: ubuntu-latest
|
|
||||||
backend: pyside6
|
backend: pyside6
|
||||||
- python-version: "3.10"
|
|
||||||
platform: ubuntu-latest
|
# python 3.7
|
||||||
|
- python-version: 3.7
|
||||||
|
platform: macos-latest
|
||||||
backend: pyqt5
|
backend: pyqt5
|
||||||
- python-version: "3.10"
|
- python-version: 3.7
|
||||||
platform: ubuntu-latest
|
platform: windows-latest
|
||||||
backend: pyqt6
|
|
||||||
|
|
||||||
# big sur, 3.9
|
|
||||||
- python-version: 3.9
|
|
||||||
platform: macos-11.0
|
|
||||||
backend: pyside2
|
backend: pyside2
|
||||||
- python-version: 3.9
|
|
||||||
platform: macos-11.0
|
|
||||||
backend: pyqt5
|
|
||||||
|
|
||||||
# legacy OS
|
|
||||||
- python-version: 3.8
|
|
||||||
platform: ubuntu-18.04
|
|
||||||
backend: pyside2
|
|
||||||
|
|
||||||
# legacy Qt
|
# legacy Qt
|
||||||
- python-version: 3.7
|
- python-version: 3.7
|
||||||
platform: ubuntu-latest
|
platform: ubuntu-latest
|
||||||
@@ -83,14 +75,19 @@ jobs:
|
|||||||
backend: pyqt514
|
backend: pyqt514
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Cancel Previous Runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.11.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- uses: tlambert03/setup-qt-libs@v1
|
- uses: tlambert03/setup-qt-libs@v1.4
|
||||||
|
|
||||||
- name: Linux opengl
|
- name: Linux opengl
|
||||||
if: runner.os == 'Linux' && ( matrix.backend == 'pyside6' || matrix.backend == 'pyqt6' )
|
if: runner.os == 'Linux' && ( matrix.backend == 'pyside6' || matrix.backend == 'pyqt6' )
|
||||||
@@ -111,11 +108,11 @@ jobs:
|
|||||||
BACKEND: ${{ matrix.backend }}
|
BACKEND: ${{ matrix.backend }}
|
||||||
|
|
||||||
- name: Coverage
|
- name: Coverage
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v3
|
||||||
|
|
||||||
- name: Install for screenshots
|
- name: Install for screenshots
|
||||||
if: matrix.screenshot
|
if: matrix.screenshot
|
||||||
run: pip install . ${{ matrix.backend }}
|
run: pip install -e .[${{ matrix.backend }}]
|
||||||
|
|
||||||
- name: Screenshots (Linux)
|
- name: Screenshots (Linux)
|
||||||
if: runner.os == 'Linux' && matrix.screenshot
|
if: runner.os == 'Linux' && matrix.screenshot
|
||||||
@@ -127,7 +124,7 @@ jobs:
|
|||||||
if: runner.os != 'Linux' && matrix.screenshot
|
if: runner.os != 'Linux' && matrix.screenshot
|
||||||
run: python examples/demo_widget.py -snap
|
run: python examples/demo_widget.py -snap
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v3
|
||||||
if: matrix.screenshot
|
if: matrix.screenshot
|
||||||
with:
|
with:
|
||||||
name: screenshots ${{ runner.os }}
|
name: screenshots ${{ runner.os }}
|
||||||
@@ -137,24 +134,23 @@ jobs:
|
|||||||
name: qtpy minreq
|
name: qtpy minreq
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: tlambert03/setup-qt-libs@v1
|
- uses: tlambert03/setup-qt-libs@v1.4
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.8'
|
python-version: "3.8"
|
||||||
|
|
||||||
- name: install
|
- name: install
|
||||||
run: |
|
run: |
|
||||||
python -m pip install -U pip
|
python -m pip install -U pip
|
||||||
python -m pip install -e .[testing,pyqt5]
|
python -m pip install -e .[test,pyqt5]
|
||||||
python -m pip install qtpy==1.1.0 typing-extensions==3.10.0.0
|
python -m pip install qtpy==1.1.0 typing-extensions==3.10.0.0
|
||||||
|
|
||||||
- name: Test napari magicgui
|
- name: Test
|
||||||
uses: GabrielBB/xvfb-action@v1
|
uses: GabrielBB/xvfb-action@v1
|
||||||
with:
|
with:
|
||||||
run: python -m pytest --color=yes
|
run: python -m pytest --color=yes
|
||||||
|
|
||||||
|
|
||||||
test_napari:
|
test_napari:
|
||||||
name: napari tests
|
name: napari tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -173,7 +169,7 @@ jobs:
|
|||||||
- uses: tlambert03/setup-qt-libs@v1
|
- uses: tlambert03/setup-qt-libs@v1
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: "3.10"
|
||||||
|
|
||||||
- name: install
|
- name: install
|
||||||
run: |
|
run: |
|
||||||
@@ -187,30 +183,27 @@ jobs:
|
|||||||
working-directory: napari-repo
|
working-directory: napari-repo
|
||||||
run: python -m pytest --color=yes napari/_qt
|
run: python -m pytest --color=yes napari/_qt
|
||||||
|
|
||||||
check_manifest:
|
check-manifest:
|
||||||
|
name: Check Manifest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- name: Check manifest
|
- run: pip install check-manifest && check-manifest
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install check-manifest
|
|
||||||
check-manifest
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
# this will run when you have tagged a commit, starting with "v*"
|
# this will run when you have tagged a commit, starting with "v*"
|
||||||
# and requires that you have put your twine API key in your
|
# and requires that you have put your twine API key in your
|
||||||
# github secrets (see readme for details)
|
# github secrets (see readme for details)
|
||||||
needs: [test, check_manifest]
|
needs: [test, check-manifest]
|
||||||
if: ${{ github.repository == 'napari/superqt' && contains(github.ref, 'tags') }}
|
if: ${{ github.repository == 'napari/superqt' && contains(github.ref, 'tags') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
@@ -2,40 +2,58 @@ repos:
|
|||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.3.0
|
rev: v4.3.0
|
||||||
hooks:
|
hooks:
|
||||||
|
- id: check-docstring-first
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
|
||||||
rev: v2.0.0
|
|
||||||
hooks:
|
|
||||||
- id: setup-cfg-fmt
|
|
||||||
args: ["--include-version-classifiers"]
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
|
||||||
rev: 5.0.4
|
|
||||||
hooks:
|
|
||||||
- id: flake8
|
|
||||||
additional_dependencies: [flake8-typing-imports==1.7.0]
|
|
||||||
exclude: examples
|
|
||||||
- repo: https://github.com/PyCQA/autoflake
|
- repo: https://github.com/PyCQA/autoflake
|
||||||
rev: v1.6.1
|
rev: v1.7.7
|
||||||
hooks:
|
hooks:
|
||||||
- id: autoflake
|
- id: autoflake
|
||||||
args: ["--in-place", "--remove-all-unused-imports"]
|
args: ["--in-place", "--remove-all-unused-imports"]
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/isort
|
- repo: https://github.com/PyCQA/isort
|
||||||
rev: 5.10.1
|
rev: 5.10.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
|
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.38.2
|
rev: v2.34.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py37-plus, --keep-runtime-typing]
|
args: [--py37-plus, --keep-runtime-typing]
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.8.0
|
rev: 22.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
|
|
||||||
|
- repo: https://github.com/PyCQA/flake8
|
||||||
|
rev: 4.0.1
|
||||||
|
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
|
||||||
|
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v3.2.0
|
||||||
|
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/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v0.981
|
rev: v0.982
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: examples
|
exclude: tests|examples
|
||||||
stages: [manual]
|
stages:
|
||||||
|
- manual
|
||||||
|
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,8 +1,51 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [v0.3.6](https://github.com/napari/superqt/tree/v0.3.6) (2022-10-03)
|
## [0.4.0](https://github.com/napari/superqt/tree/0.4.0) (2022-11-09)
|
||||||
|
|
||||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.5...v0.3.6)
|
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.8...0.4.0)
|
||||||
|
|
||||||
|
**Fixed bugs:**
|
||||||
|
|
||||||
|
- fix: fix quantity set value and add test [\#131](https://github.com/napari/superqt/pull/131) ([tlambert03](https://github.com/tlambert03))
|
||||||
|
|
||||||
|
**Refactors:**
|
||||||
|
|
||||||
|
- refactor: update pyproject and ci, add py3.11 test [\#132](https://github.com/napari/superqt/pull/132) ([tlambert03](https://github.com/tlambert03))
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- ci\(dependabot\): bump actions/upload-artifact from 2 to 3 [\#135](https://github.com/napari/superqt/pull/135) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||||
|
- ci\(dependabot\): bump codecov/codecov-action from 2 to 3 [\#134](https://github.com/napari/superqt/pull/134) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||||
|
- build: unpin pyside6 [\#133](https://github.com/napari/superqt/pull/133) ([tlambert03](https://github.com/tlambert03))
|
||||||
|
|
||||||
|
## [v0.3.8](https://github.com/napari/superqt/tree/v0.3.8) (2022-10-10)
|
||||||
|
|
||||||
|
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.7...v0.3.8)
|
||||||
|
|
||||||
|
**Fixed bugs:**
|
||||||
|
|
||||||
|
- fix: allow submodule imports [\#128](https://github.com/napari/superqt/pull/128) ([kne42](https://github.com/kne42))
|
||||||
|
|
||||||
|
## [v0.3.7](https://github.com/napari/superqt/tree/v0.3.7) (2022-10-10)
|
||||||
|
|
||||||
|
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.6...v0.3.7)
|
||||||
|
|
||||||
|
**Implemented enhancements:**
|
||||||
|
|
||||||
|
- feat: add Quantity widget \(using pint\) [\#126](https://github.com/napari/superqt/pull/126) ([tlambert03](https://github.com/tlambert03))
|
||||||
|
|
||||||
|
## [v0.3.6](https://github.com/napari/superqt/tree/v0.3.6) (2022-10-05)
|
||||||
|
|
||||||
|
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.6rc0...v0.3.6)
|
||||||
|
|
||||||
|
**Documentation updates:**
|
||||||
|
|
||||||
|
- minor fix to readme [\#125](https://github.com/napari/superqt/pull/125) ([tlambert03](https://github.com/tlambert03))
|
||||||
|
- Docs [\#124](https://github.com/napari/superqt/pull/124) ([tlambert03](https://github.com/tlambert03))
|
||||||
|
|
||||||
|
## [v0.3.6rc0](https://github.com/napari/superqt/tree/v0.3.6rc0) (2022-10-03)
|
||||||
|
|
||||||
|
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.5...v0.3.6rc0)
|
||||||
|
|
||||||
**Implemented enhancements:**
|
**Implemented enhancements:**
|
||||||
|
|
||||||
|
17
MANIFEST.in
17
MANIFEST.in
@@ -1,17 +0,0 @@
|
|||||||
include LICENSE
|
|
||||||
include README.md
|
|
||||||
include CHANGELOG.md
|
|
||||||
include src/superqt/py.typed
|
|
||||||
recursive-include src/superqt *.py
|
|
||||||
recursive-include src/superqt *.pyi
|
|
||||||
|
|
||||||
recursive-exclude * __pycache__
|
|
||||||
recursive-exclude * *.py[co]
|
|
||||||
recursive-exclude docs *
|
|
||||||
recursive-exclude examples *
|
|
||||||
recursive-exclude tests *
|
|
||||||
exclude tox.ini
|
|
||||||
exclude CONTRIBUTING.md
|
|
||||||
exclude codecov.yml
|
|
||||||
exclude .github_changelog_generator
|
|
||||||
exclude .pre-commit-config.yaml
|
|
@@ -39,7 +39,10 @@ def define_env(env: "MacrosPlugin"):
|
|||||||
|
|
||||||
exec(src)
|
exec(src)
|
||||||
_grab(dest, width)
|
_grab(dest, width)
|
||||||
return f"{{ loading=lazy; width={width} }}\n\n"
|
return (
|
||||||
|
f""
|
||||||
|
f"{{ loading=lazy; width={width} }}\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
@env.macro
|
@env.macro
|
||||||
def show_members(cls: str):
|
def show_members(cls: str):
|
||||||
|
@@ -14,6 +14,7 @@ The following are QWidget subclasses:
|
|||||||
| [`QLabeledSlider`](./qlabeledslider.md) | `QSlider` with editable `QSpinBox` that shows the current value |
|
| [`QLabeledSlider`](./qlabeledslider.md) | `QSlider` with editable `QSpinBox` that shows the current value |
|
||||||
| [`QLargeIntSpinBox`](./qlargeintspinbox.md) | `QSpinbox` that accepts arbitrarily large integers |
|
| [`QLargeIntSpinBox`](./qlargeintspinbox.md) | `QSpinbox` that accepts arbitrarily large integers |
|
||||||
| [`QRangeSlider`](./qrangeslider.md) | Multi-handle slider |
|
| [`QRangeSlider`](./qrangeslider.md) | Multi-handle slider |
|
||||||
|
| [`QQuantity`](./qquantity.md) | Pint-backed quantity widget (magnitude combined with unit dropdown) |
|
||||||
|
|
||||||
## Labels and categorical inputs
|
## Labels and categorical inputs
|
||||||
|
|
||||||
|
33
docs/widgets/qquantity.md
Normal file
33
docs/widgets/qquantity.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# QQuantity
|
||||||
|
|
||||||
|
A widget that allows the user to edit a quantity (a magnitude associated with a unit).
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
This widget requires [`pint`](https://pint.readthedocs.io):
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install pint
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install superqt[quantity]
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from superqt import QQuantity
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
w = QQuantity("1m")
|
||||||
|
w.show()
|
||||||
|
|
||||||
|
app.exec()
|
||||||
|
```
|
||||||
|
|
||||||
|
{{ show_widget(150) }}
|
||||||
|
|
||||||
|
{{ show_members('superqt.QQuantity') }}
|
9
examples/quantity.py
Normal file
9
examples/quantity.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from superqt import QQuantity
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
w = QQuantity("1m")
|
||||||
|
w.show()
|
||||||
|
|
||||||
|
app.exec()
|
171
pyproject.toml
171
pyproject.toml
@@ -1,10 +1,175 @@
|
|||||||
# pyproject.toml
|
# https://peps.python.org/pep-0517/
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.0"]
|
requires = ["setuptools>=45", "wheel", "setuptools-scm[toml]>=6.2"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
# https://peps.python.org/pep-0621/
|
||||||
|
[project]
|
||||||
|
name = "superqt"
|
||||||
|
description = "Missing widgets and components for PyQt/PySide"
|
||||||
|
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"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: X11 Applications :: Qt",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: BSD License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Topic :: Desktop Environment",
|
||||||
|
"Topic :: Software Development :: User Interfaces",
|
||||||
|
"Topic :: Software Development :: Widget Sets",
|
||||||
|
]
|
||||||
|
dynamic = ["version"]
|
||||||
|
dependencies = [
|
||||||
|
"packaging",
|
||||||
|
"pygments>=2.4.0",
|
||||||
|
"qtpy>=1.1.0",
|
||||||
|
"typing-extensions",
|
||||||
|
]
|
||||||
|
|
||||||
|
# extras
|
||||||
|
# https://peps.python.org/pep-0621/#dependencies-optional-dependencies
|
||||||
|
[project.optional-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",
|
||||||
|
"mypy",
|
||||||
|
"pdbpp",
|
||||||
|
"pre-commit",
|
||||||
|
"pydocstyle",
|
||||||
|
"pyside2",
|
||||||
|
"pytest-cov",
|
||||||
|
"pytest-qt",
|
||||||
|
"pytest",
|
||||||
|
"rich",
|
||||||
|
"tox-conda",
|
||||||
|
"tox",
|
||||||
|
]
|
||||||
|
docs = ["mkdocs-macros-plugin", "mkdocs-material", "mkdocstrings[python]"]
|
||||||
|
quantity = ["pint"]
|
||||||
|
pyside2 = ["pyside2"]
|
||||||
|
pyside6 = ["pyside6"]
|
||||||
|
pyqt5 = ["pyqt5"]
|
||||||
|
pyqt6 = ["pyqt6"]
|
||||||
|
font-fa5 = ["fonticon-fontawesome5"]
|
||||||
|
font-fa6 = ["fonticon-fontawesome6"]
|
||||||
|
font-mi5 = ["fonticon-materialdesignicons5"]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Source = "https://github.com/napari/superqt"
|
||||||
|
Tracker = "https://github.com/napari/superqt/issues"
|
||||||
|
Changelog = "https://github.com/napari/superqt/blob/main/CHANGELOG.md"
|
||||||
|
|
||||||
|
|
||||||
|
# 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]
|
[tool.setuptools_scm]
|
||||||
write_to = "src/superqt/_version.py"
|
write_to = "src/superqt/_version.py"
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# https://docs.pytest.org/en/6.2.x/customize.html
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
minversion = "6.0"
|
||||||
|
testpaths = ["tests"]
|
||||||
|
filterwarnings = [
|
||||||
|
"error",
|
||||||
|
"ignore:QPixmapCache.find:DeprecationWarning:",
|
||||||
|
"ignore:SelectableGroups dict interface:DeprecationWarning",
|
||||||
|
"ignore:The distutils package is deprecated:DeprecationWarning",
|
||||||
|
]
|
||||||
|
|
||||||
|
# https://mypy.readthedocs.io/en/stable/config_file.html
|
||||||
|
[tool.mypy]
|
||||||
|
files = "src/**/"
|
||||||
|
strict = true
|
||||||
|
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
|
||||||
|
warn_unused_ignores = false
|
||||||
|
allow_redefinition = true
|
||||||
|
|
||||||
|
# https://coverage.readthedocs.io/en/6.4/config.html
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"if TYPE_CHECKING:",
|
||||||
|
"@overload",
|
||||||
|
"except ImportError",
|
||||||
|
]
|
||||||
|
|
||||||
|
# https://github.com/mgedmin/check-manifest#configuration
|
||||||
[tool.check-manifest]
|
[tool.check-manifest]
|
||||||
ignore = ["src/superqt/_version.py", "mkdocs.yml"]
|
ignore = [
|
||||||
|
".github_changelog_generator",
|
||||||
|
".pre-commit-config.yaml",
|
||||||
|
"tests/**/*",
|
||||||
|
"tox.ini",
|
||||||
|
"src/superqt/_version.py",
|
||||||
|
"mkdocs.yml",
|
||||||
|
"docs/**/*",
|
||||||
|
"examples/**/*",
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"CONTRIBUTING.md",
|
||||||
|
"codecov.yml",
|
||||||
|
]
|
||||||
|
119
setup.cfg
119
setup.cfg
@@ -1,119 +0,0 @@
|
|||||||
[metadata]
|
|
||||||
name = superqt
|
|
||||||
description = Missing widgets for PyQt/PySide
|
|
||||||
long_description = file: README.md
|
|
||||||
long_description_content_type = text/markdown
|
|
||||||
url = https://github.com/napari/superqt
|
|
||||||
author = Talley Lambert
|
|
||||||
author_email = talley.lambert@gmail.com
|
|
||||||
license = BSD-3-Clause
|
|
||||||
license_file = LICENSE
|
|
||||||
classifiers =
|
|
||||||
Development Status :: 4 - Beta
|
|
||||||
Environment :: X11 Applications :: Qt
|
|
||||||
Intended Audience :: Developers
|
|
||||||
License :: OSI Approved :: BSD License
|
|
||||||
Operating System :: OS Independent
|
|
||||||
Programming Language :: Python
|
|
||||||
Programming Language :: Python :: 3
|
|
||||||
Programming Language :: Python :: 3 :: Only
|
|
||||||
Programming Language :: Python :: 3.7
|
|
||||||
Programming Language :: Python :: 3.8
|
|
||||||
Programming Language :: Python :: 3.9
|
|
||||||
Programming Language :: Python :: 3.10
|
|
||||||
Programming Language :: Python :: Implementation :: CPython
|
|
||||||
Topic :: Desktop Environment
|
|
||||||
Topic :: Software Development
|
|
||||||
Topic :: Software Development :: User Interfaces
|
|
||||||
Topic :: Software Development :: Widget Sets
|
|
||||||
keywords = qt, range slider, widget
|
|
||||||
project_urls =
|
|
||||||
Source = https://github.com/napari/superqt
|
|
||||||
Tracker = https://github.com/napari/superqt/issues
|
|
||||||
Changelog = https://github.com/napari/superqt/blob/master/CHANGELOG.md
|
|
||||||
|
|
||||||
[options]
|
|
||||||
packages = find:
|
|
||||||
install_requires =
|
|
||||||
packaging
|
|
||||||
pygments>=2.4.0
|
|
||||||
qtpy>=1.1.0
|
|
||||||
typing-extensions
|
|
||||||
python_requires = >=3.7
|
|
||||||
include_package_data = True
|
|
||||||
package_dir =
|
|
||||||
=src
|
|
||||||
setup_requires =
|
|
||||||
setuptools-scm
|
|
||||||
zip_safe = False
|
|
||||||
|
|
||||||
[options.packages.find]
|
|
||||||
where = src
|
|
||||||
|
|
||||||
[options.extras_require]
|
|
||||||
dev =
|
|
||||||
ipython
|
|
||||||
isort
|
|
||||||
jedi<0.18.0
|
|
||||||
mypy
|
|
||||||
pre-commit
|
|
||||||
pyside2
|
|
||||||
pytest
|
|
||||||
pytest-cov
|
|
||||||
pytest-qt
|
|
||||||
tox
|
|
||||||
tox-conda
|
|
||||||
docs =
|
|
||||||
mkdocs-macros-plugin
|
|
||||||
mkdocs-material
|
|
||||||
mkdocstrings[python]
|
|
||||||
font_fa5 =
|
|
||||||
fonticon-fontawesome5
|
|
||||||
font_mi5 =
|
|
||||||
fonticon-materialdesignicons5
|
|
||||||
pyqt5 =
|
|
||||||
pyqt5
|
|
||||||
pyqt6 =
|
|
||||||
pyqt6
|
|
||||||
pyside2 =
|
|
||||||
pyside2
|
|
||||||
pyside6 =
|
|
||||||
pyside6
|
|
||||||
testing =
|
|
||||||
pytest
|
|
||||||
pytest-cov
|
|
||||||
pytest-qt
|
|
||||||
tox
|
|
||||||
tox-conda
|
|
||||||
|
|
||||||
[options.package_data]
|
|
||||||
superqt = py.typed
|
|
||||||
|
|
||||||
[flake8]
|
|
||||||
exclude = _version.py,.eggs,examples
|
|
||||||
docstring-convention = numpy
|
|
||||||
ignore = E203,W503,E501,C901,F403,F405,D100
|
|
||||||
|
|
||||||
[pydocstyle]
|
|
||||||
convention = numpy
|
|
||||||
add_select = D402,D415,D417
|
|
||||||
ignore = D100
|
|
||||||
|
|
||||||
[isort]
|
|
||||||
profile = black
|
|
||||||
|
|
||||||
[tool:pytest]
|
|
||||||
filterwarnings =
|
|
||||||
error
|
|
||||||
ignore:QPixmapCache.find:DeprecationWarning:
|
|
||||||
ignore:SelectableGroups dict interface:DeprecationWarning
|
|
||||||
ignore:The distutils package is deprecated:DeprecationWarning
|
|
||||||
|
|
||||||
[mypy]
|
|
||||||
strict = True
|
|
||||||
files = src/superqt
|
|
||||||
|
|
||||||
[mypy-superqt.qtcompat.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
warn_unused_ignores = False
|
|
||||||
allow_redefinition = True
|
|
@@ -1,9 +1,13 @@
|
|||||||
"""superqt is a collection of QtWidgets for python."""
|
"""superqt is a collection of Qt components for python."""
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ._version import version as __version__
|
from ._version import version as __version__
|
||||||
except ImportError:
|
except ImportError:
|
||||||
__version__ = "unknown"
|
__version__ = "unknown"
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .spinbox._quantity import QQuantity
|
||||||
|
|
||||||
from ._eliding_label import QElidingLabel
|
from ._eliding_label import QElidingLabel
|
||||||
from .collapsible import QCollapsible
|
from .collapsible import QCollapsible
|
||||||
@@ -25,6 +29,7 @@ __all__ = [
|
|||||||
"ensure_main_thread",
|
"ensure_main_thread",
|
||||||
"ensure_object_thread",
|
"ensure_object_thread",
|
||||||
"QDoubleRangeSlider",
|
"QDoubleRangeSlider",
|
||||||
|
"QCollapsible",
|
||||||
"QDoubleSlider",
|
"QDoubleSlider",
|
||||||
"QElidingLabel",
|
"QElidingLabel",
|
||||||
"QEnumComboBox",
|
"QEnumComboBox",
|
||||||
@@ -34,8 +39,16 @@ __all__ = [
|
|||||||
"QLabeledSlider",
|
"QLabeledSlider",
|
||||||
"QLargeIntSpinBox",
|
"QLargeIntSpinBox",
|
||||||
"QMessageHandler",
|
"QMessageHandler",
|
||||||
|
"QQuantity",
|
||||||
|
"QRangeSlider",
|
||||||
"QSearchableComboBox",
|
"QSearchableComboBox",
|
||||||
"QSearchableListWidget",
|
"QSearchableListWidget",
|
||||||
"QRangeSlider",
|
|
||||||
"QCollapsible",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name):
|
||||||
|
if name == "QQuantity":
|
||||||
|
from .spinbox._quantity import QQuantity
|
||||||
|
|
||||||
|
return QQuantity
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
@@ -8,7 +8,7 @@ from qtpy.QtWidgets import QFrame, QPushButton, QVBoxLayout, QWidget
|
|||||||
class QCollapsible(QFrame):
|
class QCollapsible(QFrame):
|
||||||
"""A collapsible widget to hide and unhide child widgets.
|
"""A collapsible widget to hide and unhide child widgets.
|
||||||
|
|
||||||
Based on [https://stackoverflow.com/a/68141638](https://stackoverflow.com/a/68141638)
|
Based on https://stackoverflow.com/a/68141638
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_EXPANDED = "▼ "
|
_EXPANDED = "▼ "
|
||||||
|
@@ -11,7 +11,7 @@ NONE_STRING = "----"
|
|||||||
|
|
||||||
|
|
||||||
def _get_name(enum_value: Enum):
|
def _get_name(enum_value: Enum):
|
||||||
"""Create human readable name if user does not provide own implementation of __str__"""
|
"""Create human readable name if user does not implement __str__"""
|
||||||
if (
|
if (
|
||||||
enum_value.__str__.__module__ != "enum"
|
enum_value.__str__.__module__ != "enum"
|
||||||
and not enum_value.__str__.__module__.startswith("shibokensupport")
|
and not enum_value.__str__.__module__.startswith("shibokensupport")
|
||||||
@@ -91,7 +91,8 @@ class QEnumComboBox(QComboBox):
|
|||||||
return
|
return
|
||||||
if not isinstance(value, self._enum_class):
|
if not isinstance(value, self._enum_class):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"setValue(self, Enum): argument 1 has unexpected type {type(value).__name__!r}"
|
"setValue(self, Enum): argument 1 has unexpected type "
|
||||||
|
f"{type(value).__name__!r}"
|
||||||
)
|
)
|
||||||
self.setCurrentText(_get_name(value))
|
self.setCurrentText(_get_name(value))
|
||||||
|
|
||||||
|
@@ -43,16 +43,17 @@ def icon(
|
|||||||
opacity: float = 1,
|
opacity: float = 1,
|
||||||
animation: Optional[Animation] = None,
|
animation: Optional[Animation] = None,
|
||||||
transform: Optional[QTransform] = None,
|
transform: Optional[QTransform] = None,
|
||||||
states: Dict[str, Union[IconOptionDict, IconOpts]] = {},
|
states: Dict[str, Union[IconOptionDict, IconOpts]] | None = None,
|
||||||
) -> QFontIcon:
|
) -> 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.
|
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:
|
In most cases, the key should be provided by a plugin in the environment, like:
|
||||||
|
|
||||||
|
- [fonticon-fontawesome5](https://pypi.org/project/fonticon-fontawesome5/) ('fa5s' &
|
||||||
- [fonticon-fontawesome5](https://pypi.org/project/fonticon-fontawesome5/) ('fa5s' & 'fa5r' prefixes)
|
'fa5r' prefixes)
|
||||||
- [fonticon-materialdesignicons6](https://pypi.org/project/fonticon-materialdesignicons6/) ('mdi6' prefix)
|
- [fonticon-materialdesignicons6](https://pypi.org/project/fonticon-materialdesignicons6/)
|
||||||
|
('mdi6' prefix)
|
||||||
|
|
||||||
...but fonts can also be added manually using [`addFont`][superqt.fonticon.addFont].
|
...but fonts can also be added manually using [`addFont`][superqt.fonticon.addFont].
|
||||||
|
|
||||||
@@ -137,7 +138,7 @@ def icon(
|
|||||||
>>> btn.setIconSize(QSize(256, 256))
|
>>> btn.setIconSize(QSize(256, 256))
|
||||||
>>> btn.show()
|
>>> btn.show()
|
||||||
|
|
||||||
"""
|
""" # noqa: E501
|
||||||
return _QFIS.instance().icon(
|
return _QFIS.instance().icon(
|
||||||
glyph_key,
|
glyph_key,
|
||||||
scale_factor=scale_factor,
|
scale_factor=scale_factor,
|
||||||
@@ -145,7 +146,7 @@ def icon(
|
|||||||
opacity=opacity,
|
opacity=opacity,
|
||||||
animation=animation,
|
animation=animation,
|
||||||
transform=transform,
|
transform=transform,
|
||||||
states=states,
|
states=states or {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -218,7 +219,7 @@ def addFont(
|
|||||||
Tuple[str, str], optional
|
Tuple[str, str], optional
|
||||||
font-family and font-style for the file just registered, or `None` if
|
font-family and font-style for the file just registered, or `None` if
|
||||||
something goes wrong.
|
something goes wrong.
|
||||||
"""
|
""" # noqa: E501
|
||||||
return _QFIS.instance().addFont(filepath, prefix, charmap)
|
return _QFIS.instance().addFont(filepath, prefix, charmap)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -356,10 +356,9 @@ class QFontIconStore(QObject):
|
|||||||
|
|
||||||
def __init__(self, parent: Optional[QObject] = None) -> None:
|
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
# QT6 drops this
|
if tuple(QT_VERSION.split(".")) < ("6", "0"):
|
||||||
dpi = getattr(Qt.ApplicationAttribute, "AA_UseHighDpiPixmaps", None)
|
# QT6 drops this
|
||||||
if dpi:
|
QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps)
|
||||||
QApplication.setAttribute(dpi)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def instance(cls) -> QFontIconStore:
|
def instance(cls) -> QFontIconStore:
|
||||||
@@ -503,7 +502,7 @@ class QFontIconStore(QObject):
|
|||||||
opacity: float = 1,
|
opacity: float = 1,
|
||||||
animation: Optional[Animation] = None,
|
animation: Optional[Animation] = None,
|
||||||
transform: Optional[QTransform] = None,
|
transform: Optional[QTransform] = None,
|
||||||
states: Dict[str, Union[IconOptionDict, IconOpts]] = {},
|
states: Dict[str, Union[IconOptionDict, IconOpts]] | None = None,
|
||||||
) -> QFontIcon:
|
) -> QFontIcon:
|
||||||
self.key2glyph(glyph_key) # make sure it's a valid glyph_key
|
self.key2glyph(glyph_key) # make sure it's a valid glyph_key
|
||||||
default_opts = _IconOptions(
|
default_opts = _IconOptions(
|
||||||
@@ -515,7 +514,7 @@ class QFontIconStore(QObject):
|
|||||||
transform=transform,
|
transform=transform,
|
||||||
)
|
)
|
||||||
icon = QFontIcon(default_opts)
|
icon = QFontIcon(default_opts)
|
||||||
for kw, options in states.items():
|
for kw, options in (states or {}).items():
|
||||||
if isinstance(options, IconOpts):
|
if isinstance(options, IconOpts):
|
||||||
options = default_opts._update(options).dict()
|
options = default_opts._update(options).dict()
|
||||||
icon.addState(*_norm_state_mode(kw), **options)
|
icon.addState(*_norm_state_mode(kw), **options)
|
||||||
|
@@ -1,12 +0,0 @@
|
|||||||
from qtpy.QtWidgets import QSlider
|
|
||||||
|
|
||||||
from ._generic_range_slider import _GenericRangeSlider
|
|
||||||
from ._generic_slider import _GenericSlider
|
|
||||||
|
|
||||||
class QDoubleRangeSlider(_GenericRangeSlider): ...
|
|
||||||
class QDoubleSlider(_GenericSlider): ...
|
|
||||||
class QRangeSlider(_GenericRangeSlider): ...
|
|
||||||
class QLabeledSlider(QSlider): ...
|
|
||||||
class QLabeledDoubleSlider(QDoubleSlider): ...
|
|
||||||
class QLabeledRangeSlider(QRangeSlider): ...
|
|
||||||
class QLabeledDoubleRangeSlider(QDoubleRangeSlider): ...
|
|
@@ -80,11 +80,11 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
|
|||||||
self._bar_is_rigid = bool(val)
|
self._bar_is_rigid = bool(val)
|
||||||
|
|
||||||
def barMovesAllHandles(self) -> bool:
|
def barMovesAllHandles(self) -> bool:
|
||||||
"""Whether clicking on the bar moves all handles (default), or just the nearest."""
|
"""Whether clicking on the bar moves all handles, or just the nearest."""
|
||||||
return self._bar_moves_all
|
return self._bar_moves_all
|
||||||
|
|
||||||
def setBarMovesAllHandles(self, val: bool = True) -> None:
|
def setBarMovesAllHandles(self, val: bool = True) -> None:
|
||||||
"""Whether clicking on the bar moves all handles (default), or just the nearest."""
|
"""Whether clicking on the bar moves all handles, or just the nearest."""
|
||||||
self._bar_moves_all = bool(val)
|
self._bar_moves_all = bool(val)
|
||||||
|
|
||||||
def barIsVisible(self) -> bool:
|
def barIsVisible(self) -> bool:
|
||||||
|
231
src/superqt/spinbox/_quantity.py
Normal file
231
src/superqt/spinbox/_quantity.py
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pint import Quantity, Unit, UnitRegistry
|
||||||
|
from pint.util import UnitsContainer
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError(
|
||||||
|
"pint is required to use QQuantity. Install it with `pip install pint`"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
from qtpy.QtCore import Signal
|
||||||
|
from qtpy.QtWidgets import QComboBox, QDoubleSpinBox, QHBoxLayout, QSizePolicy, QWidget
|
||||||
|
|
||||||
|
from ..utils import signals_blocked
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
|
Number = Union[int, float, "Decimal"]
|
||||||
|
UREG = UnitRegistry()
|
||||||
|
NULL_OPTION = "-----"
|
||||||
|
QOVERFLOW = 2**30
|
||||||
|
SI_BASES = {
|
||||||
|
"[length]": "meter",
|
||||||
|
"[time]": "second",
|
||||||
|
"[current]": "ampere",
|
||||||
|
"[luminosity]": "candela",
|
||||||
|
"[mass]": "gram",
|
||||||
|
"[substance]": "mole",
|
||||||
|
"[temperature]": "kelvin",
|
||||||
|
}
|
||||||
|
DEFAULT_OPTIONS = {
|
||||||
|
"[length]": ["km", "m", "mm", "µm"],
|
||||||
|
"[time]": ["day", "hour", "min", "sec", "ms"],
|
||||||
|
"[current]": ["A", "mA", "µA"],
|
||||||
|
"[luminosity]": ["kcd", "cd", "mcd"],
|
||||||
|
"[mass]": ["kg", "g", "mg", "µg"],
|
||||||
|
"[substance]": ["mol", "mmol", "µmol"],
|
||||||
|
"[temperature]": ["°C", "°F", "°K"],
|
||||||
|
"radian": ["rad", "deg"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class QQuantity(QWidget):
|
||||||
|
"""A combination QDoubleSpinBox and QComboBox for entering quantities.
|
||||||
|
|
||||||
|
For this widget, `value()` returns a `pint.Quantity` object, while `setValue()`
|
||||||
|
accepts either a number, `pint.Quantity`, a string that can be parsed by `pint`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
value : Union[str, pint.Quantity, Number]
|
||||||
|
The initial value to display. If a string, it will be parsed by `pint`.
|
||||||
|
units : Union[pint.util.UnitsContainer, str, pint.Quantity], optional
|
||||||
|
The units to use if `value` is a number. If a string, it will be parsed by
|
||||||
|
`pint`. If a `pint.Quantity`, the units will be extracted from it.
|
||||||
|
ureg : pint.UnitRegistry, optional
|
||||||
|
The unit registry to use. If not provided, the registry will be extracted
|
||||||
|
from `value` if it is a `pint.Quantity`, otherwise the default registry will
|
||||||
|
be used.
|
||||||
|
parent : QWidget, optional
|
||||||
|
The parent widget, by default None
|
||||||
|
"""
|
||||||
|
|
||||||
|
valueChanged = Signal(Quantity)
|
||||||
|
unitsChanged = Signal(Unit)
|
||||||
|
dimensionalityChanged = Signal(UnitsContainer)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
value: Union[str, Quantity, Number] = 0,
|
||||||
|
units: Union[UnitsContainer, str, Quantity] = None,
|
||||||
|
ureg: Optional[UnitRegistry] = None,
|
||||||
|
parent: Optional[QWidget] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
if ureg is None:
|
||||||
|
ureg = value._REGISTRY if isinstance(value, Quantity) else UREG
|
||||||
|
else:
|
||||||
|
assert isinstance(ureg, UnitRegistry)
|
||||||
|
|
||||||
|
self._ureg = ureg
|
||||||
|
self._value: Quantity = self._ureg.Quantity(value, units=units)
|
||||||
|
|
||||||
|
# whether to preserve quantity equality when changing units or magnitude
|
||||||
|
self._preserve_quantity: bool = False
|
||||||
|
self._abbreviate_units: bool = True # TODO: implement
|
||||||
|
|
||||||
|
self._mag_spinbox = QDoubleSpinBox()
|
||||||
|
self._mag_spinbox.setDecimals(3)
|
||||||
|
self._mag_spinbox.setRange(-QOVERFLOW, QOVERFLOW - 1)
|
||||||
|
self._mag_spinbox.setValue(float(self._value.magnitude))
|
||||||
|
self._mag_spinbox.valueChanged.connect(self.setMagnitude)
|
||||||
|
|
||||||
|
self._units_combo = QComboBox()
|
||||||
|
self._units_combo.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||||
|
self._units_combo.currentTextChanged.connect(self.setUnits)
|
||||||
|
self._update_units_combo_choices()
|
||||||
|
|
||||||
|
self.setLayout(QHBoxLayout())
|
||||||
|
self.layout().addWidget(self._mag_spinbox)
|
||||||
|
self.layout().addWidget(self._units_combo)
|
||||||
|
self.layout().setContentsMargins(6, 0, 0, 0)
|
||||||
|
|
||||||
|
def unitRegistry(self) -> UnitRegistry:
|
||||||
|
"""Return the pint UnitRegistry used by this widget."""
|
||||||
|
return self._ureg
|
||||||
|
|
||||||
|
def _update_units_combo_choices(self):
|
||||||
|
if self._value.dimensionless:
|
||||||
|
with signals_blocked(self._units_combo):
|
||||||
|
self._units_combo.clear()
|
||||||
|
self._units_combo.addItem(NULL_OPTION)
|
||||||
|
self._units_combo.addItems(
|
||||||
|
[self._format_units(x) for x in SI_BASES.values()]
|
||||||
|
)
|
||||||
|
self._units_combo.setCurrentText(NULL_OPTION)
|
||||||
|
return
|
||||||
|
|
||||||
|
units = self._value.units
|
||||||
|
dims, exp = next(iter(units.dimensionality.items()))
|
||||||
|
if exp != 1:
|
||||||
|
raise NotImplementedError("Inverse units not yet implemented")
|
||||||
|
options = [
|
||||||
|
self._format_units(self._ureg.Unit(u))
|
||||||
|
for u in DEFAULT_OPTIONS.get(dims, [])
|
||||||
|
]
|
||||||
|
current = self._format_units(units)
|
||||||
|
with signals_blocked(self._units_combo):
|
||||||
|
self._units_combo.clear()
|
||||||
|
self._units_combo.addItems(options)
|
||||||
|
if self._units_combo.findText(current) == -1:
|
||||||
|
self._units_combo.addItem(current)
|
||||||
|
|
||||||
|
self._units_combo.setCurrentText(current)
|
||||||
|
|
||||||
|
def value(self) -> Quantity:
|
||||||
|
"""Return the current value as a `pint.Quantity`."""
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def text(self) -> str:
|
||||||
|
return str(self._value)
|
||||||
|
|
||||||
|
def magnitude(self) -> Union[float, int]:
|
||||||
|
"""Return the magnitude of the current value."""
|
||||||
|
return self._value.magnitude
|
||||||
|
|
||||||
|
def units(self) -> Unit:
|
||||||
|
"""Return the current units."""
|
||||||
|
return self._value.units
|
||||||
|
|
||||||
|
def dimensionality(self) -> UnitsContainer:
|
||||||
|
"""Return the current dimensionality (cast to `str` for nice repr)."""
|
||||||
|
return self._value.dimensionality
|
||||||
|
|
||||||
|
def setDecimals(self, decimals: int) -> None:
|
||||||
|
"""Set the number of decimals to display in the spinbox."""
|
||||||
|
self._mag_spinbox.setDecimals(decimals)
|
||||||
|
if self._value is not None:
|
||||||
|
self._mag_spinbox.setValue(self._value.magnitude)
|
||||||
|
|
||||||
|
def setValue(
|
||||||
|
self,
|
||||||
|
value: Union[str, Quantity, Number],
|
||||||
|
units: Union[UnitsContainer, str, Quantity] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set the current value (will cast to a pint Quantity)."""
|
||||||
|
if isinstance(value, Quantity):
|
||||||
|
if units is not None:
|
||||||
|
raise ValueError("Cannot specify units if value is a Quantity")
|
||||||
|
new_val = self._ureg.Quantity(value.magnitude, units=value.units)
|
||||||
|
else:
|
||||||
|
new_val = self._ureg.Quantity(value, units=units)
|
||||||
|
|
||||||
|
mag_change = new_val.magnitude != self._value.magnitude
|
||||||
|
units_change = new_val.units != self._value.units
|
||||||
|
dims_changed = new_val.dimensionality != self._value.dimensionality
|
||||||
|
|
||||||
|
self._value = new_val
|
||||||
|
|
||||||
|
if mag_change:
|
||||||
|
with signals_blocked(self._mag_spinbox):
|
||||||
|
self._mag_spinbox.setValue(float(self._value.magnitude))
|
||||||
|
|
||||||
|
if units_change:
|
||||||
|
with signals_blocked(self._units_combo):
|
||||||
|
self._units_combo.setCurrentText(self._format_units(self._value.units))
|
||||||
|
self.unitsChanged.emit(self._value.units)
|
||||||
|
|
||||||
|
if dims_changed:
|
||||||
|
self._update_units_combo_choices()
|
||||||
|
self.dimensionalityChanged.emit(self._value.dimensionality)
|
||||||
|
|
||||||
|
if mag_change or units_change:
|
||||||
|
self.valueChanged.emit(self._value)
|
||||||
|
|
||||||
|
def setMagnitude(self, magnitude: Number) -> None:
|
||||||
|
"""Set the magnitude of the current value."""
|
||||||
|
self.setValue(self._ureg.Quantity(magnitude, self._value.units))
|
||||||
|
|
||||||
|
def setUnits(self, units: Union[str, Unit, Quantity]) -> None:
|
||||||
|
"""Set the units of the current value.
|
||||||
|
|
||||||
|
If `units` is `None`, will convert to a dimensionless quantity.
|
||||||
|
Otherwise, units must be compatible with the current dimensionality.
|
||||||
|
"""
|
||||||
|
if units is None:
|
||||||
|
new_val = self._ureg.Quantity(self._value.magnitude)
|
||||||
|
elif self.isDimensionless():
|
||||||
|
new_val = self._ureg.Quantity(self._value.magnitude, units)
|
||||||
|
else:
|
||||||
|
new_val = self._value.to(units)
|
||||||
|
self.setValue(new_val)
|
||||||
|
|
||||||
|
def isDimensionless(self) -> bool:
|
||||||
|
"""Return `True` if the current value is dimensionless."""
|
||||||
|
return self._value.dimensionless
|
||||||
|
|
||||||
|
def magnitudeSpinBox(self) -> QDoubleSpinBox:
|
||||||
|
"""Return the `QSpinBox` widget used to edit the magnitude."""
|
||||||
|
return self._mag_spinbox
|
||||||
|
|
||||||
|
def unitsComboBox(self) -> QComboBox:
|
||||||
|
"""Return the `QCombBox` widget used to edit the units."""
|
||||||
|
return self._units_combo
|
||||||
|
|
||||||
|
def _format_units(self, u: Union[Unit, str]) -> str:
|
||||||
|
if isinstance(u, str):
|
||||||
|
return u
|
||||||
|
return f"{u:~}" if self._abbreviate_units else f"{u:}"
|
@@ -6,7 +6,8 @@ from pygments.lexers import find_lexer_class, get_lexer_by_name
|
|||||||
from pygments.util import ClassNotFound
|
from pygments.util import ClassNotFound
|
||||||
from qtpy import QtGui
|
from qtpy import QtGui
|
||||||
|
|
||||||
# inspired by https://github.com/Vector35/snippets/blob/master/QCodeEditor.py (MIT license) and
|
# inspired by https://github.com/Vector35/snippets/blob/master/QCodeEditor.py
|
||||||
|
# (MIT license) and
|
||||||
# https://pygments.org/docs/formatterdevelopment/#html-3-2-formatter
|
# https://pygments.org/docs/formatterdevelopment/#html-3-2-formatter
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +19,10 @@ def get_text_char_format(style):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
text_char_format = QtGui.QTextCharFormat()
|
text_char_format = QtGui.QTextCharFormat()
|
||||||
text_char_format.setFontFamily("monospace")
|
if hasattr(text_char_format, "setFontFamilies"):
|
||||||
|
text_char_format.setFontFamilies(["monospace"])
|
||||||
|
else:
|
||||||
|
text_char_format.setFontFamily("monospace")
|
||||||
if style.get("color"):
|
if style.get("color"):
|
||||||
text_char_format.setForeground(QtGui.QColor(f"#{style['color']}"))
|
text_char_format.setForeground(QtGui.QColor(f"#{style['color']}"))
|
||||||
|
|
||||||
@@ -85,7 +89,8 @@ class CodeSyntaxHighlight(QtGui.QSyntaxHighlighter):
|
|||||||
|
|
||||||
# dirty, dirty hack
|
# dirty, dirty hack
|
||||||
# The core problem is that pygemnts by default use string streams,
|
# The core problem is that pygemnts by default use string streams,
|
||||||
# that will not handle QTextCharFormat, so wee need use `data` property to work around this.
|
# that will not handle QTextCharFormat, so wee need use `data` property to
|
||||||
|
# work around this.
|
||||||
for i in range(len(text)):
|
for i in range(len(text)):
|
||||||
try:
|
try:
|
||||||
self.setFormat(i, 1, self.formatter.data[p + i - enters])
|
self.setFormat(i, 1, self.formatter.data[p + i - enters])
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
# https://gist.github.com/FlorianRhiem/41a1ad9b694c14fb9ac3
|
# https://gist.github.com/FlorianRhiem/41a1ad9b694c14fb9ac3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Callable, List, Optional
|
from typing import TYPE_CHECKING, Callable, List, Optional, overload
|
||||||
|
|
||||||
from qtpy.QtCore import (
|
from qtpy.QtCore import (
|
||||||
QCoreApplication,
|
QCoreApplication,
|
||||||
@@ -13,10 +15,18 @@ from qtpy.QtCore import (
|
|||||||
Slot,
|
Slot,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from typing_extensions import Literal, ParamSpec
|
||||||
|
|
||||||
|
P = ParamSpec("P")
|
||||||
|
R = TypeVar("R")
|
||||||
|
|
||||||
|
|
||||||
class CallCallable(QObject):
|
class CallCallable(QObject):
|
||||||
finished = Signal(object)
|
finished = Signal(object)
|
||||||
instances: List["CallCallable"] = []
|
instances: List[CallCallable] = []
|
||||||
|
|
||||||
def __init__(self, callable, *args, **kwargs):
|
def __init__(self, callable, *args, **kwargs):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -32,6 +42,32 @@ class CallCallable(QObject):
|
|||||||
self.finished.emit(res)
|
self.finished.emit(res)
|
||||||
|
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
@overload
|
||||||
|
def ensure_main_thread(
|
||||||
|
await_return: Literal[True],
|
||||||
|
timeout: int = 1000,
|
||||||
|
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
||||||
|
@overload
|
||||||
|
def ensure_main_thread(
|
||||||
|
func: Callable[P, R],
|
||||||
|
await_return: Literal[True],
|
||||||
|
timeout: int = 1000,
|
||||||
|
) -> Callable[P, R]: ...
|
||||||
|
@overload
|
||||||
|
def ensure_main_thread(
|
||||||
|
await_return: Literal[False] = False,
|
||||||
|
timeout: int = 1000,
|
||||||
|
) -> Callable[[Callable[P, R]], Callable[P, Future[R]]]: ...
|
||||||
|
@overload
|
||||||
|
def ensure_main_thread(
|
||||||
|
func: Callable[P, R],
|
||||||
|
await_return: Literal[False] = False,
|
||||||
|
timeout: int = 1000,
|
||||||
|
) -> Callable[P, Future[R]]: ...
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
def ensure_main_thread(
|
def ensure_main_thread(
|
||||||
func: Optional[Callable] = None, await_return: bool = False, timeout: int = 1000
|
func: Optional[Callable] = None, await_return: bool = False, timeout: int = 1000
|
||||||
):
|
):
|
||||||
@@ -65,9 +101,33 @@ def ensure_main_thread(
|
|||||||
|
|
||||||
return _func
|
return _func
|
||||||
|
|
||||||
if func is None:
|
return _out_func if func is None else _out_func(func)
|
||||||
return _out_func
|
|
||||||
return _out_func(func)
|
|
||||||
|
# fmt: off
|
||||||
|
@overload
|
||||||
|
def ensure_object_thread(
|
||||||
|
await_return: Literal[True],
|
||||||
|
timeout: int = 1000,
|
||||||
|
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
||||||
|
@overload
|
||||||
|
def ensure_object_thread(
|
||||||
|
func: Callable[P, R],
|
||||||
|
await_return: Literal[True],
|
||||||
|
timeout: int = 1000,
|
||||||
|
) -> Callable[P, R]: ...
|
||||||
|
@overload
|
||||||
|
def ensure_object_thread(
|
||||||
|
await_return: Literal[False] = False,
|
||||||
|
timeout: int = 1000,
|
||||||
|
) -> Callable[[Callable[P, R]], Callable[P, Future[R]]]: ...
|
||||||
|
@overload
|
||||||
|
def ensure_object_thread(
|
||||||
|
func: Callable[P, R],
|
||||||
|
await_return: Literal[False] = False,
|
||||||
|
timeout: int = 1000,
|
||||||
|
) -> Callable[P, Future[R]]: ...
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
def ensure_object_thread(
|
def ensure_object_thread(
|
||||||
@@ -98,9 +158,7 @@ def ensure_object_thread(
|
|||||||
|
|
||||||
return _func
|
return _func
|
||||||
|
|
||||||
if func is None:
|
return _out_func if func is None else _out_func(func)
|
||||||
return _out_func
|
|
||||||
return _out_func(func)
|
|
||||||
|
|
||||||
|
|
||||||
def _run_in_thread(
|
def _run_in_thread(
|
||||||
@@ -121,5 +179,5 @@ def _run_in_thread(
|
|||||||
f = CallCallable(func, *args, **kwargs)
|
f = CallCallable(func, *args, **kwargs)
|
||||||
f.moveToThread(thread)
|
f.moveToThread(thread)
|
||||||
f.finished.connect(future.set_result, Qt.ConnectionType.DirectConnection)
|
f.finished.connect(future.set_result, Qt.ConnectionType.DirectConnection)
|
||||||
QMetaObject.invokeMethod(f, "call", Qt.ConnectionType.QueuedConnection) # type: ignore
|
QMetaObject.invokeMethod(f, "call", Qt.ConnectionType.QueuedConnection) # type: ignore # noqa
|
||||||
return future.result(timeout=timeout / 1000) if await_return else future
|
return future.result(timeout=timeout / 1000) if await_return else future
|
||||||
|
@@ -1,52 +0,0 @@
|
|||||||
from concurrent.futures import Future
|
|
||||||
from typing import Callable, TypeVar, overload
|
|
||||||
|
|
||||||
from typing_extensions import Literal, ParamSpec
|
|
||||||
|
|
||||||
P = ParamSpec("P")
|
|
||||||
R = TypeVar("R")
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def ensure_main_thread(
|
|
||||||
await_return: Literal[True],
|
|
||||||
timeout: int = 1000,
|
|
||||||
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
||||||
@overload
|
|
||||||
def ensure_main_thread(
|
|
||||||
func: Callable[P, R],
|
|
||||||
await_return: Literal[True],
|
|
||||||
timeout: int = 1000,
|
|
||||||
) -> Callable[P, R]: ...
|
|
||||||
@overload
|
|
||||||
def ensure_main_thread(
|
|
||||||
await_return: Literal[False] = False,
|
|
||||||
timeout: int = 1000,
|
|
||||||
) -> Callable[[Callable[P, R]], Callable[P, Future[R]]]: ...
|
|
||||||
@overload
|
|
||||||
def ensure_main_thread(
|
|
||||||
func: Callable[P, R],
|
|
||||||
await_return: Literal[False] = False,
|
|
||||||
timeout: int = 1000,
|
|
||||||
) -> Callable[P, Future[R]]: ...
|
|
||||||
@overload
|
|
||||||
def ensure_object_thread(
|
|
||||||
await_return: Literal[True],
|
|
||||||
timeout: int = 1000,
|
|
||||||
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
||||||
@overload
|
|
||||||
def ensure_object_thread(
|
|
||||||
func: Callable[P, R],
|
|
||||||
await_return: Literal[True],
|
|
||||||
timeout: int = 1000,
|
|
||||||
) -> Callable[P, R]: ...
|
|
||||||
@overload
|
|
||||||
def ensure_object_thread(
|
|
||||||
await_return: Literal[False] = False,
|
|
||||||
timeout: int = 1000,
|
|
||||||
) -> Callable[[Callable[P, R]], Callable[P, Future[R]]]: ...
|
|
||||||
@overload
|
|
||||||
def ensure_object_thread(
|
|
||||||
func: Callable[P, R],
|
|
||||||
await_return: Literal[False] = False,
|
|
||||||
timeout: int = 1000,
|
|
||||||
) -> Callable[P, Future[R]]: ...
|
|
@@ -207,9 +207,9 @@ class WorkerBase(QRunnable, Generic[_R]):
|
|||||||
|
|
||||||
The end-user should never need to call this function.
|
The end-user should never need to call this function.
|
||||||
But subclasses must implement this method (See
|
But subclasses must implement this method (See
|
||||||
[`GeneratorFunction.work`][superqt.utils._qthreading.GeneratorWorker.work] for an example implementation).
|
[`GeneratorFunction.work`][superqt.utils._qthreading.GeneratorWorker.work] for
|
||||||
Minimally, it should check `self.abort_requested` periodically and
|
an example implementation). Minimally, it should check `self.abort_requested`
|
||||||
exit if True.
|
periodically and exit if True.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
@@ -670,8 +670,10 @@ def thread_worker(
|
|||||||
):
|
):
|
||||||
"""Decorator that runs a function in a separate thread when called.
|
"""Decorator that runs a function in a separate thread when called.
|
||||||
|
|
||||||
When called, the decorated function returns a [`WorkerBase`][superqt.utils.WorkerBase]. See
|
When called, the decorated function returns a
|
||||||
[`create_worker`][superqt.utils.create_worker] for additional keyword arguments that can be used
|
[`WorkerBase`][superqt.utils.WorkerBase]. See
|
||||||
|
[`create_worker`][superqt.utils.create_worker] for additional keyword arguments that
|
||||||
|
can be used
|
||||||
when calling the function.
|
when calling the function.
|
||||||
|
|
||||||
The returned worker will have these signals:
|
The returned worker will have these signals:
|
||||||
@@ -715,8 +717,9 @@ def thread_worker(
|
|||||||
worker class. by default None
|
worker class. by default None
|
||||||
worker_class : Type[WorkerBase]
|
worker_class : Type[WorkerBase]
|
||||||
The [`WorkerBase`][superqt.utils.WorkerBase] to instantiate, by default
|
The [`WorkerBase`][superqt.utils.WorkerBase] to instantiate, by default
|
||||||
[`FunctionWorker`][superqt.utils.FunctionWorker] will be used if `func` is a regular function,
|
[`FunctionWorker`][superqt.utils.FunctionWorker] will be used if `func` is a
|
||||||
and [`GeneratorWorker`][superqt.utils.GeneratorWorker] will be used if it is a generator.
|
regular function, and [`GeneratorWorker`][superqt.utils.GeneratorWorker] will be
|
||||||
|
used if it is a generator.
|
||||||
ignore_errors : bool
|
ignore_errors : bool
|
||||||
If `False` (the default), errors raised in the other thread will be
|
If `False` (the default), errors raised in the other thread will be
|
||||||
reraised in the main thread (makes debugging significantly easier).
|
reraised in the main thread (makes debugging significantly easier).
|
||||||
|
@@ -371,10 +371,10 @@ def _make_decorator(
|
|||||||
throttle.throttle()
|
throttle.throttle()
|
||||||
return future
|
return future
|
||||||
|
|
||||||
setattr(inner, "cancel", throttle.cancel)
|
setattr(inner, "cancel", throttle.cancel) # noqa
|
||||||
setattr(inner, "flush", throttle.flush)
|
setattr(inner, "flush", throttle.flush) # noqa
|
||||||
setattr(inner, "set_timeout", throttle.setTimeout)
|
setattr(inner, "set_timeout", throttle.setTimeout) # noqa
|
||||||
setattr(inner, "triggered", throttle.triggered)
|
setattr(inner, "triggered", throttle.triggered) # noqa
|
||||||
return inner # type: ignore
|
return inner # type: ignore
|
||||||
|
|
||||||
return deco(func) if func is not None else deco
|
return deco(func) if func is not None else deco
|
||||||
|
@@ -28,9 +28,9 @@ def test_message_handler_with_logger(caplog):
|
|||||||
QtCore.qCritical("critical")
|
QtCore.qCritical("critical")
|
||||||
|
|
||||||
assert len(caplog.records) == 3
|
assert len(caplog.records) == 3
|
||||||
caplog.records[0].message == "debug"
|
assert caplog.records[0].message == "debug"
|
||||||
caplog.records[0].levelno == logging.DEBUG
|
assert caplog.records[0].levelno == logging.DEBUG
|
||||||
caplog.records[1].message == "warning"
|
assert caplog.records[1].message == "warning"
|
||||||
caplog.records[1].levelno == logging.WARNING
|
assert caplog.records[1].levelno == logging.WARNING
|
||||||
caplog.records[2].message == "critical"
|
assert caplog.records[2].message == "critical"
|
||||||
caplog.records[2].levelno == logging.CRITICAL
|
assert caplog.records[2].levelno == logging.CRITICAL
|
||||||
|
41
tests/test_quantity.py
Normal file
41
tests/test_quantity.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from pint import Quantity
|
||||||
|
|
||||||
|
from superqt import QQuantity
|
||||||
|
|
||||||
|
|
||||||
|
def test_qquantity(qtbot):
|
||||||
|
w = QQuantity(1, "m")
|
||||||
|
qtbot.addWidget(w)
|
||||||
|
|
||||||
|
assert w.value() == 1 * w.unitRegistry().meter
|
||||||
|
assert w.magnitude() == 1
|
||||||
|
assert w.units() == w.unitRegistry().meter
|
||||||
|
assert w.text() == "1 meter"
|
||||||
|
w.setUnits("cm")
|
||||||
|
assert w.value() == 100 * w.unitRegistry().centimeter
|
||||||
|
assert w.magnitude() == 100
|
||||||
|
assert w.units() == w.unitRegistry().centimeter
|
||||||
|
assert w.text() == "100.0 centimeter"
|
||||||
|
w.setMagnitude(10)
|
||||||
|
assert w.value() == 10 * w.unitRegistry().centimeter
|
||||||
|
assert w.magnitude() == 10
|
||||||
|
assert w.units() == w.unitRegistry().centimeter
|
||||||
|
assert w.text() == "10 centimeter"
|
||||||
|
w.setValue(1 * w.unitRegistry().meter)
|
||||||
|
assert w.value() == 1 * w.unitRegistry().meter
|
||||||
|
assert w.magnitude() == 1
|
||||||
|
assert w.units() == w.unitRegistry().meter
|
||||||
|
assert w.text() == "1 meter"
|
||||||
|
|
||||||
|
w.setUnits(None)
|
||||||
|
assert w.isDimensionless()
|
||||||
|
assert w.unitsComboBox().currentText() == "-----"
|
||||||
|
assert w.magnitude() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_qquantity_value(qtbot):
|
||||||
|
w = QQuantity()
|
||||||
|
qtbot.addWidget(w)
|
||||||
|
assert w.value() == Quantity(0)
|
||||||
|
w.setValue(Quantity("1 meter"))
|
||||||
|
assert w.value() == Quantity("1 meter")
|
@@ -15,15 +15,18 @@ skip_on_linux_qt6 = pytest.mark.skipif(
|
|||||||
reason="hover events not working on linux pyqt6",
|
reason="hover events not working on linux pyqt6",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_PointF = QPointF()
|
||||||
|
|
||||||
def _mouse_event(pos=QPointF(), type_=QEvent.Type.MouseMove):
|
|
||||||
|
def _mouse_event(pos=_PointF, type_=QEvent.Type.MouseMove):
|
||||||
"""Create a mouse event of `type_` at `pos`."""
|
"""Create a mouse event of `type_` at `pos`."""
|
||||||
return QMouseEvent(
|
return QMouseEvent(
|
||||||
type_,
|
type_,
|
||||||
QPointF(pos),
|
QPointF(pos), # localPos
|
||||||
Qt.MouseButton.LeftButton,
|
QPointF(), # windowPos / globalPos
|
||||||
Qt.MouseButton.LeftButton,
|
Qt.MouseButton.LeftButton, # button
|
||||||
Qt.KeyboardModifier.NoModifier,
|
Qt.MouseButton.LeftButton, # buttons
|
||||||
|
Qt.KeyboardModifier.NoModifier, # modifiers
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
5
tox.ini
5
tox.ini
@@ -1,5 +1,5 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py{37,38,39,310}-{linux,macos,windows}-{pyqt5,pyside2,pyqt6,pyside6},py37-linux-{pyqt512,pyqt513,pyqt514}
|
envlist = py{37,38,39,310,311}-{linux,macos,windows}-{pyqt5,pyside2,pyqt6,pyside6},py37-linux-{pyqt512,pyqt513,pyqt514}
|
||||||
toxworkdir=/tmp/.tox
|
toxworkdir=/tmp/.tox
|
||||||
isolated_build=True
|
isolated_build=True
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ python =
|
|||||||
3.8: py38
|
3.8: py38
|
||||||
3.9: py39
|
3.9: py39
|
||||||
3.10: py310
|
3.10: py310
|
||||||
|
3.11: py311
|
||||||
|
|
||||||
[gh-actions:env]
|
[gh-actions:env]
|
||||||
PLATFORM =
|
PLATFORM =
|
||||||
@@ -54,7 +55,7 @@ deps =
|
|||||||
pyqt514: pyqt5==5.14.*
|
pyqt514: pyqt5==5.14.*
|
||||||
pyside514: pyside2==5.14.*
|
pyside514: pyside2==5.14.*
|
||||||
extras =
|
extras =
|
||||||
testing
|
test
|
||||||
pyqt5: pyqt5
|
pyqt5: pyqt5
|
||||||
pyside2: pyside2
|
pyside2: pyside2
|
||||||
pyqt6: pyqt6
|
pyqt6: pyqt6
|
||||||
|
Reference in New Issue
Block a user