refactor: update pyproject and ci, add py3.11 test (#132)

* refactor: reorg repo

* fix: include pyi in manifest

* remove extra

* changes

* why no trigger

* fix needs

* include python 3.11

* remove cache

* add back license

* bump versions

* fix py37

* fix napari test

* remove timeout

* fix py37 test

* test: fix py311 tests

* change windows test
This commit is contained in:
Talley Lambert
2022-11-08 20:32:47 -05:00
committed by GitHub
parent 3ece7a27b1
commit 3c8b5bcf98
21 changed files with 377 additions and 324 deletions

10
.github/dependabot.yml vendored Normal file
View 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):"

View File

@@ -3,13 +3,11 @@ name: Test
on:
push:
branches:
- master
- main
tags:
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
pull_request:
branches:
- master
- main
workflow_dispatch:
@@ -17,60 +15,54 @@ jobs:
test:
name: ${{ matrix.platform }} py${{ matrix.python-version }} ${{ matrix.backend }}
runs-on: ${{ matrix.platform }}
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
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]
include:
# 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
backend: pyside6
screenshot: 1
- python-version: 3.9
- python-version: "3.10"
platform: windows-latest
backend: pyside6
screenshot: 1
- python-version: 3.9
platform: macos-11.0
- python-version: "3.10"
platform: macos-latest
backend: pyside6
screenshot: 1
- python-version: 3.9
- python-version: "3.11"
platform: ubuntu-latest
backend: pyqt6
- python-version: 3.9
- python-version: "3.11"
platform: windows-latest
backend: pyqt6
- python-version: 3.9
platform: macos-11.0
backend: pyqt6
# py3.10
- python-version: "3.10"
platform: ubuntu-latest
backend: pyqt5
- python-version: "3.11"
platform: macos-latest
backend: pyside6
- python-version: "3.10"
platform: ubuntu-latest
# python 3.7
- python-version: 3.7
platform: macos-latest
backend: pyqt5
- python-version: "3.10"
platform: ubuntu-latest
backend: pyqt6
# big sur, 3.9
- python-version: 3.9
platform: macos-11.0
- python-version: 3.7
platform: windows-latest
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
- python-version: 3.7
platform: ubuntu-latest
@@ -83,14 +75,19 @@ jobs:
backend: pyqt514
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 }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- uses: tlambert03/setup-qt-libs@v1
- uses: tlambert03/setup-qt-libs@v1.4
- name: Linux opengl
if: runner.os == 'Linux' && ( matrix.backend == 'pyside6' || matrix.backend == 'pyqt6' )
@@ -111,7 +108,7 @@ jobs:
BACKEND: ${{ matrix.backend }}
- name: Coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v2
- name: Install for screenshots
if: matrix.screenshot
@@ -137,24 +134,23 @@ jobs:
name: qtpy minreq
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: tlambert03/setup-qt-libs@v1
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: tlambert03/setup-qt-libs@v1.4
- uses: actions/setup-python@v4
with:
python-version: '3.8'
python-version: "3.8"
- name: install
run: |
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
- name: Test napari magicgui
- name: Test
uses: GabrielBB/xvfb-action@v1
with:
run: python -m pytest --color=yes
test_napari:
name: napari tests
runs-on: ubuntu-latest
@@ -173,7 +169,7 @@ jobs:
- uses: tlambert03/setup-qt-libs@v1
- uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: "3.10"
- name: install
run: |
@@ -187,30 +183,27 @@ jobs:
working-directory: napari-repo
run: python -m pytest --color=yes napari/_qt
check_manifest:
check-manifest:
name: Check Manifest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Check manifest
run: |
python -m pip install --upgrade pip
pip install check-manifest
check-manifest
- run: pip install check-manifest && check-manifest
deploy:
# this will run when you have tagged a commit, starting with "v*"
# and requires that you have put your twine API key in your
# github secrets (see readme for details)
needs: [test, check_manifest]
needs: [test, check-manifest]
if: ${{ github.repository == 'napari/superqt' && contains(github.ref, 'tags') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Install dependencies

View File

@@ -2,40 +2,58 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-docstring-first
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.2.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
rev: v1.7.7
hooks:
- id: autoflake
args: ["--in-place", "--remove-all-unused-imports"]
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/asottile/pyupgrade
rev: v2.34.0
hooks:
- id: pyupgrade
args: [--py37-plus, --keep-runtime-typing]
- repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- 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
rev: v0.982
hooks:
- id: mypy
exclude: examples
stages: [manual]
exclude: tests|examples
stages:
- manual

View File

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

View File

@@ -39,7 +39,10 @@ def define_env(env: "MacrosPlugin"):
exec(src)
_grab(dest, width)
return f"![{page.title}](../{dest.parent.name}/{dest.name}){{ loading=lazy; width={width} }}\n\n"
return (
f"![{page.title}](../{dest.parent.name}/{dest.name})"
f"{{ loading=lazy; width={width} }}\n\n"
)
@env.macro
def show_members(cls: str):

View File

@@ -1,10 +1,175 @@
# pyproject.toml
# https://peps.python.org/pep-0517/
[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"
# 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]
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]
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",
]

123
setup.cfg
View File

@@ -1,123 +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 :: 3.11
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
quantity =
pint
testing =
pint
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

View File

@@ -8,7 +8,7 @@ from qtpy.QtWidgets import QFrame, QPushButton, QVBoxLayout, QWidget
class QCollapsible(QFrame):
"""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 = ""

View File

@@ -11,7 +11,7 @@ NONE_STRING = "----"
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 (
enum_value.__str__.__module__ != "enum"
and not enum_value.__str__.__module__.startswith("shibokensupport")
@@ -91,7 +91,8 @@ class QEnumComboBox(QComboBox):
return
if not isinstance(value, self._enum_class):
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))

View File

@@ -43,16 +43,17 @@ def icon(
opacity: float = 1,
animation: Optional[Animation] = None,
transform: Optional[QTransform] = None,
states: Dict[str, Union[IconOptionDict, IconOpts]] = {},
states: Dict[str, Union[IconOptionDict, IconOpts]] | None = None,
) -> QFontIcon:
"""Create a QIcon for `glyph_key`, with a number of optional settings
The `glyph_key` (e.g. 'fa5s.smile') represents a Font-family & style, and a glpyh.
In most cases, the key should be provided by a plugin in the environment, like:
- [fonticon-fontawesome5](https://pypi.org/project/fonticon-fontawesome5/) ('fa5s' & 'fa5r' prefixes)
- [fonticon-materialdesignicons6](https://pypi.org/project/fonticon-materialdesignicons6/) ('mdi6' prefix)
- [fonticon-fontawesome5](https://pypi.org/project/fonticon-fontawesome5/) ('fa5s' &
'fa5r' prefixes)
- [fonticon-materialdesignicons6](https://pypi.org/project/fonticon-materialdesignicons6/)
('mdi6' prefix)
...but fonts can also be added manually using [`addFont`][superqt.fonticon.addFont].
@@ -137,7 +138,7 @@ def icon(
>>> btn.setIconSize(QSize(256, 256))
>>> btn.show()
"""
""" # noqa: E501
return _QFIS.instance().icon(
glyph_key,
scale_factor=scale_factor,
@@ -145,7 +146,7 @@ def icon(
opacity=opacity,
animation=animation,
transform=transform,
states=states,
states=states or {},
)
@@ -218,7 +219,7 @@ def addFont(
Tuple[str, str], optional
font-family and font-style for the file just registered, or `None` if
something goes wrong.
"""
""" # noqa: E501
return _QFIS.instance().addFont(filepath, prefix, charmap)

View File

@@ -502,7 +502,7 @@ class QFontIconStore(QObject):
opacity: float = 1,
animation: Optional[Animation] = None,
transform: Optional[QTransform] = None,
states: Dict[str, Union[IconOptionDict, IconOpts]] = {},
states: Dict[str, Union[IconOptionDict, IconOpts]] | None = None,
) -> QFontIcon:
self.key2glyph(glyph_key) # make sure it's a valid glyph_key
default_opts = _IconOptions(
@@ -514,7 +514,7 @@ class QFontIconStore(QObject):
transform=transform,
)
icon = QFontIcon(default_opts)
for kw, options in states.items():
for kw, options in (states or {}).items():
if isinstance(options, IconOpts):
options = default_opts._update(options).dict()
icon.addState(*_norm_state_mode(kw), **options)

View File

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

View File

@@ -80,11 +80,11 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
self._bar_is_rigid = bool(val)
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
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)
def barIsVisible(self) -> bool:

View File

@@ -6,7 +6,8 @@ from pygments.lexers import find_lexer_class, get_lexer_by_name
from pygments.util import ClassNotFound
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
@@ -88,7 +89,8 @@ class CodeSyntaxHighlight(QtGui.QSyntaxHighlighter):
# dirty, dirty hack
# 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)):
try:
self.setFormat(i, 1, self.formatter.data[p + i - enters])

View File

@@ -1,7 +1,9 @@
# https://gist.github.com/FlorianRhiem/41a1ad9b694c14fb9ac3
from __future__ import annotations
from concurrent.futures import Future
from functools import wraps
from typing import Callable, List, Optional
from typing import TYPE_CHECKING, Callable, List, Optional, overload
from qtpy.QtCore import (
QCoreApplication,
@@ -13,10 +15,18 @@ from qtpy.QtCore import (
Slot,
)
if TYPE_CHECKING:
from typing import TypeVar
from typing_extensions import Literal, ParamSpec
P = ParamSpec("P")
R = TypeVar("R")
class CallCallable(QObject):
finished = Signal(object)
instances: List["CallCallable"] = []
instances: List[CallCallable] = []
def __init__(self, callable, *args, **kwargs):
super().__init__()
@@ -32,6 +42,32 @@ class CallCallable(QObject):
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(
func: Optional[Callable] = None, await_return: bool = False, timeout: int = 1000
):
@@ -65,9 +101,33 @@ def ensure_main_thread(
return _func
if func is None:
return _out_func
return _out_func(func)
return _out_func if func is None else _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(
@@ -98,9 +158,7 @@ def ensure_object_thread(
return _func
if func is None:
return _out_func
return _out_func(func)
return _out_func if func is None else _out_func(func)
def _run_in_thread(
@@ -121,5 +179,5 @@ def _run_in_thread(
f = CallCallable(func, *args, **kwargs)
f.moveToThread(thread)
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

View File

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

View File

@@ -207,9 +207,9 @@ class WorkerBase(QRunnable, Generic[_R]):
The end-user should never need to call this function.
But subclasses must implement this method (See
[`GeneratorFunction.work`][superqt.utils._qthreading.GeneratorWorker.work] for an example implementation).
Minimally, it should check `self.abort_requested` periodically and
exit if True.
[`GeneratorFunction.work`][superqt.utils._qthreading.GeneratorWorker.work] for
an example implementation). Minimally, it should check `self.abort_requested`
periodically and exit if True.
Examples
--------
@@ -670,8 +670,10 @@ def thread_worker(
):
"""Decorator that runs a function in a separate thread when called.
When called, the decorated function returns a [`WorkerBase`][superqt.utils.WorkerBase]. See
[`create_worker`][superqt.utils.create_worker] for additional keyword arguments that can be used
When called, the decorated function returns a
[`WorkerBase`][superqt.utils.WorkerBase]. See
[`create_worker`][superqt.utils.create_worker] for additional keyword arguments that
can be used
when calling the function.
The returned worker will have these signals:
@@ -715,8 +717,9 @@ def thread_worker(
worker class. by default None
worker_class : Type[WorkerBase]
The [`WorkerBase`][superqt.utils.WorkerBase] to instantiate, by default
[`FunctionWorker`][superqt.utils.FunctionWorker] will be used if `func` is a regular function,
and [`GeneratorWorker`][superqt.utils.GeneratorWorker] will be used if it is a generator.
[`FunctionWorker`][superqt.utils.FunctionWorker] will be used if `func` is a
regular function, and [`GeneratorWorker`][superqt.utils.GeneratorWorker] will be
used if it is a generator.
ignore_errors : bool
If `False` (the default), errors raised in the other thread will be
reraised in the main thread (makes debugging significantly easier).

View File

@@ -371,10 +371,10 @@ def _make_decorator(
throttle.throttle()
return future
setattr(inner, "cancel", throttle.cancel)
setattr(inner, "flush", throttle.flush)
setattr(inner, "set_timeout", throttle.setTimeout)
setattr(inner, "triggered", throttle.triggered)
setattr(inner, "cancel", throttle.cancel) # noqa
setattr(inner, "flush", throttle.flush) # noqa
setattr(inner, "set_timeout", throttle.setTimeout) # noqa
setattr(inner, "triggered", throttle.triggered) # noqa
return inner # type: ignore
return deco(func) if func is not None else deco

View File

@@ -28,9 +28,9 @@ def test_message_handler_with_logger(caplog):
QtCore.qCritical("critical")
assert len(caplog.records) == 3
caplog.records[0].message == "debug"
caplog.records[0].levelno == logging.DEBUG
caplog.records[1].message == "warning"
caplog.records[1].levelno == logging.WARNING
caplog.records[2].message == "critical"
caplog.records[2].levelno == logging.CRITICAL
assert caplog.records[0].message == "debug"
assert caplog.records[0].levelno == logging.DEBUG
assert caplog.records[1].message == "warning"
assert caplog.records[1].levelno == logging.WARNING
assert caplog.records[2].message == "critical"
assert caplog.records[2].levelno == logging.CRITICAL

View File

@@ -15,8 +15,10 @@ skip_on_linux_qt6 = pytest.mark.skipif(
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`."""
return QMouseEvent(
type_,

View File

@@ -1,5 +1,5 @@
[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
isolated_build=True
@@ -21,6 +21,7 @@ python =
3.8: py38
3.9: py39
3.10: py310
3.11: py311
[gh-actions:env]
PLATFORM =
@@ -54,7 +55,7 @@ deps =
pyqt514: pyqt5==5.14.*
pyside514: pyside2==5.14.*
extras =
testing
test
pyqt5: pyqt5
pyside2: pyside2
pyqt6: pyqt6