From 3c8b5bcf982615089a8213fe66dbed74ff5ac513 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 8 Nov 2022 20:32:47 -0500 Subject: [PATCH] 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 --- .github/dependabot.yml | 10 ++ .github/workflows/test_and_deploy.yml | 109 ++++++------ .pre-commit-config.yaml | 44 +++-- MANIFEST.in | 17 -- docs/_macros.py | 5 +- pyproject.toml | 171 ++++++++++++++++++- setup.cfg | 123 ------------- src/superqt/collapsible/_collapsible.py | 2 +- src/superqt/combobox/_enum_combobox.py | 5 +- src/superqt/fonticon/__init__.py | 15 +- src/superqt/fonticon/_qfont_icon.py | 4 +- src/superqt/sliders/__init__.pyi | 12 -- src/superqt/sliders/_generic_range_slider.py | 4 +- src/superqt/utils/_code_syntax_highlight.py | 6 +- src/superqt/utils/_ensure_thread.py | 76 ++++++++- src/superqt/utils/_ensure_thread.pyi | 52 ------ src/superqt/utils/_qthreading.py | 17 +- src/superqt/utils/_throttler.py | 8 +- tests/test_qmessage_handler.py | 12 +- tests/test_sliders/_testutil.py | 4 +- tox.ini | 5 +- 21 files changed, 377 insertions(+), 324 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 MANIFEST.in delete mode 100644 setup.cfg delete mode 100644 src/superqt/sliders/__init__.pyi delete mode 100644 src/superqt/utils/_ensure_thread.pyi diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..96505a9 --- /dev/null +++ b/.github/dependabot.yml @@ -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):" diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 27fb023..8dc4796 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fe7a023..437794b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index d33d4c7..0000000 --- a/MANIFEST.in +++ /dev/null @@ -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 diff --git a/docs/_macros.py b/docs/_macros.py index 9d2694c..7a49c84 100644 --- a/docs/_macros.py +++ b/docs/_macros.py @@ -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): diff --git a/pyproject.toml b/pyproject.toml index 4654cea..db7cc52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2575ebd..0000000 --- a/setup.cfg +++ /dev/null @@ -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 diff --git a/src/superqt/collapsible/_collapsible.py b/src/superqt/collapsible/_collapsible.py index f5325dc..37b47b9 100644 --- a/src/superqt/collapsible/_collapsible.py +++ b/src/superqt/collapsible/_collapsible.py @@ -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 = "▼ " diff --git a/src/superqt/combobox/_enum_combobox.py b/src/superqt/combobox/_enum_combobox.py index 75d4c50..80abc5f 100644 --- a/src/superqt/combobox/_enum_combobox.py +++ b/src/superqt/combobox/_enum_combobox.py @@ -11,7 +11,7 @@ NONE_STRING = "----" def _get_name(enum_value: Enum): - """Create human readable name if user does not 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)) diff --git a/src/superqt/fonticon/__init__.py b/src/superqt/fonticon/__init__.py index 6062ab0..e4c7b55 100644 --- a/src/superqt/fonticon/__init__.py +++ b/src/superqt/fonticon/__init__.py @@ -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) diff --git a/src/superqt/fonticon/_qfont_icon.py b/src/superqt/fonticon/_qfont_icon.py index 50575b2..e8517a1 100644 --- a/src/superqt/fonticon/_qfont_icon.py +++ b/src/superqt/fonticon/_qfont_icon.py @@ -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) diff --git a/src/superqt/sliders/__init__.pyi b/src/superqt/sliders/__init__.pyi deleted file mode 100644 index c54dcf3..0000000 --- a/src/superqt/sliders/__init__.pyi +++ /dev/null @@ -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): ... diff --git a/src/superqt/sliders/_generic_range_slider.py b/src/superqt/sliders/_generic_range_slider.py index 6087747..d46ad1d 100644 --- a/src/superqt/sliders/_generic_range_slider.py +++ b/src/superqt/sliders/_generic_range_slider.py @@ -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: diff --git a/src/superqt/utils/_code_syntax_highlight.py b/src/superqt/utils/_code_syntax_highlight.py index f2640aa..11d137c 100644 --- a/src/superqt/utils/_code_syntax_highlight.py +++ b/src/superqt/utils/_code_syntax_highlight.py @@ -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]) diff --git a/src/superqt/utils/_ensure_thread.py b/src/superqt/utils/_ensure_thread.py index 86876fb..a29632f 100644 --- a/src/superqt/utils/_ensure_thread.py +++ b/src/superqt/utils/_ensure_thread.py @@ -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 diff --git a/src/superqt/utils/_ensure_thread.pyi b/src/superqt/utils/_ensure_thread.pyi deleted file mode 100644 index 8717c45..0000000 --- a/src/superqt/utils/_ensure_thread.pyi +++ /dev/null @@ -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]]: ... diff --git a/src/superqt/utils/_qthreading.py b/src/superqt/utils/_qthreading.py index eb8eab8..8ef9c2d 100644 --- a/src/superqt/utils/_qthreading.py +++ b/src/superqt/utils/_qthreading.py @@ -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). diff --git a/src/superqt/utils/_throttler.py b/src/superqt/utils/_throttler.py index 97563a2..86e18f1 100644 --- a/src/superqt/utils/_throttler.py +++ b/src/superqt/utils/_throttler.py @@ -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 diff --git a/tests/test_qmessage_handler.py b/tests/test_qmessage_handler.py index beeca9f..36e5644 100644 --- a/tests/test_qmessage_handler.py +++ b/tests/test_qmessage_handler.py @@ -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 diff --git a/tests/test_sliders/_testutil.py b/tests/test_sliders/_testutil.py index 15994fd..d73fe91 100644 --- a/tests/test_sliders/_testutil.py +++ b/tests/test_sliders/_testutil.py @@ -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_, diff --git a/tox.ini b/tox.ini index 638b331..e61104d 100644 --- a/tox.ini +++ b/tox.ini @@ -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